Merge "Implement animation for TRANSIT_MINIMIZE" into main
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index fea2b7b..a92a543 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -18,7 +18,7 @@
                tests/
                tools/
 bpfmt = -d
-ktfmt = --kotlinlang-style --include-dirs=services/permission,packages/SystemUI
+ktfmt = --kotlinlang-style --include-dirs=services/permission,packages/SystemUI,libs/WindowManager/Shell/src/com/android/wm/shell/freeform
 
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
diff --git a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
index 33f6899..ecb9a73 100644
--- a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
+++ b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
@@ -444,8 +444,13 @@
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (action == null) {
+                return;
+            }
+
             final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
-            switch (intent.getAction()) {
+            switch (action) {
                 case Intent.ACTION_USER_REMOVED:
                     if (userId > 0) {
                         mHandler.doUserRemoved(userId);
diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
index a37779e..3e650da 100644
--- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
@@ -666,7 +666,12 @@
 
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override public void onReceive(Context context, Intent intent) {
-            switch (intent.getAction()) {
+            final String action = intent.getAction();
+            if (action == null) {
+                return;
+            }
+
+            switch (action) {
                 case ConnectivityManager.CONNECTIVITY_ACTION: {
                     updateConnectivityState(intent);
                 } break;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
index e3ac780..7a21697 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
@@ -84,9 +84,13 @@
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (action == null) {
+                return;
+            }
+
             final String pkgName = getPackageName(intent);
             final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1);
-            final String action = intent.getAction();
             if (pkgUid == -1) {
                 Slog.e(TAG, "Didn't get package UID in intent (" + action + ")");
                 return;
diff --git a/core/api/current.txt b/core/api/current.txt
index 3e179e7..8bd4367 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9665,6 +9665,47 @@
 
 }
 
+package android.app.wallpaper {
+
+  @FlaggedApi("android.app.live_wallpaper_content_handling") public final class WallpaperDescription implements android.os.Parcelable {
+    method public int describeContents();
+    method @Nullable public android.content.ComponentName getComponent();
+    method @NonNull public android.os.PersistableBundle getContent();
+    method @Nullable public CharSequence getContextDescription();
+    method @Nullable public android.net.Uri getContextUri();
+    method @NonNull public java.util.List<java.lang.CharSequence> getDescription();
+    method @Nullable public String getId();
+    method @Nullable public android.net.Uri getThumbnail();
+    method @Nullable public CharSequence getTitle();
+    method @NonNull public android.app.wallpaper.WallpaperDescription.Builder toBuilder();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @Nullable public static final android.os.Parcelable.Creator<android.app.wallpaper.WallpaperDescription> CREATOR;
+  }
+
+  public static final class WallpaperDescription.Builder {
+    ctor public WallpaperDescription.Builder();
+    method @NonNull public android.app.wallpaper.WallpaperDescription build();
+    method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setContent(@NonNull android.os.PersistableBundle);
+    method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setContextDescription(@Nullable CharSequence);
+    method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setContextUri(@Nullable android.net.Uri);
+    method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setDescription(@NonNull java.util.List<java.lang.CharSequence>);
+    method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setId(@Nullable String);
+    method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setThumbnail(@Nullable android.net.Uri);
+    method @NonNull public android.app.wallpaper.WallpaperDescription.Builder setTitle(@Nullable CharSequence);
+  }
+
+  @FlaggedApi("android.app.live_wallpaper_content_handling") public final class WallpaperInstance implements android.os.Parcelable {
+    ctor public WallpaperInstance(@Nullable android.app.WallpaperInfo, @NonNull android.app.wallpaper.WallpaperDescription);
+    method public int describeContents();
+    method @NonNull public android.app.wallpaper.WallpaperDescription getDescription();
+    method @NonNull public String getId();
+    method @Nullable public android.app.WallpaperInfo getInfo();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.wallpaper.WallpaperInstance> CREATOR;
+  }
+
+}
+
 package android.appwidget {
 
   public class AppWidgetHost {
@@ -10873,6 +10914,7 @@
     field public static final String MEDIA_COMMUNICATION_SERVICE = "media_communication";
     field public static final String MEDIA_METRICS_SERVICE = "media_metrics";
     field public static final String MEDIA_PROJECTION_SERVICE = "media_projection";
+    field @FlaggedApi("android.media.tv.flags.media_quality_fw") public static final String MEDIA_QUALITY_SERVICE = "media_quality";
     field public static final String MEDIA_ROUTER_SERVICE = "media_router";
     field public static final String MEDIA_SESSION_SERVICE = "media_session";
     field public static final String MIDI_SERVICE = "midi";
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 5225174..50cd267 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -7681,7 +7681,9 @@
         });
 
         // Register callback to report native memory metrics post GC cleanup
-        if (Flags.reportPostgcMemoryMetricsReadonly() &&
+        // Note: we do not report memory metrics of isolated processes unless
+        // their native allocations become more significant
+        if (!Process.isIsolated() && Flags.reportPostgcMemoryMetrics() &&
             com.android.libcore.readonly.Flags.postCleanupApis()) {
             VMRuntime.addPostCleanupCallback(new Runnable() {
                 @Override public void run() {
diff --git a/core/java/android/app/IUiAutomationConnection.aidl b/core/java/android/app/IUiAutomationConnection.aidl
old mode 100644
new mode 100755
index 9dcfe89..ec5cf75
--- a/core/java/android/app/IUiAutomationConnection.aidl
+++ b/core/java/android/app/IUiAutomationConnection.aidl
@@ -61,6 +61,8 @@
     oneway void shutdown();
     void executeShellCommandWithStderr(String command, in ParcelFileDescriptor sink,
                 in ParcelFileDescriptor source, in ParcelFileDescriptor stderrSink);
+    void executeShellCommandArrayWithStderr(in String[] command, in ParcelFileDescriptor sink,
+                in ParcelFileDescriptor source, in ParcelFileDescriptor stderrSink);
     List<String> getAdoptedShellPermissions();
     void addOverridePermissionState(int uid, String permission, int result);
     void removeOverridePermissionState(int uid, String permission);
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 62ae6ac..26bfec3 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -953,7 +953,12 @@
     public static void setTestMode(boolean mode) {
         synchronized (sGlobalLock) {
             if (sTestMode == mode) {
-                throw new IllegalStateException("cannot set test mode redundantly: mode=" + mode);
+                final String msg = "cannot set test mode redundantly: mode=" + mode;
+                if (Flags.enforcePicTestmodeProtocol()) {
+                    throw new IllegalStateException(msg);
+                } else {
+                    Log.e(TAG, msg);
+                }
             }
             sTestMode = mode;
             if (mode) {
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index c6b8f3b..e451116 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -150,6 +150,8 @@
 import android.media.musicrecognition.IMusicRecognitionManager;
 import android.media.musicrecognition.MusicRecognitionManager;
 import android.media.projection.MediaProjectionManager;
+import android.media.quality.IMediaQualityManager;
+import android.media.quality.MediaQualityManager;
 import android.media.soundtrigger.SoundTriggerManager;
 import android.media.tv.ITvInputManager;
 import android.media.tv.TvInputManager;
@@ -1775,6 +1777,19 @@
                         }
                     });
         }
+        registerService(Context.MEDIA_QUALITY_SERVICE, MediaQualityManager.class,
+                new CachedServiceFetcher<MediaQualityManager>() {
+                    @Override
+                    public MediaQualityManager createService(ContextImpl ctx)
+                            throws ServiceNotFoundException {
+                        IBinder iBinder = ServiceManager
+                                .getServiceOrThrow(Context.MEDIA_QUALITY_SERVICE);
+                        IMediaQualityManager service = IMediaQualityManager
+                                .Stub.asInterface(iBinder);
+                        return new MediaQualityManager(ctx, service);
+                    }
+                });
+
         sInitializing = true;
         try {
             // Note: the following functions need to be @SystemApis, once they become mainline
diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java
old mode 100644
new mode 100755
index 12f2081..d4bad39
--- a/core/java/android/app/UiAutomationConnection.java
+++ b/core/java/android/app/UiAutomationConnection.java
@@ -565,7 +565,12 @@
             // Rethrow the exception. This will be propagated to the remote caller.
             throw ex;
         }
+        handleExecuteShellCommandProcess(process, sink, source, stderrSink);
+    }
 
+    private void handleExecuteShellCommandProcess(final java.lang.Process process,
+            final ParcelFileDescriptor sink, final ParcelFileDescriptor source,
+            final ParcelFileDescriptor stderrSink) {
         // Read from process and write to pipe
         final Thread readFromProcess;
         if (sink != null) {
@@ -628,6 +633,26 @@
     }
 
     @Override
+    public void executeShellCommandArrayWithStderr(final String[] command,
+            final ParcelFileDescriptor sink, final ParcelFileDescriptor source,
+            final ParcelFileDescriptor stderrSink) throws RemoteException {
+        synchronized (mLock) {
+            throwIfCalledByNotTrustedUidLocked();
+            throwIfShutdownLocked();
+            throwIfNotConnectedLocked();
+        }
+        final java.lang.Process process;
+
+        try {
+            process = Runtime.getRuntime().exec(command);
+        } catch (IOException exc) {
+            throw new RuntimeException(
+                    "Error running shell command '" + String.join(" ", command) + "'", exc);
+        }
+        handleExecuteShellCommandProcess(process, sink, source, stderrSink);
+    }
+
+    @Override
     public void shutdown() {
         synchronized (mLock) {
             if (isConnectedLocked()) {
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index 5b478d09..8944bb9 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -47,10 +47,10 @@
  * <p>An app function is a piece of functionality that apps expose to the system for cross-app
  * orchestration.
  *
- * <p>**Developer Workflow:**
+ * <p>**Building App Functions:**
  *
- * <p>Most developers should interact with app functions through the AppFunctions SDK. This SDK
- * library offers a more convenient and type-safe way to represent the inputs and outputs of an app
+ * <p>Most developers should build app functions through the AppFunctions SDK. This SDK library
+ * offers a more convenient and type-safe way to represent the inputs and outputs of an app
  * function, using custom data classes called "AppFunction Schemas".
  *
  * <p>The suggested way to build an app function is to use the AppFunctions SDK. The SDK provides
diff --git a/core/java/android/app/metrics.aconfig b/core/java/android/app/metrics.aconfig
index 55d9c2d..488f1c7 100644
--- a/core/java/android/app/metrics.aconfig
+++ b/core/java/android/app/metrics.aconfig
@@ -8,12 +8,3 @@
      description: "Controls whether to report memory metrics post GC cleanup"
      bug: "331243037"
 }
-
-flag {
-     namespace: "system_performance"
-     name: "report_postgc_memory_metrics_readonly"
-     is_exported: false
-     description: "Controls whether to report memory metrics post GC cleanup (readonly)"
-     bug: "331243037"
-     is_fixed_read_only: true
-}
diff --git a/core/java/android/app/performance.aconfig b/core/java/android/app/performance.aconfig
index 7c6989e..7484a85 100644
--- a/core/java/android/app/performance.aconfig
+++ b/core/java/android/app/performance.aconfig
@@ -9,3 +9,12 @@
      description: "PropertyInvalidatedCache uses shared memory for nonces."
      bug: "366552454"
 }
+
+flag {
+     namespace: "system_performance"
+     name: "enforce_pic_testmode_protocol"
+     is_exported: true
+     is_fixed_read_only: true
+     description: "Enforce PropertyInvalidatedCache.setTestMode() protocol"
+     bug: "366552454"
+}
diff --git a/core/java/android/app/wallpaper.aconfig b/core/java/android/app/wallpaper.aconfig
index c5bd56f..4b880d0 100644
--- a/core/java/android/app/wallpaper.aconfig
+++ b/core/java/android/app/wallpaper.aconfig
@@ -14,3 +14,11 @@
   description: "Fixes timing of wallpaper changed notification and adds extra information. Only effective after rebooting."
   bug: "369814294"
 }
+
+flag {
+  name: "live_wallpaper_content_handling"
+  namespace: "systemui"
+  description: "Support for user-generated content in live wallpapers. Only effective after rebooting."
+  bug: "347235611"
+  is_exported: true
+}
diff --git a/core/java/android/app/wallpaper/WallpaperDescription.aidl b/core/java/android/app/wallpaper/WallpaperDescription.aidl
new file mode 100644
index 0000000..8c959b8
--- /dev/null
+++ b/core/java/android/app/wallpaper/WallpaperDescription.aidl
@@ -0,0 +1,20 @@
+
+/*
+** 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 android.app.wallpaper;
+
+parcelable WallpaperDescription;
diff --git a/core/java/android/app/wallpaper/WallpaperDescription.java b/core/java/android/app/wallpaper/WallpaperDescription.java
new file mode 100644
index 0000000..dedcb48
--- /dev/null
+++ b/core/java/android/app/wallpaper/WallpaperDescription.java
@@ -0,0 +1,426 @@
+/*
+ * 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.app.wallpaper;
+
+import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING;
+
+import android.annotation.FlaggedApi;
+import android.app.WallpaperInfo;
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Describes a wallpaper, including associated metadata and optional content to be used by its
+ * {@link android.service.wallpaper.WallpaperService.Engine}, the {@link ComponentName} to be used
+ * by {@link android.app.WallpaperManager}, and an optional id to differentiate between different
+ * distinct wallpapers rendered by the same wallpaper service.
+ *
+ * <p>This class is used to communicate among a wallpaper rendering service, a wallpaper chooser UI,
+ * and {@link android.app.WallpaperManager}. This class describes a specific instance of a live
+ * wallpaper, unlike {@link WallpaperInfo} which is common to all instances of a wallpaper
+ * component. Each {@link WallpaperDescription} can have distinct metadata.
+ * </p>
+ */
+@FlaggedApi(FLAG_LIVE_WALLPAPER_CONTENT_HANDLING)
+public final class WallpaperDescription implements Parcelable {
+    private static final String TAG = "WallpaperDescription";
+    private static final  String XML_TAG_CONTENT = "content";
+    private static final String XML_TAG_DESCRIPTION = "description";
+
+    @Nullable private final ComponentName mComponent;
+    @Nullable private final String mId;
+    @Nullable private final Uri mThumbnail;
+    @Nullable private final CharSequence mTitle;
+    @NonNull private final List<CharSequence> mDescription;
+    @Nullable private final Uri mContextUri;
+    @Nullable private final CharSequence mContextDescription;
+    @NonNull private final PersistableBundle mContent;
+
+    private WallpaperDescription(@Nullable ComponentName component,
+            @Nullable String id, @Nullable Uri thumbnail, @Nullable CharSequence title,
+            @Nullable List<CharSequence> description, @Nullable Uri contextUri,
+            @Nullable CharSequence contextDescription,
+            @Nullable PersistableBundle content) {
+        this.mComponent = component;
+        this.mId = id;
+        this.mThumbnail = thumbnail;
+        this.mTitle = title;
+        this.mDescription = (description != null) ? description : new ArrayList<>();
+        this.mContextUri = contextUri;
+        this.mContextDescription = contextDescription;
+        this.mContent = (content != null) ? content : new PersistableBundle();
+    }
+
+    /** @return the component for this wallpaper, or {@code null} for a static wallpaper */
+    @Nullable public ComponentName getComponent() {
+        return mComponent;
+    }
+
+    /** @return the id for this wallpaper, or {@code null} if not provided */
+    @Nullable public String getId() {
+        return mId;
+    }
+
+    /** @return the thumbnail for this wallpaper, or {@code null} if not provided */
+    @Nullable public Uri getThumbnail() {
+        return mThumbnail;
+    }
+
+    /**
+     * @return the title for this wallpaper, with each list element intended to be a separate
+     * line, or {@code null} if not provided
+     */
+    @Nullable public CharSequence getTitle() {
+        return mTitle;
+    }
+
+    /** @return the description for this wallpaper */
+    @NonNull
+    public List<CharSequence> getDescription() {
+        return new ArrayList<>();
+    }
+
+    /** @return the {@link Uri} for the action associated with the wallpaper, or {@code null} if not
+     * provided */
+    @Nullable public Uri getContextUri() {
+        return mContextUri;
+    }
+
+    /** @return the description for the action associated with the wallpaper, or {@code null} if not
+     * provided */
+    @Nullable public CharSequence getContextDescription() {
+        return mContextDescription;
+    }
+
+    /** @return any additional content required to render this wallpaper */
+    @NonNull
+    public PersistableBundle getContent() {
+        return mContent;
+    }
+
+    ////// Comparison overrides
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof WallpaperDescription that)) return false;
+        return Objects.equals(mComponent, that.mComponent) && Objects.equals(mId,
+                that.mId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mComponent, mId);
+    }
+
+    ////// XML storage
+
+    /** @hide */
+    public void saveToXml(TypedXmlSerializer out) throws IOException, XmlPullParserException {
+        if (mComponent != null) {
+            out.attribute(null, "component", mComponent.flattenToShortString());
+        }
+        if (mId != null) out.attribute(null, "id", mId);
+        if (mThumbnail != null) out.attribute(null, "thumbnail", mThumbnail.toString());
+        if (mTitle != null) out.attribute(null, "title", toHtml(mTitle));
+        if (mContextUri != null) out.attribute(null, "contexturi", mContextUri.toString());
+        if (mContextDescription != null) {
+            out.attribute(null, "contextdescription", toHtml(mContextDescription));
+        }
+        out.startTag(null, XML_TAG_DESCRIPTION);
+        for (CharSequence s : mDescription) out.attribute(null, "descriptionline", toHtml(s));
+        out.endTag(null, XML_TAG_DESCRIPTION);
+        try {
+            out.startTag(null, XML_TAG_CONTENT);
+            mContent.saveToXml(out);
+        } catch (XmlPullParserException e) {
+            // Be extra conservative and don't fail when writing content since it could come
+            // from third parties
+            Log.e(TAG, "unable to convert wallpaper content to XML");
+        } finally {
+            out.endTag(null, XML_TAG_CONTENT);
+        }
+    }
+
+    /** @hide */
+    public static WallpaperDescription restoreFromXml(TypedXmlPullParser in) throws IOException,
+            XmlPullParserException {
+        final int outerDepth = in.getDepth();
+        String component = in.getAttributeValue(null, "component");
+        ComponentName componentName = (component != null) ? ComponentName.unflattenFromString(
+                component) : null;
+        String id = in.getAttributeValue(null, "id");
+        String thumbnailString = in.getAttributeValue(null, "thumbnail");
+        Uri thumbnail = (thumbnailString != null) ? Uri.parse(thumbnailString) : null;
+        CharSequence title = fromHtml(in.getAttributeValue(null, "title"));
+        String contextUriString = in.getAttributeValue(null, "contexturi");
+        Uri contextUri = (contextUriString != null) ? Uri.parse(contextUriString) : null;
+        CharSequence contextDescription = fromHtml(
+                in.getAttributeValue(null, "contextdescription"));
+
+        List<CharSequence> description = new ArrayList<>();
+        PersistableBundle content = null;
+        int type;
+        while ((type = in.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || in.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+            String name = in.getName();
+            if (XML_TAG_DESCRIPTION.equals(name)) {
+                for (int i = 0; i < in.getAttributeCount(); i++) {
+                    description.add(fromHtml(in.getAttributeValue(i)));
+                }
+            } else if (XML_TAG_CONTENT.equals(name)) {
+                content = PersistableBundle.restoreFromXml(in);
+            }
+        }
+
+        return new WallpaperDescription(componentName, id, thumbnail, title, description,
+                contextUri, contextDescription, content);
+    }
+
+    private static String toHtml(@NonNull CharSequence c) {
+        Spanned s = (c instanceof Spanned) ? (Spanned) c : new SpannedString(c);
+        return Html.toHtml(s, Html.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL);
+    }
+
+    private static CharSequence fromHtml(@Nullable String text) {
+        if (text == null) {
+            return  null;
+        } else {
+            return removeTrailingWhitespace(Html.fromHtml(text, Html.FROM_HTML_MODE_COMPACT));
+        }
+    }
+
+    // Html.fromHtml and toHtml add a trailing line. This removes it. See
+    // https://stackoverflow.com/q/9589381
+    private static CharSequence removeTrailingWhitespace(CharSequence s) {
+        if (s == null) return null;
+
+        int end = s.length();
+        while (end > 0 && Character.isWhitespace(s.charAt(end - 1))) {
+            end--;
+        }
+
+        return s.subSequence(0, end);
+    }
+
+    ////// Parcelable implementation
+
+    WallpaperDescription(@NonNull Parcel in) {
+        mComponent = ComponentName.readFromParcel(in);
+        mId = in.readString8();
+        mThumbnail = Uri.CREATOR.createFromParcel(in);
+        mTitle = in.readCharSequence();
+        mDescription = Arrays.stream(in.readCharSequenceArray()).toList();
+        mContextUri = Uri.CREATOR.createFromParcel(in);
+        mContextDescription = in.readCharSequence();
+        mContent = PersistableBundle.CREATOR.createFromParcel(in);
+    }
+
+    @Nullable
+    public static final Creator<WallpaperDescription> CREATOR = new Creator<>() {
+        @Override
+        public WallpaperDescription createFromParcel(Parcel source) {
+            return new WallpaperDescription(source);
+        }
+
+        @Override
+        public WallpaperDescription[] newArray(int size) {
+            return new WallpaperDescription[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        ComponentName.writeToParcel(mComponent, dest);
+        dest.writeString8(mId);
+        Uri.writeToParcel(dest, mThumbnail);
+        dest.writeCharSequence(mTitle);
+        dest.writeCharSequenceArray(mDescription.toArray(new CharSequence[0]));
+        Uri.writeToParcel(dest, mContextUri);
+        dest.writeCharSequence(mContextDescription);
+        dest.writePersistableBundle(mContent);
+    }
+
+    ////// Builder
+
+    /**
+     * Convert the current description to a {@link Builder}.
+     * @return the Builder representing this description
+     */
+    @NonNull
+    public Builder toBuilder() {
+        return new Builder().setComponent(mComponent).setId(mId).setThumbnail(mThumbnail).setTitle(
+                mTitle).setDescription(mDescription).setContextUri(
+                mContextUri).setContextDescription(mContextDescription).setContent(mContent);
+    }
+
+    /** Builder for the immutable {@link WallpaperDescription} class */
+    public static final class Builder {
+        @Nullable private ComponentName mComponent;
+        @Nullable private String mId;
+        @Nullable private Uri mThumbnail;
+        @Nullable private CharSequence mTitle;
+        @NonNull private List<CharSequence> mDescription = new ArrayList<>();
+        @Nullable private Uri mContextUri;
+        @Nullable private CharSequence mContextDescription;
+        @NonNull private PersistableBundle mContent = new PersistableBundle();
+
+        /** Creates a new, empty {@link Builder}. */
+        public Builder() {}
+
+        /**
+         * Specify the component for this wallpaper.
+         *
+         * <p>This method is hidden because only trusted apps should be able to specify the
+         * component, which names a wallpaper service to be started by the system.
+         * </p>
+         *
+         * @param component component name, or {@code null} for static wallpaper
+         * @hide
+         */
+        @NonNull
+        public Builder setComponent(@Nullable ComponentName component) {
+            mComponent = component;
+            return this;
+        }
+
+        /**
+         * Set the id for this wallpaper.
+         *
+         * <p>IDs are used to distinguish among different instances of wallpapers rendered by the
+         * same component, and should be unique among all wallpapers for that component.
+         * </p>
+         *
+         * @param id the id, or {@code null} for none
+         */
+        @NonNull
+        public Builder setId(@Nullable String id) {
+            mId = id;
+            return this;
+        }
+
+        /**
+         * Set the thumbnail Uri for this wallpaper.
+         *
+         * @param thumbnail the thumbnail Uri, or {@code null} for none
+         */
+        @NonNull
+        public Builder setThumbnail(@Nullable Uri thumbnail) {
+            mThumbnail = thumbnail;
+            return this;
+        }
+
+        /**
+         * Set the title for this wallpaper.
+         *
+         * @param title the title, or {@code null} for none
+         */
+        @NonNull
+        public Builder setTitle(@Nullable CharSequence title) {
+            mTitle = title;
+            return this;
+        }
+
+        /**
+         * Set the description for this wallpaper. Each array element should be shown on a
+         * different line.
+         *
+         * @param description the description, or an empty list for none
+         */
+        @NonNull
+        public Builder setDescription(@NonNull List<CharSequence> description) {
+            mDescription = description;
+            return this;
+        }
+
+        /**
+         * Set the Uri for the action associated with this wallpaper, to be shown as a link with the
+         * wallpaper information.
+         *
+         * @param contextUri the action Uri, or {@code null} for no action
+         */
+        @NonNull
+        public Builder setContextUri(@Nullable Uri contextUri) {
+            mContextUri = contextUri;
+            return this;
+        }
+
+        /**
+         * Set the link text for the action associated with this wallpaper.
+         *
+         * @param contextDescription the link text, or {@code null} for default text
+         */
+        @NonNull
+        public Builder setContextDescription(@Nullable CharSequence contextDescription) {
+            mContextDescription = contextDescription;
+            return this;
+        }
+
+        /**
+         * Set the additional content required to render this wallpaper.
+         *
+         * <p>When setting additional content (asset id, etc.), best practice is to set an ID as
+         * well. This allows WallpaperManager and other code to distinguish between different
+         * wallpapers handled by this component.
+         * </p>
+         *
+         * @param content additional content, or an empty bundle for none
+         */
+        @NonNull
+        public Builder setContent(@NonNull PersistableBundle content) {
+            mContent = content;
+            return this;
+        }
+
+        /** Creates and returns the {@link WallpaperDescription} represented by this builder. */
+        @NonNull
+        public WallpaperDescription build() {
+            return new WallpaperDescription(mComponent, mId, mThumbnail, mTitle, mDescription,
+                    mContextUri, mContextDescription, mContent);
+        }
+    }
+}
diff --git a/core/java/android/app/wallpaper/WallpaperInstance.aidl b/core/java/android/app/wallpaper/WallpaperInstance.aidl
new file mode 100644
index 0000000..15a15be
--- /dev/null
+++ b/core/java/android/app/wallpaper/WallpaperInstance.aidl
@@ -0,0 +1,20 @@
+
+/*
+** 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 android.app.wallpaper;
+
+parcelable WallpaperInstance;
diff --git a/core/java/android/app/wallpaper/WallpaperInstance.java b/core/java/android/app/wallpaper/WallpaperInstance.java
new file mode 100644
index 0000000..48a649b
--- /dev/null
+++ b/core/java/android/app/wallpaper/WallpaperInstance.java
@@ -0,0 +1,162 @@
+/*
+ * 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.app.wallpaper;
+
+import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING;
+
+import android.annotation.FlaggedApi;
+import android.app.WallpaperInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Describes a wallpaper that has been set as a current wallpaper.
+ *
+ * <p>This class is used by {@link android.app.WallpaperManager} to store information about a
+ * wallpaper that is currently in use. Because it has been set as an active wallpaper it offers
+ * some guarantees that {@link WallpaperDescription} does not:
+ * <ul>
+ *     <li>It contains the {@link WallpaperInfo} corresponding to the
+ *     {@link android.content.ComponentName}</li> specified in the description
+ *     <li>{@link #getId()} is guaranteed to be non-null</li>
+ * </ul>
+ * </p>
+ */
+@FlaggedApi(FLAG_LIVE_WALLPAPER_CONTENT_HANDLING)
+public final class WallpaperInstance implements Parcelable {
+    private static final String DEFAULT_ID = "default_id";
+    @Nullable private final WallpaperInfo mInfo;
+    @NonNull private final WallpaperDescription mDescription;
+    @Nullable private final String mIdOverride;
+
+    /**
+     * Create a WallpaperInstance for the wallpaper given by {@link WallpaperDescription}.
+     *
+     * @param info the live wallpaper info for this wallpaper, or null if static
+     * @param description description of the wallpaper for this instance
+     */
+    public WallpaperInstance(@Nullable WallpaperInfo info,
+            @NonNull WallpaperDescription description) {
+        this(info, description, null);
+    }
+
+    /**
+     * Create a WallpaperInstance for the wallpaper given by {@link WallpaperDescription}.
+     *
+     * This is provided as an escape hatch to provide an explicit id for cases where the
+     * description id and {@link WallpaperInfo} are both {@code null}.
+     *
+     * @param info the live wallpaper info for this wallpaper, or null if static
+     * @param description description of the wallpaper for this instance
+     * @param idOverride optional id to override the value given in the description
+     *
+     * @hide
+     */
+    public WallpaperInstance(@Nullable WallpaperInfo info,
+            @NonNull WallpaperDescription description, @Nullable String idOverride) {
+        mInfo = info;
+        mDescription = description;
+        mIdOverride = idOverride;
+    }
+
+    /** @return the live wallpaper info, or {@code null} if static */
+    @Nullable public WallpaperInfo getInfo() {
+        return mInfo;
+    }
+
+    /**
+     * See {@link WallpaperDescription.Builder#getId()} for rules about id uniqueness.
+     *
+     * @return the ID of the wallpaper instance if given by the wallpaper description, otherwise a
+     * default value
+     */
+    @NonNull public String getId() {
+        if (mIdOverride != null) {
+            return mIdOverride;
+        } else if (mDescription.getId() != null) {
+            return mDescription.getId();
+        } else if (mInfo != null) {
+            return mInfo.getComponent().flattenToString();
+        } else {
+            return DEFAULT_ID;
+        }
+    }
+
+    /** @return the description for this wallpaper */
+    @NonNull public WallpaperDescription getDescription() {
+        return mDescription;
+    }
+
+    ////// Comparison overrides
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof WallpaperInstance that)) return false;
+        if (mInfo == null) {
+            return that.mInfo == null && Objects.equals(getId(), that.getId());
+        } else {
+            return that.mInfo != null
+                    && Objects.equals(mInfo.getComponent(), that.mInfo.getComponent())
+                    && Objects.equals(getId(), that.getId());
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return (mInfo != null) ? Objects.hash(mInfo.getComponent(), getId()) : Objects.hash(
+                getId());
+    }
+
+    ////// Parcelable implementation
+
+    WallpaperInstance(@NonNull Parcel in) {
+        mInfo = in.readTypedObject(WallpaperInfo.CREATOR);
+        mDescription = WallpaperDescription.CREATOR.createFromParcel(in);
+        mIdOverride = in.readString8();
+    }
+
+    @NonNull
+    public static final Creator<WallpaperInstance> CREATOR = new Creator<>() {
+        @Override
+        public WallpaperInstance createFromParcel(Parcel in) {
+            return new WallpaperInstance(in);
+        }
+
+        @Override
+        public WallpaperInstance[] newArray(int size) {
+            return new WallpaperInstance[size];
+        }
+    };
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeTypedObject(mInfo, flags);
+        mDescription.writeToParcel(dest, flags);
+        dest.writeString8(mIdOverride);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+}
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 07106e8..5893da3 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4325,6 +4325,7 @@
            //@hide: ECM_ENHANCED_CONFIRMATION_SERVICE,
             CONTACT_KEYS_SERVICE,
             RANGING_SERVICE,
+            MEDIA_QUALITY_SERVICE,
             ADVANCED_PROTECTION_SERVICE,
 
     })
@@ -6767,6 +6768,17 @@
     public static final String SUPERVISION_SERVICE = "supervision";
 
     /**
+     * Use with {@link #getSystemService(String)} to retrieve a
+     * {@link android.media.quality.MediaQuality} for standardize picture and audio
+     * API parameters.
+     *
+     * @see #getSystemService(String)
+     * @see android.media.quality.MediaQuality
+     */
+    @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+    public static final String MEDIA_QUALITY_SERVICE = "media_quality";
+
+    /**
      * Determine whether the given permission is allowed for a particular
      * process and user ID running in the system.
      *
diff --git a/core/java/android/content/pm/parsing/ApkLite.java b/core/java/android/content/pm/parsing/ApkLite.java
index 4220590..c83cf96 100644
--- a/core/java/android/content/pm/parsing/ApkLite.java
+++ b/core/java/android/content/pm/parsing/ApkLite.java
@@ -156,6 +156,12 @@
      */
     private final @Nullable String[][] mUsesSdkLibrariesCertDigests;
 
+    private final @NonNull List<String> mUsesStaticLibraries;
+
+    private final @Nullable long[] mUsesStaticLibrariesVersions;
+
+    private final @Nullable String[][] mUsesStaticLibrariesCertDigests;
+
     /**
      * Indicates if this system app can be updated.
      */
@@ -185,7 +191,10 @@
             Set<String> requiredSplitTypes, Set<String> splitTypes,
             boolean hasDeviceAdminReceiver, boolean isSdkLibrary,
             List<String> usesSdkLibraries, long[] usesSdkLibrariesVersionsMajor,
-            String[][] usesSdkLibrariesCertDigests, boolean updatableSystem,
+            String[][] usesSdkLibrariesCertDigests,
+            List<String> usesStaticLibraries, long[] usesStaticLibrariesVersionsMajor,
+            String[][] usesStaticLibrariesCertDigests,
+            boolean updatableSystem,
             String emergencyInstaller, List<SharedLibraryInfo> declaredLibraries) {
         mPath = path;
         mPackageName = packageName;
@@ -223,6 +232,9 @@
         mUsesSdkLibraries = usesSdkLibraries;
         mUsesSdkLibrariesVersionsMajor = usesSdkLibrariesVersionsMajor;
         mUsesSdkLibrariesCertDigests = usesSdkLibrariesCertDigests;
+        mUsesStaticLibraries = usesStaticLibraries;
+        mUsesStaticLibrariesVersions = usesStaticLibrariesVersionsMajor;
+        mUsesStaticLibrariesCertDigests = usesStaticLibrariesCertDigests;
         mUpdatableSystem = updatableSystem;
         mEmergencyInstaller = emergencyInstaller;
         mArchivedPackage = null;
@@ -266,6 +278,9 @@
         mUsesSdkLibraries = Collections.emptyList();
         mUsesSdkLibrariesVersionsMajor = null;
         mUsesSdkLibrariesCertDigests = null;
+        mUsesStaticLibraries = Collections.emptyList();
+        mUsesStaticLibrariesVersions = null;
+        mUsesStaticLibrariesCertDigests = null;
         mUpdatableSystem = true;
         mEmergencyInstaller = null;
         mArchivedPackage = archivedPackage;
@@ -602,6 +617,21 @@
         return mUsesSdkLibrariesCertDigests;
     }
 
+    @DataClass.Generated.Member
+    public @NonNull List<String> getUsesStaticLibraries() {
+        return mUsesStaticLibraries;
+    }
+
+    @DataClass.Generated.Member
+    public @Nullable long[] getUsesStaticLibrariesVersions() {
+        return mUsesStaticLibrariesVersions;
+    }
+
+    @DataClass.Generated.Member
+    public @Nullable String[][] getUsesStaticLibrariesCertDigests() {
+        return mUsesStaticLibrariesCertDigests;
+    }
+
     /**
      * Indicates if this system app can be updated.
      */
@@ -632,10 +662,10 @@
     }
 
     @DataClass.Generated(
-            time = 1729247366948L,
+            time = 1730202160705L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/content/pm/parsing/ApkLite.java",
-            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final  int mVersionCodeMajor\nprivate final  int mVersionCode\nprivate final  int mRevisionCode\nprivate final  int mInstallLocation\nprivate final  int mMinSdkVersion\nprivate final  int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final  boolean mFeatureSplit\nprivate final  boolean mIsolatedSplits\nprivate final  boolean mSplitRequired\nprivate final  boolean mCoreApp\nprivate final  boolean mDebuggable\nprivate final  boolean mProfileableByShell\nprivate final  boolean mMultiArch\nprivate final  boolean mUse32bitAbi\nprivate final  boolean mExtractNativeLibs\nprivate final  boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final  boolean mOverlayIsStatic\nprivate final  int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final  int mRollbackDataPolicy\nprivate final  boolean mHasDeviceAdminReceiver\nprivate final  boolean mIsSdkLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final  boolean mUpdatableSystem\nprivate final @android.annotation.Nullable java.lang.String mEmergencyInstaller\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic  long getLongVersionCode()\nprivate  boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
+            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final  int mVersionCodeMajor\nprivate final  int mVersionCode\nprivate final  int mRevisionCode\nprivate final  int mInstallLocation\nprivate final  int mMinSdkVersion\nprivate final  int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final  boolean mFeatureSplit\nprivate final  boolean mIsolatedSplits\nprivate final  boolean mSplitRequired\nprivate final  boolean mCoreApp\nprivate final  boolean mDebuggable\nprivate final  boolean mProfileableByShell\nprivate final  boolean mMultiArch\nprivate final  boolean mUse32bitAbi\nprivate final  boolean mExtractNativeLibs\nprivate final  boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final  boolean mOverlayIsStatic\nprivate final  int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final  int mRollbackDataPolicy\nprivate final  boolean mHasDeviceAdminReceiver\nprivate final  boolean mIsSdkLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final  boolean mUpdatableSystem\nprivate final @android.annotation.Nullable java.lang.String mEmergencyInstaller\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic  long getLongVersionCode()\nprivate  boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
index 50d8758..34c3f57 100644
--- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
+++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
@@ -91,6 +91,7 @@
     private static final String TAG_USES_SPLIT = "uses-split";
     private static final String TAG_MANIFEST = "manifest";
     private static final String TAG_USES_SDK_LIBRARY = "uses-sdk-library";
+    private static final String TAG_USES_STATIC_LIBRARY = "uses-static-library";
     private static final String TAG_SDK_LIBRARY = "sdk-library";
     private static final int SDK_VERSION = Build.VERSION.SDK_INT;
     private static final String[] SDK_CODENAMES = Build.VERSION.ACTIVE_CODENAMES;
@@ -466,6 +467,11 @@
         List<String> usesSdkLibraries = new ArrayList<>();
         long[] usesSdkLibrariesVersionsMajor = new long[0];
         String[][] usesSdkLibrariesCertDigests = new String[0][0];
+
+        List<String> usesStaticLibraries = new ArrayList<>();
+        long[] usesStaticLibrariesVersions = new long[0];
+        String[][] usesStaticLibrariesCertDigests = new String[0][0];
+
         List<SharedLibraryInfo> declaredLibraries = new ArrayList<>();
 
         // Only search the tree when the tag is the direct child of <manifest> tag
@@ -580,6 +586,51 @@
                                     usesSdkLibrariesCertDigests, new String[]{usesSdkCertDigest},
                                     /*allowDuplicates=*/ true);
                             break;
+                        case TAG_USES_STATIC_LIBRARY:
+                            String usesStaticLibName = parser.getAttributeValue(
+                                    ANDROID_RES_NAMESPACE, "name");
+                            long usesStaticLibVersion = parser.getAttributeIntValue(
+                                    ANDROID_RES_NAMESPACE, "version", -1);
+                            String usesStaticLibCertDigest = parser.getAttributeValue(
+                                    ANDROID_RES_NAMESPACE, "certDigest");
+
+                            if (usesStaticLibName == null || usesStaticLibName.isBlank()
+                                    || usesStaticLibVersion < 0
+                                    || usesStaticLibCertDigest == null) {
+                                return input.error(
+                                        PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
+                                        "Bad uses-static-library declaration name: "
+                                                + usesStaticLibName
+                                                + " version: " + usesStaticLibVersion
+                                                + " certDigest: " + usesStaticLibCertDigest);
+                            }
+
+                            if (usesStaticLibraries.contains(usesStaticLibName)) {
+                                return input.error(
+                                        PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
+                                        "Bad uses-sdk-library declaration. Depending on"
+                                                + " multiple versions of static library: "
+                                                + usesStaticLibName);
+                            }
+
+                            usesStaticLibraries.add(usesStaticLibName);
+                            usesStaticLibrariesVersions = ArrayUtils.appendLong(
+                                    usesStaticLibrariesVersions, usesStaticLibVersion,
+                                    /*allowDuplicates=*/ true);
+
+                            // We allow ":" delimiters in the SHA declaration as this is the format
+                            // emitted by the certtool making it easy for developers to copy/paste.
+                            // TODO(372862145): Add test for this replacement
+                            usesStaticLibCertDigest =
+                                    usesStaticLibCertDigest.replace(":", "").toLowerCase();
+
+                            // TODO(372862145): Add support for multiple signer for app targeting
+                            //  O-MR1
+                            usesStaticLibrariesCertDigests = ArrayUtils.appendElement(
+                                    String[].class, usesStaticLibrariesCertDigests,
+                                    new String[]{usesStaticLibCertDigest},
+                                    /*allowDuplicates=*/ true);
+                            break;
                         case TAG_SDK_LIBRARY:
                             isSdkLibrary = true;
                             // Mirrors ParsingPackageUtils#parseSdkLibrary until lite and full
@@ -753,7 +804,9 @@
                         rollbackDataPolicy, requiredSplitTypes.first, requiredSplitTypes.second,
                         hasDeviceAdminReceiver, isSdkLibrary, usesSdkLibraries,
                         usesSdkLibrariesVersionsMajor, usesSdkLibrariesCertDigests,
-                        updatableSystem, emergencyInstaller, declaredLibraries));
+                        usesStaticLibraries, usesStaticLibrariesVersions,
+                        usesStaticLibrariesCertDigests, updatableSystem, emergencyInstaller,
+                        declaredLibraries));
     }
 
     private static boolean isDeviceAdminReceiver(
diff --git a/core/java/android/content/pm/parsing/PackageLite.java b/core/java/android/content/pm/parsing/PackageLite.java
index 79c5973..76b25fe 100644
--- a/core/java/android/content/pm/parsing/PackageLite.java
+++ b/core/java/android/content/pm/parsing/PackageLite.java
@@ -121,6 +121,12 @@
 
     private final @Nullable String[][] mUsesSdkLibrariesCertDigests;
 
+    private final @NonNull List<String> mUsesStaticLibraries;
+
+    private final @Nullable long[] mUsesStaticLibrariesVersions;
+
+    private final @Nullable String[][] mUsesStaticLibrariesCertDigests;
+
     private final @NonNull List<SharedLibraryInfo> mDeclaredLibraries;
 
     /**
@@ -158,6 +164,9 @@
         mUsesSdkLibraries = baseApk.getUsesSdkLibraries();
         mUsesSdkLibrariesVersionsMajor = baseApk.getUsesSdkLibrariesVersionsMajor();
         mUsesSdkLibrariesCertDigests = baseApk.getUsesSdkLibrariesCertDigests();
+        mUsesStaticLibraries = baseApk.getUsesStaticLibraries();
+        mUsesStaticLibrariesVersions = baseApk.getUsesStaticLibrariesVersions();
+        mUsesStaticLibrariesCertDigests = baseApk.getUsesStaticLibrariesCertDigests();
         mSplitNames = splitNames;
         mSplitTypes = splitTypes;
         mIsFeatureSplits = isFeatureSplits;
@@ -462,6 +471,21 @@
     }
 
     @DataClass.Generated.Member
+    public @NonNull List<String> getUsesStaticLibraries() {
+        return mUsesStaticLibraries;
+    }
+
+    @DataClass.Generated.Member
+    public @Nullable long[] getUsesStaticLibrariesVersions() {
+        return mUsesStaticLibrariesVersions;
+    }
+
+    @DataClass.Generated.Member
+    public @Nullable String[][] getUsesStaticLibrariesCertDigests() {
+        return mUsesStaticLibrariesCertDigests;
+    }
+
+    @DataClass.Generated.Member
     public @NonNull List<SharedLibraryInfo> getDeclaredLibraries() {
         return mDeclaredLibraries;
     }
@@ -475,10 +499,10 @@
     }
 
     @DataClass.Generated(
-            time = 1729248757933L,
+            time = 1730203707341L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/content/pm/parsing/PackageLite.java",
-            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.NonNull java.lang.String mBaseApkPath\nprivate final @android.annotation.Nullable java.lang.String[] mSplitApkPaths\nprivate final @android.annotation.Nullable java.lang.String[] mSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mUsesSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mBaseRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mSplitTypes\nprivate final  int mVersionCodeMajor\nprivate final  int mVersionCode\nprivate final  int mTargetSdk\nprivate final  int mBaseRevisionCode\nprivate final @android.annotation.Nullable int[] mSplitRevisionCodes\nprivate final  int mInstallLocation\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final @android.annotation.Nullable boolean[] mIsFeatureSplits\nprivate final  boolean mIsolatedSplits\nprivate final  boolean mSplitRequired\nprivate final  boolean mCoreApp\nprivate final  boolean mDebuggable\nprivate final  boolean mMultiArch\nprivate final  boolean mUse32bitAbi\nprivate final  boolean mExtractNativeLibs\nprivate final  boolean mProfileableByShell\nprivate final  boolean mUseEmbeddedDex\nprivate final  boolean mIsSdkLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic  java.util.List<java.lang.String> getAllApkPaths()\npublic  long getLongVersionCode()\nprivate  boolean hasAnyRequiredSplitTypes()\nclass PackageLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
+            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.NonNull java.lang.String mBaseApkPath\nprivate final @android.annotation.Nullable java.lang.String[] mSplitApkPaths\nprivate final @android.annotation.Nullable java.lang.String[] mSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mUsesSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mBaseRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mSplitTypes\nprivate final  int mVersionCodeMajor\nprivate final  int mVersionCode\nprivate final  int mTargetSdk\nprivate final  int mBaseRevisionCode\nprivate final @android.annotation.Nullable int[] mSplitRevisionCodes\nprivate final  int mInstallLocation\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final @android.annotation.Nullable boolean[] mIsFeatureSplits\nprivate final  boolean mIsolatedSplits\nprivate final  boolean mSplitRequired\nprivate final  boolean mCoreApp\nprivate final  boolean mDebuggable\nprivate final  boolean mMultiArch\nprivate final  boolean mUse32bitAbi\nprivate final  boolean mExtractNativeLibs\nprivate final  boolean mProfileableByShell\nprivate final  boolean mUseEmbeddedDex\nprivate final  boolean mIsSdkLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic  java.util.List<java.lang.String> getAllApkPaths()\npublic  long getLongVersionCode()\nprivate  boolean hasAnyRequiredSplitTypes()\nclass PackageLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/hardware/input/KeyGlyphMap.java b/core/java/android/hardware/input/KeyGlyphMap.java
index b517a63..6a16502 100644
--- a/core/java/android/hardware/input/KeyGlyphMap.java
+++ b/core/java/android/hardware/input/KeyGlyphMap.java
@@ -158,6 +158,15 @@
         return getDrawable(context, mModifierGlyphs.get(modifier, 0));
     }
 
+    /**
+     * Provides the drawable resource for the glyph for a modifier state (e.g. META_META_ON).
+     * Returns null if not available.
+     */
+    @Nullable
+    public Drawable getDrawableForModifierState(Context context, int modifierState) {
+        return getDrawable(context, mModifierGlyphs.get(modifierState, 0));
+    }
+
     @Nullable
     private Drawable getDrawable(Context context, @DrawableRes int drawableRes) {
         PackageManager pm = context.getPackageManager();
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 49e2358..8c3f0ef 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -603,12 +603,6 @@
     @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public static final long DISALLOW_INPUT_METHOD_INTERFACE_OVERRIDE = 148086656L;
 
-    /**
-     * Enable the logic to allow hiding the IME caption bar ("fake" IME navigation bar).
-     * @hide
-     */
-    public static final boolean ENABLE_HIDE_IME_CAPTION_BAR = true;
-
     LayoutInflater mInflater;
     TypedArray mThemeAttrs;
     @UnsupportedAppUsage
diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java
index 3ce67b0..b08454d 100644
--- a/core/java/android/inputmethodservice/NavigationBarController.java
+++ b/core/java/android/inputmethodservice/NavigationBarController.java
@@ -16,7 +16,6 @@
 
 package android.inputmethodservice;
 
-import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR;
 import static android.view.WindowInsets.Type.captionBar;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 
@@ -260,15 +259,15 @@
             setIconTintInternal(calculateTargetDarkIntensity(mAppearance,
                     mDrawLegacyNavigationBarBackground));
 
-            if (ENABLE_HIDE_IME_CAPTION_BAR) {
-                mNavigationBarFrame.setOnApplyWindowInsetsListener((view, insets) -> {
-                    if (mNavigationBarFrame != null) {
-                        boolean visible = insets.isVisible(captionBar());
-                        mNavigationBarFrame.setVisibility(visible ? View.VISIBLE : View.GONE);
-                    }
-                    return view.onApplyWindowInsets(insets);
-                });
-            }
+            mNavigationBarFrame.setOnApplyWindowInsetsListener((view, insets) -> {
+                if (mNavigationBarFrame != null) {
+                    // The IME window receives IME-specific captionBar insets, representing the
+                    // IME navigation bar.
+                    boolean visible = insets.isVisible(captionBar());
+                    mNavigationBarFrame.setVisibility(visible ? View.VISIBLE : View.GONE);
+                }
+                return view.onApplyWindowInsets(insets);
+            });
         }
 
         private void uninstallNavigationBarFrameIfNecessary() {
@@ -279,9 +278,7 @@
             if (parent instanceof ViewGroup) {
                 ((ViewGroup) parent).removeView(mNavigationBarFrame);
             }
-            if (ENABLE_HIDE_IME_CAPTION_BAR) {
-                mNavigationBarFrame.setOnApplyWindowInsetsListener(null);
-            }
+            mNavigationBarFrame.setOnApplyWindowInsetsListener(null);
             mNavigationBarFrame = null;
         }
 
@@ -474,9 +471,6 @@
                         decor.bringChildToFront(mNavigationBarFrame);
                     }
                 }
-                if (!ENABLE_HIDE_IME_CAPTION_BAR) {
-                    mNavigationBarFrame.setVisibility(View.VISIBLE);
-                }
             }
         }
 
@@ -497,10 +491,8 @@
                     mShouldShowImeSwitcherWhenImeIsShown;
             mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown;
 
-            if (ENABLE_HIDE_IME_CAPTION_BAR) {
-                mService.mWindow.getWindow().getDecorView().getWindowInsetsController()
-                        .setImeCaptionBarInsetsHeight(getImeCaptionBarHeight());
-            }
+            mService.mWindow.getWindow().getDecorView().getWindowInsetsController()
+                    .setImeCaptionBarInsetsHeight(getImeCaptionBarHeight(imeDrawsImeNavBar));
 
             if (imeDrawsImeNavBar) {
                 installNavigationBarFrameIfNecessary();
@@ -608,9 +600,11 @@
 
         /**
          * Returns the height of the IME caption bar if this should be shown, or {@code 0} instead.
+         *
+         * @param imeDrawsImeNavBar whether the IME should show the IME navigation bar.
          */
-        private int getImeCaptionBarHeight() {
-            return mImeDrawsImeNavBar
+        private int getImeCaptionBarHeight(boolean imeDrawsImeNavBar) {
+            return imeDrawsImeNavBar
                     ? mService.getResources().getDimensionPixelSize(
                             com.android.internal.R.dimen.navigation_bar_frame_height)
                     : 0;
diff --git a/core/java/android/net/vcn/OWNERS b/core/java/android/net/vcn/OWNERS
index 2441e77..937699a 100644
--- a/core/java/android/net/vcn/OWNERS
+++ b/core/java/android/net/vcn/OWNERS
@@ -1,7 +1,6 @@
 set noparent
 
-benedictwong@google.com
-ckesting@google.com
 evitayan@google.com
-junyin@google.com
 nharold@google.com
+benedictwong@google.com #{LAST_RESORT_SUGGESTION}
+yangji@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 35d7911..25d2246 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -16,7 +16,6 @@
 
 package android.view;
 
-import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR;
 import static android.os.Trace.TRACE_TAG_VIEW;
 import static android.view.InsetsControllerProto.CONTROL;
 import static android.view.InsetsControllerProto.STATE;
@@ -2148,9 +2147,6 @@
 
     @Override
     public void setImeCaptionBarInsetsHeight(int height) {
-        if (!ENABLE_HIDE_IME_CAPTION_BAR) {
-            return;
-        }
         Rect newFrame = new Rect(mFrame.left, mFrame.bottom - height, mFrame.right, mFrame.bottom);
         InsetsSource source = mState.peekSource(ID_IME_CAPTION_BAR);
         if (mImeCaptionBarInsetsHeight != height
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 966e835..ed7a796 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -340,3 +340,11 @@
     is_fixed_read_only: true
     bug: "362938401"
 }
+
+flag {
+  name: "unify_back_navigation_transition"
+  namespace: "windowing_frontend"
+  description: "Always create predictive back transition when start back gesture animation"
+  bug: "372230928"
+  is_fixed_read_only: true
+}
diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
index 2be7273..30deb49 100644
--- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java
+++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
@@ -190,12 +190,12 @@
                 final float segWidth = segment.mFraction * totalWidth;
                 // Advance the start position to account for a point immediately prior.
                 final float startOffset = getSegStartOffset(prevPart, pointRadius,
-                        mState.mSegPointGap);
+                        mState.mSegPointGap, x);
                 final float start = x + startOffset;
                 // Retract the end position to account for the padding and a point immediately
                 // after.
                 final float endOffset = getSegEndOffset(nextPart, pointRadius, mState.mSegPointGap,
-                        mState.mSegSegGap);
+                        mState.mSegSegGap, x + segWidth, totalWidth);
                 final float end = x + segWidth - endOffset;
 
                 // Transparent is not allowed (and also is the default in the data), so use that
@@ -215,8 +215,17 @@
                 // width (ignoring offset and padding)
                 x += segWidth;
             } else if (part instanceof Point point) {
-                mPointRect.set((int) (x - pointRadius), (int) (centerY - pointRadius),
-                        (int) (x + pointRadius), (int) (centerY + pointRadius));
+                final float pointWidth = 2 * pointRadius;
+                float start = x - pointRadius;
+                if (start < 0) start = 0;
+                float end = start + pointWidth;
+                if (end > totalWidth) {
+                    end = totalWidth;
+                    if (totalWidth > pointWidth) start = totalWidth - pointWidth;
+                }
+                mPointRect.set((int) start, (int) (centerY - pointRadius), (int) end,
+                        (int) (centerY + pointRadius));
+
                 if (point.mIcon != null) {
                     point.mIcon.setBounds(mPointRect);
                     point.mIcon.draw(canvas);
@@ -238,14 +247,22 @@
         }
     }
 
-    private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap) {
-        return (prevPart instanceof Point) ? pointRadius + segPointGap : 0F;
+    private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap,
+            float startX) {
+        if (!(prevPart instanceof Point)) return 0F;
+        final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0;
+        return pointOffset + pointRadius + segPointGap;
     }
 
     private static float getSegEndOffset(Part nextPart, float pointRadius, float segPointGap,
-            float segSegGap) {
+            float segSegGap, float endX, float totalWidth) {
         if (nextPart == null) return 0F;
-        return (nextPart instanceof Point) ? segPointGap + pointRadius : segSegGap;
+        if (!(nextPart instanceof Point)) return segSegGap;
+
+        final float pointWidth = 2 * pointRadius;
+        final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth)
+                ? (endX + pointRadius - totalWidth) : 0;
+        return segPointGap + pointRadius + pointOffset;
     }
 
     @Override
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 4c38246..561d351 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -851,6 +851,7 @@
     <protected-broadcast android:name="android.telephony.action.ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED" />
     <protected-broadcast android:name="android.service.ondeviceintelligence.MODEL_LOADED" />
     <protected-broadcast android:name="android.service.ondeviceintelligence.MODEL_UNLOADED" />
+    <protected-broadcast android:name="android.telephony.action.ACTION_SATELLITE_START_NON_EMERGENCY_SESSION" />
 
 
     <!-- ====================================================================== -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 4111cfd..ac9bb93 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -7143,6 +7143,10 @@
          root TaskDisplayArea is set to WINDOWING_MODE_FREEFORM. -->
     <bool name="config_enterDesktopByDefaultOnFreeformDisplay">false</bool>
 
+    <!-- Whether a desktop window should be maximized when it's dragged to the top edge of the
+         screen. -->
+    <bool name="config_dragToMaximizeInDesktopMode">false</bool>
+
     <!-- Frame rate compatibility value for Wallpaper
          FRAME_RATE_COMPATIBILITY_MIN (102) is used by default for lower power consumption -->
     <integer name="config_wallpaperFrameRateCompatibility">102</integer>
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index b5892f6..779422a 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -472,4 +472,10 @@
     <integer name="config_mt_sms_polling_throttle_millis">300000</integer>
     <java-symbol type="integer" name="config_mt_sms_polling_throttle_millis" />
 
+
+    <!-- The receiver class of the intent that hidden menu sends to start satellite non-emergency mode -->
+    <string name="config_satellite_carrier_roaming_non_emergency_session_class" translatable="false"></string>
+    <java-symbol type="string" name="config_satellite_carrier_roaming_non_emergency_session_class" />
+
+
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d7cbc16..51c405ae 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5577,6 +5577,10 @@
        root TaskDisplayArea is set to WINDOWING_MODE_FREEFORM. -->
   <java-symbol type="bool" name="config_enterDesktopByDefaultOnFreeformDisplay" />
 
+  <!-- Whether a desktop window should be maximized when it's dragged to the top edge of the
+       screen. -->
+  <java-symbol type="bool" name="config_dragToMaximizeInDesktopMode" />
+
   <!-- Frame rate compatibility value for Wallpaper -->
   <java-symbol type="integer" name="config_wallpaperFrameRateCompatibility" />
 
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 54262e8..a382d79 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -150,6 +150,7 @@
         ":HelloWorldUsingSdk1AndSdk1",
         ":HelloWorldUsingSdk1And2",
         ":HelloWorldUsingSdkMalformedNegativeVersion",
+        ":CtsStaticSharedLibConsumerApp1",
     ],
 }
 
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index da7da7d..9675d6b 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -46,6 +46,7 @@
     <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.BIND_WALLPAPER"/>
     <uses-permission android:name="android.permission.BLUETOOTH" />
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
     <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
@@ -1770,6 +1771,25 @@
                 <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
             </intent-filter>
         </activity>
+
+        <!-- Used by WallpaperInstanceTest -->
+        <service
+            android:name="stub.StubWallpaperService"
+            android:directBootAware="true"
+            android:enabled="true"
+            android:exported="true"
+            android:label="Stub wallpaper"
+            android:permission="android.permission.BIND_WALLPAPER">
+
+            <intent-filter>
+                <action android:name="android.service.wallpaper.WallpaperService" />
+            </intent-filter>
+
+            <!-- Link to XML that defines the wallpaper info. -->
+            <meta-data
+                android:name="android.service.wallpaper"
+                android:resource="@xml/livewallpaper" />
+        </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/core/tests/coretests/AndroidTest.xml b/core/tests/coretests/AndroidTest.xml
index 3bc8172..3f7c83a 100644
--- a/core/tests/coretests/AndroidTest.xml
+++ b/core/tests/coretests/AndroidTest.xml
@@ -39,6 +39,8 @@
             value="/data/local/tmp/tests/coretests/pm/HelloWorldUsingSdkMalformedNegativeVersion.apk"/>
         <option name="push-file" key="HelloWorldSdk1.apk"
             value="/data/local/tmp/tests/coretests/pm/HelloWorldSdk1.apk"/>
+        <option name="push-file" key="CtsStaticSharedLibConsumerApp1.apk"
+            value="/data/local/tmp/tests/coretests/pm/CtsStaticSharedLibConsumerApp1.apk"/>
     </target_preparer>
 
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
diff --git a/core/tests/coretests/res/xml/livewallpaper.xml b/core/tests/coretests/res/xml/livewallpaper.xml
new file mode 100644
index 0000000..3b3f4a7
--- /dev/null
+++ b/core/tests/coretests/res/xml/livewallpaper.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<wallpaper
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:settingsSliceUri="content://com.android.frameworks.coretests/slice"
+    android:supportsAmbientMode="true"/>
diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
index e417181..c2d8f91 100644
--- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
+++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
@@ -425,7 +425,9 @@
         PropertyInvalidatedCache.setTestMode(false);
         try {
             PropertyInvalidatedCache.setTestMode(false);
-            fail("expected an IllegalStateException");
+            if (Flags.enforcePicTestmodeProtocol()) {
+                fail("expected an IllegalStateException");
+            }
         } catch (IllegalStateException e) {
             // The expected exception.
         }
diff --git a/core/tests/coretests/src/android/app/wallpaper/OWNERS b/core/tests/coretests/src/android/app/wallpaper/OWNERS
new file mode 100644
index 0000000..93b068d
--- /dev/null
+++ b/core/tests/coretests/src/android/app/wallpaper/OWNERS
@@ -0,0 +1 @@
+include platform/frameworks/base:/core/java/android/service/wallpaper/OWNERS
diff --git a/core/tests/coretests/src/android/app/wallpaper/WallpaperDescriptionTest.java b/core/tests/coretests/src/android/app/wallpaper/WallpaperDescriptionTest.java
new file mode 100644
index 0000000..01c2abf
--- /dev/null
+++ b/core/tests/coretests/src/android/app/wallpaper/WallpaperDescriptionTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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.app.wallpaper;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.PersistableBundle;
+import android.util.Xml;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class WallpaperDescriptionTest {
+    private static final String TAG = "WallpaperDescriptionTest";
+
+    private final ComponentName mTestComponent = new ComponentName("fakePackage", "fakeClass");
+
+    @Test
+    public void equals_ignoresIrrelevantFields() {
+        String id = "fakeId";
+        WallpaperDescription desc1 = new WallpaperDescription.Builder().setComponent(
+                mTestComponent).setId(id).setTitle("fake one").build();
+        WallpaperDescription desc2 = new WallpaperDescription.Builder().setComponent(
+                mTestComponent).setId(id).setTitle("fake different").build();
+
+        assertThat(desc1).isEqualTo(desc2);
+    }
+
+    @Test
+    public void hash_ignoresIrrelevantFields() {
+        String id = "fakeId";
+        WallpaperDescription desc1 = new WallpaperDescription.Builder().setComponent(
+                mTestComponent).setId(id).setTitle("fake one").build();
+        WallpaperDescription desc2 = new WallpaperDescription.Builder().setComponent(
+                mTestComponent).setId(id).setTitle("fake different").build();
+
+        assertThat(desc1.hashCode()).isEqualTo(desc2.hashCode());
+    }
+
+    @Test
+    public void xml_roundTripSucceeds() throws IOException, XmlPullParserException {
+        final Uri thumbnail = Uri.parse("http://www.bogus.com/thumbnail");
+        final List<CharSequence> description = List.of("line1", "line2");
+        final Uri contextUri = Uri.parse("http://www.bogus.com/contextUri");
+        final PersistableBundle content = new PersistableBundle();
+        content.putString("ckey", "cvalue");
+        WallpaperDescription source = new WallpaperDescription.Builder()
+                .setComponent(mTestComponent).setId("fakeId").setThumbnail(thumbnail)
+                .setTitle("Fake title").setDescription(description)
+                .setContextUri(contextUri).setContextDescription("Context description")
+                .setContent(content).build();
+
+        ByteArrayOutputStream ostream = new ByteArrayOutputStream();
+        TypedXmlSerializer serializer = Xml.newBinarySerializer();
+        serializer.setOutput(ostream, StandardCharsets.UTF_8.name());
+        serializer.startDocument(null, true);
+        serializer.startTag(null, "test");
+        source.saveToXml(serializer);
+        serializer.endTag(null, "test");
+        serializer.endDocument();
+        ostream.close();
+
+        WallpaperDescription destination = null;
+        ByteArrayInputStream istream = new ByteArrayInputStream(ostream.toByteArray());
+        TypedXmlPullParser parser = Xml.newBinaryPullParser();
+        parser.setInput(istream, StandardCharsets.UTF_8.name());
+        int type;
+        do {
+            type = parser.next();
+            if (type == XmlPullParser.START_TAG && "test".equals(parser.getName())) {
+                destination = WallpaperDescription.restoreFromXml(parser);
+            }
+        } while (type != XmlPullParser.END_DOCUMENT);
+
+        assertThat(destination).isNotNull();
+        assertThat(destination.getComponent()).isEqualTo(source.getComponent());
+        assertThat(destination.getId()).isEqualTo(source.getId());
+        assertThat(destination.getThumbnail()).isEqualTo(source.getThumbnail());
+        assertWithMessage("title mismatch").that(
+                CharSequence.compare(destination.getTitle(), source.getTitle())).isEqualTo(0);
+        assertThat(destination.getDescription()).hasSize(source.getDescription().size());
+        for (int i = 0; i < destination.getDescription().size(); i++) {
+            CharSequence strDest = destination.getDescription().get(i);
+            CharSequence strSrc = source.getDescription().get(i);
+            assertWithMessage("description string mismatch")
+                    .that(CharSequence.compare(strDest, strSrc)).isEqualTo(0);
+        }
+        assertThat(destination.getContextUri()).isEqualTo(source.getContextUri());
+        assertWithMessage("context description mismatch").that(
+                CharSequence.compare(destination.getContextDescription(),
+                source.getContextDescription())).isEqualTo(0);
+        assertThat(destination.getContent()).isNotNull();
+        assertThat(destination.getContent().getString("ckey")).isEqualTo(
+                source.getContent().getString("ckey"));
+    }
+
+    @Test
+    public void parcel_roundTripSucceeds() {
+        final Uri thumbnail = Uri.parse("http://www.bogus.com/thumbnail");
+        final List<CharSequence> description = List.of("line1", "line2");
+        final Uri contextUri = Uri.parse("http://www.bogus.com/contextUri");
+        final PersistableBundle content = new PersistableBundle();
+        content.putString("ckey", "cvalue");
+        WallpaperDescription source = new WallpaperDescription.Builder().setComponent(
+                mTestComponent).setId("fakeId").setThumbnail(thumbnail).setTitle(
+                "Fake title").setDescription(description).setContextUri(
+                contextUri).setContextDescription("Context description").setContent(
+                content).build();
+
+        Parcel parcel = Parcel.obtain();
+        source.writeToParcel(parcel, 0);
+        // Reset parcel for reading
+        parcel.setDataPosition(0);
+        WallpaperDescription destination = WallpaperDescription.CREATOR.createFromParcel(parcel);
+
+        assertThat(destination.getComponent()).isEqualTo(source.getComponent());
+        assertThat(destination.getId()).isEqualTo(source.getId());
+        assertThat(destination.getThumbnail()).isEqualTo(source.getThumbnail());
+        assertWithMessage("title mismatch").that(
+                CharSequence.compare(destination.getTitle(), source.getTitle())).isEqualTo(0);
+        assertThat(destination.getDescription()).hasSize(source.getDescription().size());
+        for (int i = 0; i < destination.getDescription().size(); i++) {
+            CharSequence strDest = destination.getDescription().get(i);
+            CharSequence strSrc = source.getDescription().get(i);
+            assertWithMessage("description string mismatch")
+                    .that(CharSequence.compare(strDest, strSrc)).isEqualTo(0);
+        }
+        assertThat(destination.getContextUri()).isEqualTo(source.getContextUri());
+        assertWithMessage("context description mismatch").that(
+                CharSequence.compare(destination.getContextDescription(),
+                        source.getContextDescription())).isEqualTo(0);
+        assertThat(destination.getContent()).isNotNull();
+        assertThat(destination.getContent().getString("ckey")).isEqualTo(
+                source.getContent().getString("ckey"));
+    }
+
+    @Test
+    public void parcel_roundTripSucceeds_withNulls() {
+        WallpaperDescription source = new WallpaperDescription.Builder().build();
+
+        Parcel parcel = Parcel.obtain();
+        source.writeToParcel(parcel, 0);
+        // Reset parcel for reading
+        parcel.setDataPosition(0);
+        WallpaperDescription destination = WallpaperDescription.CREATOR.createFromParcel(parcel);
+
+        assertThat(destination.getComponent()).isEqualTo(source.getComponent());
+        assertThat(destination.getId()).isEqualTo(source.getId());
+        assertThat(destination.getThumbnail()).isEqualTo(source.getThumbnail());
+        assertThat(destination.getTitle()).isNull();
+        assertThat(destination.getDescription()).hasSize(source.getDescription().size());
+        for (int i = 0; i < destination.getDescription().size(); i++) {
+            CharSequence strDest = destination.getDescription().get(i);
+            CharSequence strSrc = source.getDescription().get(i);
+            assertWithMessage("description string mismatch")
+                    .that(CharSequence.compare(strDest, strSrc)).isEqualTo(0);
+        }
+        assertThat(destination.getContextUri()).isEqualTo(source.getContextUri());
+        assertThat(destination.getContextDescription()).isNull();
+        assertThat(destination.getContent()).isNotNull();
+        assertThat(destination.getContent().keySet()).isEmpty();
+    }
+
+    @Test
+    public void toBuilder_succeeds() {
+        final String sourceId = "sourceId";
+        final Uri thumbnail = Uri.parse("http://www.bogus.com/thumbnail");
+        final List<CharSequence> description = List.of("line1", "line2");
+        final Uri contextUri = Uri.parse("http://www.bogus.com/contextUri");
+        final PersistableBundle content = new PersistableBundle();
+        content.putString("ckey", "cvalue");
+        final String destinationId = "destinationId";
+        WallpaperDescription source = new WallpaperDescription.Builder().setComponent(
+                mTestComponent).setId(sourceId).setThumbnail(thumbnail).setTitle(
+                "Fake title").setDescription(description).setContextUri(
+                contextUri).setContextDescription("Context description").setContent(
+                content).build();
+
+        WallpaperDescription destination = source.toBuilder().setId(destinationId).build();
+
+        assertThat(destination.getComponent()).isEqualTo(source.getComponent());
+        assertThat(destination.getId()).isEqualTo(destinationId);
+        assertThat(destination.getThumbnail()).isEqualTo(source.getThumbnail());
+        assertWithMessage("title mismatch").that(
+                CharSequence.compare(destination.getTitle(), source.getTitle())).isEqualTo(0);
+        assertThat(destination.getDescription()).hasSize(source.getDescription().size());
+        for (int i = 0; i < destination.getDescription().size(); i++) {
+            CharSequence strDest = destination.getDescription().get(i);
+            CharSequence strSrc = source.getDescription().get(i);
+            assertWithMessage("description string mismatch")
+                    .that(CharSequence.compare(strDest, strSrc)).isEqualTo(0);
+        }
+        assertThat(destination.getContextUri()).isEqualTo(source.getContextUri());
+        assertWithMessage("context description mismatch").that(
+                CharSequence.compare(destination.getContextDescription(),
+                        source.getContextDescription())).isEqualTo(0);
+        assertThat(destination.getContent()).isNotNull();
+        assertThat(destination.getContent().getString("ckey")).isEqualTo(
+                source.getContent().getString("ckey"));
+    }
+}
diff --git a/core/tests/coretests/src/android/app/wallpaper/WallpaperInstanceTest.java b/core/tests/coretests/src/android/app/wallpaper/WallpaperInstanceTest.java
new file mode 100644
index 0000000..d5a8937
--- /dev/null
+++ b/core/tests/coretests/src/android/app/wallpaper/WallpaperInstanceTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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.app.wallpaper;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.WallpaperInfo;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Parcel;
+import android.service.wallpaper.WallpaperService;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class WallpaperInstanceTest {
+    @Test
+    public void equals_bothNullInfo_sameId_isTrue() {
+        WallpaperDescription description = new WallpaperDescription.Builder().setId("123").build();
+        WallpaperInstance instance1 = new WallpaperInstance(null, description);
+        WallpaperInstance instance2 = new WallpaperInstance(null, description);
+
+        assertThat(instance1).isEqualTo(instance2);
+    }
+
+    @Test
+    public void equals_bothNullInfo_differentIds_isFalse() {
+        WallpaperDescription description1 = new WallpaperDescription.Builder().setId("123").build();
+        WallpaperDescription description2 = new WallpaperDescription.Builder().setId("456").build();
+        WallpaperInstance instance1 = new WallpaperInstance(null, description1);
+        WallpaperInstance instance2 = new WallpaperInstance(null, description2);
+
+        assertThat(instance1).isNotEqualTo(instance2);
+    }
+
+    @Test
+    public void equals_singleNullInfo_isFalse() throws Exception {
+        WallpaperDescription description = new WallpaperDescription.Builder().build();
+        WallpaperInstance instance1 = new WallpaperInstance(null, description);
+        WallpaperInstance instance2 = new WallpaperInstance(makeWallpaperInfo(), description);
+
+        assertThat(instance1).isNotEqualTo(instance2);
+    }
+
+    @Test
+    public void equals_sameInfoAndId_isTrue() throws Exception {
+        WallpaperDescription description = new WallpaperDescription.Builder().setId("123").build();
+        WallpaperInstance instance1 = new WallpaperInstance(makeWallpaperInfo(), description);
+        WallpaperInstance instance2 = new WallpaperInstance(makeWallpaperInfo(), description);
+
+        assertThat(instance1).isEqualTo(instance2);
+    }
+
+    @Test
+    public void equals_sameInfo_differentIds_isFalse() throws Exception {
+        WallpaperDescription description1 = new WallpaperDescription.Builder().setId("123").build();
+        WallpaperDescription description2 = new WallpaperDescription.Builder().setId("456").build();
+        WallpaperInstance instance1 = new WallpaperInstance(makeWallpaperInfo(), description1);
+        WallpaperInstance instance2 = new WallpaperInstance(makeWallpaperInfo(), description2);
+
+        assertThat(instance1).isNotEqualTo(instance2);
+    }
+
+    @Test
+    public void hash_nullInfo_works() {
+        WallpaperDescription description1 = new WallpaperDescription.Builder().setId("123").build();
+        WallpaperDescription description2 = new WallpaperDescription.Builder().setId("456").build();
+        WallpaperInstance base = new WallpaperInstance(null, description1);
+        WallpaperInstance sameId = new WallpaperInstance(null, description1);
+        WallpaperInstance differentId = new WallpaperInstance(null, description2);
+
+        assertThat(base.hashCode()).isEqualTo(sameId.hashCode());
+        assertThat(base.hashCode()).isNotEqualTo(differentId.hashCode());
+    }
+
+    @Test
+    public void hash_withInfo_works() throws Exception {
+        WallpaperDescription description1 = new WallpaperDescription.Builder().setId("123").build();
+        WallpaperDescription description2 = new WallpaperDescription.Builder().setId("456").build();
+        WallpaperInstance base = new WallpaperInstance(makeWallpaperInfo(), description1);
+        WallpaperInstance sameId = new WallpaperInstance(makeWallpaperInfo(), description1);
+        WallpaperInstance differentId = new WallpaperInstance(makeWallpaperInfo(), description2);
+
+        assertThat(base.hashCode()).isEqualTo(sameId.hashCode());
+        assertThat(base.hashCode()).isNotEqualTo(differentId.hashCode());
+    }
+
+    @Test
+    public void id_fromOverride() throws Exception {
+        final String id = "override";
+        WallpaperInstance instance = new WallpaperInstance(makeWallpaperInfo(),
+                new WallpaperDescription.Builder().setId("abc123").build(), id);
+
+        assertThat(instance.getId()).isEqualTo(id);
+    }
+
+    @Test
+    public void id_fromDescription() throws Exception {
+        final String id = "abc123";
+        WallpaperInstance instance = new WallpaperInstance(makeWallpaperInfo(),
+                new WallpaperDescription.Builder().setId(id).build());
+
+        assertThat(instance.getId()).isEqualTo(id);
+    }
+
+    @Test
+    public void id_fromComponent() throws Exception {
+        WallpaperInfo info = makeWallpaperInfo();
+        WallpaperInstance instance = new WallpaperInstance(info,
+                new WallpaperDescription.Builder().build());
+
+        assertThat(instance.getId()).isEqualTo(info.getComponent().flattenToString());
+    }
+
+    @Test
+    public void id_default() {
+        WallpaperInstance instance = new WallpaperInstance(null,
+                new WallpaperDescription.Builder().build());
+
+        assertThat(instance.getId()).isNotNull();
+    }
+
+    @Test
+    public void parcel_roundTripSucceeds() throws Exception {
+        WallpaperInstance source = new WallpaperInstance(makeWallpaperInfo(),
+                new WallpaperDescription.Builder().build());
+
+        Parcel parcel = Parcel.obtain();
+        source.writeToParcel(parcel, 0);
+        // Reset parcel for reading
+        parcel.setDataPosition(0);
+
+        WallpaperInstance destination = WallpaperInstance.CREATOR.createFromParcel(parcel);
+
+        assertThat(destination.getInfo()).isNotNull();
+        assertThat(destination.getInfo().getComponent()).isEqualTo(source.getInfo().getComponent());
+        assertThat(destination.getId()).isEqualTo(source.getId());
+        assertThat(destination.getDescription()).isEqualTo(source.getDescription());
+    }
+
+    @Test
+    public void parcel_roundTripSucceeds_withNulls() {
+        WallpaperInstance source = new WallpaperInstance(null,
+                new WallpaperDescription.Builder().build());
+
+        Parcel parcel = Parcel.obtain();
+        source.writeToParcel(parcel, 0);
+        // Reset parcel for reading
+        parcel.setDataPosition(0);
+
+        WallpaperInstance destination = WallpaperInstance.CREATOR.createFromParcel(parcel);
+
+        assertThat(destination.getInfo()).isEqualTo(source.getInfo());
+        assertThat(destination.getId()).isEqualTo(source.getId());
+        assertThat(destination.getDescription()).isEqualTo(source.getDescription());
+    }
+
+    private WallpaperInfo makeWallpaperInfo() throws Exception {
+        Context context = InstrumentationRegistry.getTargetContext();
+        Intent intent = new Intent(WallpaperService.SERVICE_INTERFACE);
+        intent.setPackage("com.android.frameworks.coretests");
+        PackageManager pm = context.getPackageManager();
+        List<ResolveInfo> result = pm.queryIntentServices(intent, PackageManager.GET_META_DATA);
+        assertThat(result).hasSize(1);
+        ResolveInfo info = result.getFirst();
+        return new WallpaperInfo(context, info);
+    }
+}
diff --git a/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java b/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java
index f9b481f..d4618d7 100644
--- a/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java
+++ b/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java
@@ -71,6 +71,7 @@
     private static final String TEST_APP_USING_SDK1_AND_SDK1 = "HelloWorldUsingSdk1AndSdk1.apk";
     private static final String TEST_APP_USING_SDK_MALFORMED_VERSION =
             "HelloWorldUsingSdkMalformedNegativeVersion.apk";
+    private static final String TEST_APP_USING_STATIC_LIB = "CtsStaticSharedLibConsumerApp1.apk";
     private static final String TEST_SDK1 = "HelloWorldSdk1.apk";
     private static final String TEST_SDK1_PACKAGE = "com.test.sdk1_1";
     private static final String TEST_SDK1_NAME = "com.test.sdk1";
@@ -166,6 +167,30 @@
 
     @SuppressLint("CheckResult")
     @Test
+    public void testParseApkLite_getUsesStaticLibrary_sameAsPackageParser() throws Exception {
+        File apkFile = copyApkToTmpDir(TEST_APP_USING_STATIC_LIB);
+        ParseResult<ApkLite> result = ApkLiteParseUtils
+                .parseApkLite(ParseTypeImpl.forDefaultParsing().reset(), apkFile, 0);
+        assertThat(result.isError()).isFalse();
+        ApkLite baseApk = result.getResult();
+
+        AndroidPackage pkg = mPackageParser2.parsePackage(apkFile, 0, true).hideAsFinal();
+        assertThat(baseApk.getUsesStaticLibraries())
+                .containsExactlyElementsIn(pkg.getUsesStaticLibraries());
+        List<Long> versionsBoxed = Arrays.stream(pkg.getUsesStaticLibrariesVersions()).boxed()
+                .toList();
+        assertThat(baseApk.getUsesStaticLibrariesVersions()).asList()
+                .containsExactlyElementsIn(versionsBoxed);
+
+        String[][] liteCerts = baseApk.getUsesStaticLibrariesCertDigests();
+        String[][] pkgCerts = pkg.getUsesStaticLibrariesCertDigests();
+        for (int i = 0; i < liteCerts.length; i++) {
+            assertThat(liteCerts[i]).isEqualTo(pkgCerts[i]);
+        }
+    }
+
+    @SuppressLint("CheckResult")
+    @Test
     public void testParseApkLite_malformedUsesSdkLibrary_duplicate() throws Exception {
         File apkFile = copyApkToTmpDir(TEST_APP_USING_SDK1_AND_SDK1);
         ParseResult<ApkLite> result = ApkLiteParseUtils
diff --git a/core/tests/coretests/src/android/os/IpcDataCacheTest.java b/core/tests/coretests/src/android/os/IpcDataCacheTest.java
index 2fc1ffd6..e14608a 100644
--- a/core/tests/coretests/src/android/os/IpcDataCacheTest.java
+++ b/core/tests/coretests/src/android/os/IpcDataCacheTest.java
@@ -489,7 +489,9 @@
         IpcDataCache.setTestMode(false);
         try {
             IpcDataCache.setTestMode(false);
-            fail("expected an IllegalStateException");
+            if (android.app.Flags.enforcePicTestmodeProtocol()) {
+                fail("expected an IllegalStateException");
+            }
         } catch (IllegalStateException e) {
             // The expected exception.
         }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java
index f466d60..19f837c 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java
@@ -23,7 +23,6 @@
 import android.app.WindowConfiguration;
 import android.content.Context;
 import android.graphics.Rect;
-import android.hardware.display.DisplayManagerGlobal;
 import android.util.RotationUtils;
 import android.view.DisplayInfo;
 import android.view.Surface;
@@ -31,7 +30,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.UiContext;
-import androidx.annotation.VisibleForTesting;
 
 /**
  * Util class for both Sidecar and Extensions.
@@ -46,24 +44,15 @@
      * Rotates the input rectangle specified in default display orientation to the current display
      * rotation.
      *
-     * @param displayId the display id.
+     * @param displayInfo the display information.
      * @param rotation the target rotation relative to the default display orientation.
      * @param inOutRect the input/output Rect as specified in the default display orientation.
      */
-    public static void rotateRectToDisplayRotation(
-            int displayId, @Surface.Rotation int rotation, @NonNull Rect inOutRect) {
-        final DisplayManagerGlobal dmGlobal = DisplayManagerGlobal.getInstance();
-        final DisplayInfo displayInfo = dmGlobal.getDisplayInfo(displayId);
-
-        rotateRectToDisplayRotation(displayInfo, rotation, inOutRect);
-    }
-
     // We suppress the Lint error CheckResult for Rect#intersect because in case the displayInfo and
     // folding features are out of sync, e.g. when a foldable devices is unfolding, it is acceptable
     // to provide the original folding feature Rect even if they don't intersect.
     @SuppressLint("RectIntersectReturnValueIgnored")
-    @VisibleForTesting
-    static void rotateRectToDisplayRotation(@NonNull DisplayInfo displayInfo,
+    public static void rotateRectToDisplayRotation(@NonNull DisplayInfo displayInfo,
             @Surface.Rotation int rotation, @NonNull Rect inOutRect) {
         // The inOutRect is specified in the default display orientation, so here we need to get
         // the display width and height in the default orientation to perform the intersection and
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index 556da37..5ce73b9 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -34,11 +34,14 @@
 import android.content.ContextWrapper;
 import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.hardware.display.DisplayManagerGlobal;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.StrictMode;
 import android.util.ArrayMap;
 import android.util.Log;
+import android.view.DisplayInfo;
+import android.view.Surface;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
@@ -96,8 +99,29 @@
 
     private final SupportedWindowFeatures mSupportedWindowFeatures;
 
+    private final DisplayStateProvider mDisplayStateProvider;
+
     public WindowLayoutComponentImpl(@NonNull Context context,
             @NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer) {
+        this(context, foldingFeatureProducer, new DisplayStateProvider() {
+            @Override
+            public int getDisplayRotation(@NonNull WindowConfiguration windowConfiguration) {
+                return windowConfiguration.getDisplayRotation();
+            }
+
+            @NonNull
+            @Override
+            public DisplayInfo getDisplayInfo(int displayId) {
+                return DisplayManagerGlobal.getInstance().getDisplayInfo(displayId);
+            }
+        });
+    }
+
+    @VisibleForTesting
+    WindowLayoutComponentImpl(@NonNull Context context,
+            @NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer,
+            @NonNull DisplayStateProvider displayStateProvider) {
+        mDisplayStateProvider = displayStateProvider;
         ((Application) context.getApplicationContext())
                 .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged());
         mFoldingFeatureProducer = foldingFeatureProducer;
@@ -145,21 +169,7 @@
                     || containsConsumer(consumer)) {
                 return;
             }
-            final IllegalArgumentException exception = new IllegalArgumentException(
-                    "Context must be a UI Context with display association, which should be"
-                    + " an Activity, WindowContext or InputMethodService");
-            if (!context.isUiContext()) {
-                throw exception;
-            }
-            if (context.getAssociatedDisplayId() == INVALID_DISPLAY) {
-                // This is to identify if #isUiContext of a non-UI Context is overridden.
-                // #isUiContext is more likely to be overridden than #getAssociatedDisplayId
-                // since #isUiContext is a public API.
-                StrictMode.onIncorrectContextUsed("The registered Context is a UI Context "
-                        + "but not associated with any display. "
-                        + "This Context may not receive any WindowLayoutInfo update. "
-                        + dumpAllBaseContextToString(context), exception);
-            }
+            assertUiContext(context);
             Log.d(TAG, "Register WindowLayoutInfoListener on "
                     + dumpAllBaseContextToString(context));
             mFoldingFeatureProducer.getData((features) -> {
@@ -339,6 +349,7 @@
     @Override
     @NonNull
     public WindowLayoutInfo getCurrentWindowLayoutInfo(@NonNull @UiContext Context context) {
+        assertUiContext(context);
         synchronized (mLock) {
             return getWindowLayoutInfo(context, mLastReportedFoldingFeatures);
         }
@@ -353,6 +364,25 @@
         return mSupportedWindowFeatures;
     }
 
+    private void assertUiContext(@NonNull Context context) {
+        final IllegalArgumentException exception = new IllegalArgumentException(
+                "Context must be a UI Context with display association, which should be "
+                        + "an Activity, WindowContext or InputMethodService");
+        if (!context.isUiContext()) {
+            throw exception;
+        }
+        if (context.getAssociatedDisplayId() == INVALID_DISPLAY) {
+            // This is to identify if #isUiContext of a non-UI Context is overridden.
+            // #isUiContext is more likely to be overridden than #getAssociatedDisplayId
+            // since #isUiContext is a public API.
+            StrictMode.onIncorrectContextUsed("The given context is a UI context, "
+                    + "but it is not associated with any display. "
+                    + "This context may not receive WindowLayoutInfo updates and "
+                    + "may get an empty WindowLayoutInfo return value. "
+                    + dumpAllBaseContextToString(context), exception);
+        }
+    }
+
     /** @see #getWindowLayoutInfo(Context, List) */
     private WindowLayoutInfo getWindowLayoutInfo(int displayId,
             @NonNull WindowConfiguration windowConfiguration,
@@ -401,15 +431,16 @@
 
         // We will transform the feature bounds to the Activity window, so using the rotation
         // from the same source (WindowConfiguration) to make sure they are synchronized.
-        final int rotation = windowConfiguration.getDisplayRotation();
+        final int rotation = mDisplayStateProvider.getDisplayRotation(windowConfiguration);
+        final DisplayInfo displayInfo = mDisplayStateProvider.getDisplayInfo(displayId);
 
         for (CommonFoldingFeature baseFeature : storedFeatures) {
             Integer state = convertToExtensionState(baseFeature.getState());
             if (state == null) {
                 continue;
             }
-            Rect featureRect = baseFeature.getRect();
-            rotateRectToDisplayRotation(displayId, rotation, featureRect);
+            final Rect featureRect = baseFeature.getRect();
+            rotateRectToDisplayRotation(displayInfo, rotation, featureRect);
             transformToWindowSpaceRect(windowConfiguration, featureRect);
 
             if (isZero(featureRect)) {
@@ -530,4 +561,13 @@
         public void onLowMemory() {
         }
     }
+
+    @VisibleForTesting
+    interface DisplayStateProvider {
+        @Surface.Rotation
+        int getDisplayRotation(@NonNull WindowConfiguration windowConfiguration);
+
+        @NonNull
+        DisplayInfo getDisplayInfo(int displayId);
+    }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
index 6e0e711..3b30f1b 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
@@ -24,7 +24,9 @@
 import android.app.Activity;
 import android.app.ActivityThread;
 import android.graphics.Rect;
+import android.hardware.display.DisplayManagerGlobal;
 import android.os.IBinder;
+import android.view.DisplayInfo;
 
 import androidx.window.common.layout.CommonFoldingFeature;
 
@@ -96,13 +98,18 @@
             return Collections.emptyList();
         }
 
-        final List<SidecarDisplayFeature> features = new ArrayList<>();
+        // We will transform the feature bounds to the Activity window, so using the rotation
+        // from the same source (WindowConfiguration) to make sure they are synchronized.
         final int rotation = activity.getResources().getConfiguration().windowConfiguration
                 .getDisplayRotation();
+        final DisplayInfo displayInfo =
+                DisplayManagerGlobal.getInstance().getDisplayInfo(displayId);
+
+        final List<SidecarDisplayFeature> features = new ArrayList<>();
         for (CommonFoldingFeature baseFeature : featureList) {
             final SidecarDisplayFeature feature = new SidecarDisplayFeature();
             final Rect featureRect = baseFeature.getRect();
-            rotateRectToDisplayRotation(displayId, rotation, featureRect);
+            rotateRectToDisplayRotation(displayInfo, rotation, featureRect);
             transformToWindowSpaceRect(activity, featureRect);
             feature.setRect(featureRect);
             feature.setType(baseFeature.getType());
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java
index ed4eddf..0643feb 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java
@@ -26,6 +26,8 @@
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 import android.view.Display;
+import android.view.DisplayInfo;
+import android.view.Surface;
 
 import androidx.annotation.NonNull;
 import androidx.test.core.app.ApplicationProvider;
@@ -72,6 +74,11 @@
         mWindowLayoutComponent.onDisplayFeaturesChanged(Collections.emptyList());
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void testAddWindowLayoutListener_nonUiContext_throwsError() {
+        mWindowLayoutComponent.addWindowLayoutInfoListener(mAppContext, info -> {});
+    }
+
     @Test
     public void testGetCurrentWindowLayoutInfo_noFoldingFeature_returnsEmptyList() {
         final Context testUiContext = new TestUiContext(mAppContext);
@@ -88,6 +95,29 @@
         final WindowConfiguration windowConfiguration =
                 testUiContext.getResources().getConfiguration().windowConfiguration;
         final Rect featureRect = windowConfiguration.getBounds();
+        // Mock DisplayStateProvider to control rotation and DisplayInfo, preventing dependency on
+        // the real device orientation or display configuration. This improves test reliability on
+        // devices like foldables or tablets that might have varying configurations.
+        final WindowLayoutComponentImpl.DisplayStateProvider displayStateProvider =
+                new WindowLayoutComponentImpl.DisplayStateProvider() {
+                    @Override
+                    public int getDisplayRotation(
+                            @NonNull WindowConfiguration windowConfiguration) {
+                        return Surface.ROTATION_0;
+                    }
+
+                    @NonNull
+                    @Override
+                    public DisplayInfo getDisplayInfo(int displayId) {
+                        final DisplayInfo displayInfo = new DisplayInfo();
+                        displayInfo.logicalWidth = featureRect.width();
+                        displayInfo.logicalHeight = featureRect.height();
+                        return displayInfo;
+                    }
+                };
+        mWindowLayoutComponent = new WindowLayoutComponentImpl(mAppContext,
+                mock(DeviceStateManagerFoldingFeatureProducer.class),
+                displayStateProvider);
         final CommonFoldingFeature foldingFeature = new CommonFoldingFeature(
                 CommonFoldingFeature.COMMON_TYPE_HINGE,
                 CommonFoldingFeature.COMMON_STATE_FLAT,
@@ -102,12 +132,9 @@
                 featureRect, FoldingFeature.TYPE_HINGE, FoldingFeature.STATE_FLAT));
     }
 
-    @Test
-    public void testGetCurrentWindowLayoutInfo_nonUiContext_returnsEmptyList() {
-        final WindowLayoutInfo layoutInfo =
-                mWindowLayoutComponent.getCurrentWindowLayoutInfo(mAppContext);
-
-        assertThat(layoutInfo.getDisplayFeatures()).isEmpty();
+    @Test(expected = IllegalArgumentException.class)
+    public void testGetCurrentWindowLayoutInfo_nonUiContext_throwsError() {
+        mWindowLayoutComponent.getCurrentWindowLayoutInfo(mAppContext);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 0150bcd..6bc995f1 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -102,6 +102,15 @@
             "persist.wm.debug.enter_desktop_by_default_on_freeform_display";
 
     /**
+     * Sysprop declaring whether to enable drag-to-maximize for desktop windows.
+     *
+     * <p>If it is not defined, then {@code R.integer.config_dragToMaximizeInDesktopMode}
+     * is used.
+     */
+    public static final String ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP =
+            "persist.wm.debug.enable_drag_to_maximize";
+
+    /**
      * Sysprop declaring the maximum number of Tasks to show in Desktop Mode at any one time.
      *
      * <p>If it is not defined, then {@code R.integer.config_maxDesktopWindowingActiveTasks} is
@@ -230,6 +239,18 @@
                         R.bool.config_enterDesktopByDefaultOnFreeformDisplay));
     }
 
+    /**
+     * Return {@code true} if a window should be maximized when it's dragged to the top edge of the
+     * screen.
+     */
+    public static boolean shouldMaximizeWhenDragToTopEdge(@NonNull Context context) {
+        if (!Flags.enableDragToMaximize()) {
+            return false;
+        }
+        return SystemProperties.getBoolean(ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP,
+                context.getResources().getBoolean(R.bool.config_dragToMaximizeInDesktopMode));
+    }
+
     /** Dumps DesktopModeStatus flags and configs. */
     public static void dump(PrintWriter pw, String prefix, Context context) {
         String innerPrefix = prefix + "  ";
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 5eb5d89..ff3dc33 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -19,6 +19,7 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION;
 import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
@@ -1262,6 +1263,15 @@
             return handleCloseTransition(info, st, ft, finishCallback);
         }
 
+        @Override
+        public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+                @Nullable SurfaceControl.Transaction finishTransaction) {
+            if (transition == mClosePrepareTransition && aborted) {
+                mClosePrepareTransition = null;
+                applyFinishOpenTransition();
+            }
+        }
+
         void createClosePrepareTransition() {
             if (mClosePrepareTransition != null) {
                 Log.e(TAG, "Re-create close prepare transition");
@@ -1324,8 +1334,8 @@
             }
             if (!isOpen) {
                 // Close transition, the transition info should be:
-                // init info(open A & wallpaper)
-                // current info(close B target)
+                // init info(open A & wallpaper) => init info(open A & change B & wallpaper)
+                // current info(close B target) => current info(change A & close B)
                 // remove init info(open/change A target & wallpaper)
                 boolean moveToTop = false;
                 boolean excludeOpenTarget = false;
@@ -1515,14 +1525,17 @@
                 return false;
             }
             SurfaceControl openingLeash = null;
+            SurfaceControl closingLeash = null;
             if (mApps != null) {
                 for (int i = mApps.length - 1; i >= 0; --i) {
                     if (mApps[i].mode == MODE_OPENING) {
                         openingLeash = mApps[i].leash;
+                    } else if (mApps[i].mode == MODE_CLOSING) {
+                        closingLeash = mApps[i].leash;
                     }
                 }
             }
-            if (openingLeash != null) {
+            if (openingLeash != null && closingLeash != null) {
                 int rootIdx = -1;
                 for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                     final TransitionInfo.Change c = info.getChanges().get(i);
@@ -1532,6 +1545,9 @@
                         st.reparent(c.getLeash(), openingLeash);
                         st.setAlpha(c.getLeash(), 1.0f);
                         rootIdx = TransitionUtil.rootIndexFor(c, info);
+                    } else if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED)
+                            && c.getMode() == TRANSIT_CHANGE) {
+                        st.reparent(c.getLeash(), closingLeash);
                     }
                 }
                 // The root leash and the leash of opening target should actually in the same level,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 8d25a9a..c6cd320 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -703,6 +703,7 @@
             Transitions transitions,
             KeyguardManager keyguardManager,
             ReturnToDragStartAnimator returnToDragStartAnimator,
+            Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler,
             EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler,
             ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler,
             DesktopModeDragAndDropTransitionHandler desktopModeDragAndDropTransitionHandler,
@@ -736,6 +737,7 @@
                 transitions,
                 keyguardManager,
                 returnToDragStartAnimator,
+                desktopMixedTransitionHandler.get(),
                 enterDesktopTransitionHandler,
                 exitDesktopTransitionHandler,
                 desktopModeDragAndDropTransitionHandler,
@@ -960,6 +962,7 @@
             @DynamicOverride DesktopRepository desktopRepository,
             FreeformTaskTransitionHandler freeformTaskTransitionHandler,
             CloseDesktopTaskTransitionHandler closeDesktopTaskTransitionHandler,
+            Optional<DesktopImmersiveController> desktopImmersiveController,
             InteractionJankMonitor interactionJankMonitor,
             @ShellMainThread Handler handler) {
         if (!DesktopModeStatus.canEnterDesktopMode(context)) {
@@ -972,6 +975,7 @@
                         desktopRepository,
                         freeformTaskTransitionHandler,
                         closeDesktopTaskTransitionHandler,
+                        desktopImmersiveController.get(),
                         interactionJankMonitor,
                         handler));
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
index d0bc5f0..e741892 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
@@ -130,7 +130,8 @@
         displayId: Int
     ) {
         if (!Flags.enableFullyImmersiveInDesktop()) return
-        exitImmersiveIfApplicable(wct, displayId)?.invoke(transition)
+        val result = exitImmersiveIfApplicable(wct, displayId)
+        result.asExit()?.runOnTransitionStart?.invoke(transition)
     }
 
     /**
@@ -145,16 +146,23 @@
         wct: WindowContainerTransaction,
         displayId: Int,
         excludeTaskId: Int? = null,
-    ): ((IBinder) -> Unit)? {
-        if (!Flags.enableFullyImmersiveInDesktop()) return null
-        val immersiveTask = desktopRepository.getTaskInFullImmersiveState(displayId) ?: return null
+    ): ExitResult {
+        if (!Flags.enableFullyImmersiveInDesktop()) return ExitResult.NoExit
+        val immersiveTask = desktopRepository.getTaskInFullImmersiveState(displayId)
+            ?: return ExitResult.NoExit
         if (immersiveTask == excludeTaskId) {
-            return null
+            return ExitResult.NoExit
         }
-        val taskInfo = shellTaskOrganizer.getRunningTaskInfo(immersiveTask) ?: return null
+        val taskInfo = shellTaskOrganizer.getRunningTaskInfo(immersiveTask)
+            ?: return ExitResult.NoExit
         logV("Appending immersive exit for task: $immersiveTask in display: $displayId")
         wct.setBounds(taskInfo.token, getExitDestinationBounds(taskInfo))
-        return { transition -> addPendingImmersiveExit(immersiveTask, displayId, transition) }
+        return ExitResult.Exit(
+            exitingTask = immersiveTask,
+            runOnTransitionStart = { transition ->
+                addPendingImmersiveExit(immersiveTask, displayId, transition)
+            }
+        )
     }
 
     /**
@@ -167,22 +175,25 @@
     fun exitImmersiveIfApplicable(
         wct: WindowContainerTransaction,
         taskInfo: RunningTaskInfo
-    ): ((IBinder) -> Unit)? {
-        if (!Flags.enableFullyImmersiveInDesktop()) return null
+    ): ExitResult {
+        if (!Flags.enableFullyImmersiveInDesktop()) return ExitResult.NoExit
         if (desktopRepository.isTaskInFullImmersiveState(taskInfo.taskId)) {
             // A full immersive task is being minimized, make sure the immersive state is broken
             // (i.e. resize back to max bounds).
             wct.setBounds(taskInfo.token, getExitDestinationBounds(taskInfo))
             logV("Appending immersive exit for task: ${taskInfo.taskId}")
-            return { transition ->
-                addPendingImmersiveExit(
-                    taskId = taskInfo.taskId,
-                    displayId = taskInfo.displayId,
-                    transition = transition
-                )
-            }
+            return ExitResult.Exit(
+                exitingTask = taskInfo.taskId,
+                runOnTransitionStart = { transition ->
+                    addPendingImmersiveExit(
+                        taskId = taskInfo.taskId,
+                        displayId = taskInfo.displayId,
+                        transition = transition
+                    )
+                }
+            )
         }
-        return null
+        return ExitResult.NoExit
     }
 
 
@@ -461,6 +472,20 @@
         var transition: IBinder,
     )
 
+    /** The result of an external exit request. */
+    sealed class ExitResult {
+        /** An immersive task exit (meaning, resize) was appended to the request. */
+        data class Exit(
+            val exitingTask: Int,
+            val runOnTransitionStart: ((IBinder) -> Unit)
+        ) : ExitResult()
+        /** There was no exit appended to the request. */
+        data object NoExit : ExitResult()
+
+        /** Returns the result as an [Exit] or null if it isn't of that type. */
+        fun asExit(): Exit? = if (this is Exit) this else null
+    }
+
     private enum class Direction {
         ENTER, EXIT
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
index 54a07f2..0bc571f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -27,15 +27,18 @@
 import android.window.TransitionInfo
 import android.window.TransitionRequestInfo
 import android.window.WindowContainerTransaction
+import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.protolog.ProtoLog
+import com.android.window.flags.Flags
 import com.android.wm.shell.freeform.FreeformTaskTransitionHandler
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 import com.android.wm.shell.shared.annotations.ShellMainThread
 import com.android.wm.shell.transition.MixedTransitionHandler
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
 
 /** The [Transitions.TransitionHandler] coordinates transition handlers in desktop windowing. */
 class DesktopMixedTransitionHandler(
@@ -44,10 +47,14 @@
     private val desktopRepository: DesktopRepository,
     private val freeformTaskTransitionHandler: FreeformTaskTransitionHandler,
     private val closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler,
+    private val desktopImmersiveController: DesktopImmersiveController,
     private val interactionJankMonitor: InteractionJankMonitor,
     @ShellMainThread private val handler: Handler,
 ) : MixedTransitionHandler, FreeformTaskTransitionStarter {
 
+    @VisibleForTesting
+    val pendingMixedTransitions = mutableListOf<PendingMixedTransition>()
+
     /** Delegates starting transition to [FreeformTaskTransitionHandler]. */
     override fun startWindowingModeTransition(
         targetWindowingMode: Int,
@@ -65,6 +72,40 @@
         }
         requireNotNull(wct)
         return transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, /* handler= */ this)
+            .also { transition ->
+                pendingMixedTransitions.add(PendingMixedTransition.Close(transition))
+            }
+    }
+
+    /**
+     * Starts a launch transition for [taskId], with an optional [exitingImmersiveTask] if it was
+     * included in the [wct] and is expected to be animated by this handler.
+     */
+    fun startLaunchTransition(
+        @WindowManager.TransitionType transitionType: Int,
+        wct: WindowContainerTransaction,
+        taskId: Int,
+        exitingImmersiveTask: Int? = null,
+    ): IBinder {
+        if (!Flags.enableFullyImmersiveInDesktop()) {
+            return transitions.startTransition(transitionType, wct, /* handler= */ null)
+        }
+        if (exitingImmersiveTask == null) {
+            logV("Starting mixed launch transition for task#%d", taskId)
+        } else {
+            logV(
+                "Starting mixed launch transition for task#%d with immersive exit of task#%d",
+                taskId, exitingImmersiveTask
+            )
+        }
+        return transitions.startTransition(transitionType, wct, /* handler= */ this)
+            .also { transition ->
+                pendingMixedTransitions.add(PendingMixedTransition.Launch(
+                    transition = transition,
+                    launchingTask = taskId,
+                    exitingImmersiveTask = exitingImmersiveTask
+                ))
+            }
     }
 
     /** Returns null, as it only handles transitions started from Shell. */
@@ -78,11 +119,43 @@
         info: TransitionInfo,
         startTransaction: SurfaceControl.Transaction,
         finishTransaction: SurfaceControl.Transaction,
-        finishCallback: Transitions.TransitionFinishCallback,
+        finishCallback: TransitionFinishCallback,
+    ): Boolean {
+        val pending = pendingMixedTransitions.find { pending -> pending.transition == transition }
+            ?: return false.also {
+                logW("Should have pending desktop transition")
+            }
+        pendingMixedTransitions.remove(pending)
+        logV("Animating pending mixed transition: %s", pending)
+        return when (pending) {
+            is PendingMixedTransition.Close -> animateCloseTransition(
+                transition,
+                info,
+                startTransaction,
+                finishTransaction,
+                finishCallback
+            )
+            is PendingMixedTransition.Launch -> animateLaunchTransition(
+                pending,
+                transition,
+                info,
+                startTransaction,
+                finishTransaction,
+                finishCallback
+            )
+        }
+    }
+
+    private fun animateCloseTransition(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: TransitionFinishCallback,
     ): Boolean {
         val closeChange = findCloseDesktopTaskChange(info)
         if (closeChange == null) {
-            ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: Should have closing desktop task", TAG)
+            logW("Should have closing desktop task")
             return false
         }
         if (isLastDesktopTask(closeChange)) {
@@ -106,6 +179,74 @@
         )
     }
 
+    private fun animateLaunchTransition(
+        pending: PendingMixedTransition.Launch,
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: TransitionFinishCallback,
+    ): Boolean {
+        // Check if there's also an immersive change during this launch.
+        val immersiveExitChange = pending.exitingImmersiveTask?.let { exitingTask ->
+            findDesktopTaskChange(info, exitingTask)
+        }
+        val launchChange = findDesktopTaskChange(info, pending.launchingTask)
+            ?: error("Should have pending launching task change")
+
+        var subAnimationCount = -1
+        var combinedWct: WindowContainerTransaction? = null
+        val finishCb = TransitionFinishCallback { wct ->
+            --subAnimationCount
+            combinedWct = combinedWct.merge(wct)
+            if (subAnimationCount > 0) return@TransitionFinishCallback
+            finishCallback.onTransitionFinished(combinedWct)
+        }
+
+        logV(
+            "Animating pending mixed launch transition task#%d immersiveExitTask#%s",
+            launchChange.taskInfo!!.taskId, immersiveExitChange?.taskInfo?.taskId
+        )
+        if (immersiveExitChange != null) {
+            subAnimationCount = 2
+            // Animate the immersive exit change separately.
+            info.changes.remove(immersiveExitChange)
+            desktopImmersiveController.animateResizeChange(
+                immersiveExitChange,
+                startTransaction,
+                finishTransaction,
+                finishCb
+            )
+            // Let the leftover/default handler animate the remaining changes.
+            return dispatchToLeftoverHandler(
+                transition,
+                info,
+                startTransaction,
+                finishTransaction,
+                finishCb
+            )
+        }
+        // There's nothing to animate separately, so let the left over handler animate
+        // the entire transition.
+        subAnimationCount = 1
+        return dispatchToLeftoverHandler(
+            transition,
+            info,
+            startTransaction,
+            finishTransaction,
+            finishCb
+        )
+    }
+
+    override fun onTransitionConsumed(
+        transition: IBinder,
+        aborted: Boolean,
+        finishTransaction: SurfaceControl.Transaction?
+    ) {
+        pendingMixedTransitions.removeAll { pending -> pending.transition == transition }
+        super.onTransitionConsumed(transition, aborted, finishTransaction)
+    }
+
     /**
      * Dispatch close desktop task animation to the default transition handlers. Allows delegating
      * it to Launcher to animate in sync with show Home transition.
@@ -126,14 +267,34 @@
             CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE,
         )
         // Dispatch the last desktop task closing animation.
+        return dispatchToLeftoverHandler(
+            transition = transition,
+            info = info,
+            startTransaction = startTransaction,
+            finishTransaction = finishTransaction,
+            finishCallback = finishCallback,
+            doOnFinishCallback = {
+                // Finish the jank trace when closing the last window in desktop mode.
+                interactionJankMonitor.end(CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE)
+            }
+        )
+    }
+
+    private fun dispatchToLeftoverHandler(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: TransitionFinishCallback,
+        doOnFinishCallback: (() -> Unit)? = null,
+    ): Boolean {
         return transitions.dispatchTransition(
             transition,
             info,
             startTransaction,
             finishTransaction,
             { wct ->
-                // Finish the jank trace when closing the last window in desktop mode.
-                interactionJankMonitor.end(CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE)
+                doOnFinishCallback?.invoke()
                 finishCallback.onTransitionFinished(wct)
             },
             /* skip= */ this
@@ -155,6 +316,43 @@
         }
     }
 
+    private fun findDesktopTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? {
+        return info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId }
+    }
+
+    private fun WindowContainerTransaction?.merge(
+        wct: WindowContainerTransaction?
+    ): WindowContainerTransaction? {
+        if (wct == null) return this
+        if (this == null) return wct
+        return this.merge(wct)
+    }
+
+    /** A scheduled transition that will potentially be animated by more than one handler */
+    sealed class PendingMixedTransition {
+        abstract val transition: IBinder
+
+        /** A task is closing. */
+        data class Close(
+            override val transition: IBinder,
+        ) : PendingMixedTransition()
+
+        /** A task is opening or moving to front. */
+        data class Launch(
+            override val transition: IBinder,
+            val launchingTask: Int,
+            val exitingImmersiveTask: Int?,
+        ) : PendingMixedTransition()
+    }
+
+    private fun logV(msg: String, vararg arguments: Any?) {
+        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+    }
+
+    private fun logW(msg: String, vararg arguments: Any?) {
+        ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+    }
+
     companion object {
         private const val TAG = "DesktopMixedTransitionHandler"
     }
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 38a4c71..577c18c 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
@@ -87,6 +87,7 @@
 import com.android.wm.shell.common.SingleInstanceRemoteListener
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing
+import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitResult
 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.DragStartState
 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType
 import com.android.wm.shell.desktopmode.DesktopRepository.VisibleTasksListener
@@ -146,6 +147,7 @@
     private val transitions: Transitions,
     private val keyguardManager: KeyguardManager,
     private val returnToDragStartAnimator: ReturnToDragStartAnimator,
+    private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler,
     private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler,
     private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler,
     private val desktopModeDragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler,
@@ -379,7 +381,7 @@
         // TODO(342378842): Instead of using default display, support multiple displays
         val taskIdToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(
             DEFAULT_DISPLAY, wct, taskId)
-        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
+        val exitResult = desktopImmersiveController.exitImmersiveIfApplicable(
             wct = wct,
             displayId = DEFAULT_DISPLAY,
             excludeTaskId = taskId,
@@ -393,7 +395,7 @@
         // TODO(343149901): Add DPI changes for task launch
         val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
         taskIdToMinimize?.let { addPendingMinimizeTransition(transition, it) }
-        runOnTransit?.invoke(transition)
+        exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
         return true
     }
 
@@ -410,7 +412,7 @@
         }
         logV("moveRunningTaskToDesktop taskId=%d", task.taskId)
         exitSplitIfApplicable(wct, task)
-        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
+        val exitResult = desktopImmersiveController.exitImmersiveIfApplicable(
             wct = wct,
             displayId = task.displayId,
             excludeTaskId = task.taskId,
@@ -422,7 +424,7 @@
 
         val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
         taskIdToMinimize?.let { addPendingMinimizeTransition(transition, it) }
-        runOnTransit?.invoke(transition)
+        exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
     }
 
     /**
@@ -459,12 +461,12 @@
         val taskIdToMinimize =
             bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
         addMoveToDesktopChanges(wct, taskInfo)
-        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
+        val exitResult = desktopImmersiveController.exitImmersiveIfApplicable(
             wct, taskInfo.displayId)
         val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
         transition?.let {
             taskIdToMinimize?.let { taskId -> addPendingMinimizeTransition(it, taskId) }
-            runOnTransit?.invoke(transition)
+            exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
         }
     }
 
@@ -507,7 +509,8 @@
                 taskId
             )
         )
-        return desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo)
+        return desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo).asExit()
+            ?.runOnTransitionStart
     }
 
     fun minimizeTask(taskInfo: RunningTaskInfo) {
@@ -520,7 +523,7 @@
             removeWallpaperActivity(wct)
         }
         // Notify immersive handler as it might need to exit immersive state.
-        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo)
+        val exitResult = desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo)
 
         wct.reorder(taskInfo.token, false)
         val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct)
@@ -531,7 +534,7 @@
                 taskId = taskId
             )
         }
-        runOnTransit?.invoke(transition)
+        exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
     }
 
     /** Move a task with given `taskId` to fullscreen */
@@ -627,7 +630,7 @@
         logV("moveBackgroundTaskToFront taskId=%s", taskId)
         val wct = WindowContainerTransaction()
         // TODO: b/342378842 - Instead of using default display, support multiple displays
-        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
+        val exitResult = desktopImmersiveController.exitImmersiveIfApplicable(
             wct = wct,
             displayId = DEFAULT_DISPLAY,
             excludeTaskId = taskId,
@@ -638,8 +641,13 @@
                 launchWindowingMode = WINDOWING_MODE_FREEFORM
             }.toBundle(),
         )
-        val transition = startLaunchTransition(TRANSIT_OPEN, wct, taskId, remoteTransition)
-        runOnTransit?.invoke(transition)
+        val transition = startLaunchTransition(
+            TRANSIT_OPEN,
+            wct,
+            taskId,
+            remoteTransition = remoteTransition
+        )
+        exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
     }
 
     /**
@@ -658,32 +666,39 @@
         }
         val wct = WindowContainerTransaction()
         wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
-        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
+        val result = desktopImmersiveController.exitImmersiveIfApplicable(
             wct = wct,
             displayId = taskInfo.displayId,
             excludeTaskId = taskInfo.taskId,
         )
-        val transition =
-            startLaunchTransition(
-                TRANSIT_TO_FRONT,
-                wct,
-                taskInfo.taskId,
-                remoteTransition,
-                taskInfo.displayId
-            )
-        runOnTransit?.invoke(transition)
+        val exitResult = if (result is ExitResult.Exit) { result } else { null }
+        val transition = startLaunchTransition(
+            transitionType = TRANSIT_TO_FRONT,
+            wct = wct,
+            taskId = taskInfo.taskId,
+            exitingImmersiveTask = exitResult?.exitingTask,
+            remoteTransition = remoteTransition,
+            displayId = taskInfo.displayId,
+        )
+        exitResult?.runOnTransitionStart?.invoke(transition)
     }
 
     private fun startLaunchTransition(
         transitionType: Int,
         wct: WindowContainerTransaction,
         taskId: Int,
-        remoteTransition: RemoteTransition?,
+        exitingImmersiveTask: Int? = null,
+        remoteTransition: RemoteTransition? = null,
         displayId: Int = DEFAULT_DISPLAY,
     ): IBinder {
         val taskIdToMinimize = addAndGetMinimizeChanges(displayId, wct, taskId)
         if (remoteTransition == null) {
-            val t = transitions.startTransition(transitionType, wct, null /* handler */)
+            val t = desktopMixedTransitionHandler.startLaunchTransition(
+                transitionType = transitionType,
+                wct = wct,
+                taskId = taskId,
+                exitingImmersiveTask = exitingImmersiveTask,
+            )
             taskIdToMinimize?.let { addPendingMinimizeTransition(t, it) }
             return t
         }
@@ -842,6 +857,39 @@
         toggleResizeDesktopTaskTransitionHandler.startTransition(wct)
     }
 
+    private fun dragToMaximizeDesktopTask(
+        taskInfo: RunningTaskInfo,
+        taskSurface: SurfaceControl,
+        currentDragBounds: Rect,
+        motionEvent: MotionEvent
+    ) {
+        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+        val stableBounds = Rect()
+        displayLayout.getStableBounds(stableBounds)
+        if (isTaskMaximized(taskInfo, stableBounds)) {
+            // Handle the case where we attempt to drag-to-maximize when already maximized: the task
+            // position won't need to change but we want to animate the surface going back to the
+            // maximized position.
+            val containerBounds = taskInfo.configuration.windowConfiguration.bounds
+            if (containerBounds != currentDragBounds) {
+                returnToDragStartAnimator.start(
+                    taskInfo.taskId,
+                    taskSurface,
+                    startBounds = currentDragBounds,
+                    endBounds = containerBounds,
+                    isResizable = taskInfo.isResizeable
+                )
+            }
+            return
+        }
+
+        // TODO(b/375356605): Introduce a new ResizeTrigger for drag-to-top.
+        desktopModeEventLogger.logTaskResizingStarted(
+            ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, motionEvent, taskInfo, displayController
+        )
+        toggleDesktopTaskSize(taskInfo, ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, motionEvent)
+    }
+
     private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect {
         if (taskInfo.isResizeable) {
             // if resizable then expand to entire stable bounds (full display minus insets)
@@ -1373,14 +1421,15 @@
             wct.startTask(requestedTaskId, options.toBundle())
             val taskIdToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(
                 callingTask.displayId, wct, requestedTaskId)
-            val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
-                wct = wct,
-                displayId = callingTask.displayId,
-                excludeTaskId = requestedTaskId,
-            )
+            val exitResult = desktopImmersiveController
+                .exitImmersiveIfApplicable(
+                    wct = wct,
+                    displayId = callingTask.displayId,
+                    excludeTaskId = requestedTaskId,
+                )
             val transition = transitions.startTransition(TRANSIT_OPEN, wct, null)
             taskIdToMinimize?.let { addPendingMinimizeTransition(transition, it) }
-            runOnTransit?.invoke(transition)
+            exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
         } else {
             val splitPosition = splitScreenController.determineNewInstancePosition(callingTask)
             splitScreenController.startTask(requestedTaskId, splitPosition,
@@ -1902,11 +1951,15 @@
             )
         when (indicatorType) {
             IndicatorType.TO_FULLSCREEN_INDICATOR -> {
-                moveToFullscreenWithAnimation(
-                    taskInfo,
-                    position,
-                    DesktopModeTransitionSource.TASK_DRAG
-                )
+                if (DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)) {
+                    dragToMaximizeDesktopTask(taskInfo, taskSurface, currentDragBounds, motionEvent)
+                } else {
+                    moveToFullscreenWithAnimation(
+                        taskInfo,
+                        position,
+                        DesktopModeTransitionSource.TASK_DRAG
+                    )
+                }
             }
             IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
                 handleSnapResizingTask(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt
index 98bdf05..c0613e2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt
@@ -28,7 +28,7 @@
 class FreeformTaskTransitionStarterInitializer(
     shellInit: ShellInit,
     private val windowDecorViewModel: WindowDecorViewModel,
-    private val freeformTaskTransitionStarter: FreeformTaskTransitionStarter
+    private val freeformTaskTransitionStarter: FreeformTaskTransitionStarter,
 ) {
     init {
         shellInit.addInitCallback(::onShellInit, this)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/TaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/TaskChangeListener.kt
index fb86a9f..aee92ea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/TaskChangeListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/TaskChangeListener.kt
@@ -24,27 +24,27 @@
  * The implementations are responsible for handle all the task management.
  */
 interface TaskChangeListener {
-  /** Notifies a task opening in freeform mode. */
-  fun onTaskOpening(taskInfo: RunningTaskInfo)
+    /** Notifies a task opening in freeform mode. */
+    fun onTaskOpening(taskInfo: RunningTaskInfo)
 
-  /** Notifies a task info update on the given task from Shell Transitions framework. */
-  fun onTaskChanging(taskInfo: RunningTaskInfo)
+    /** Notifies a task info update on the given task from Shell Transitions framework. */
+    fun onTaskChanging(taskInfo: RunningTaskInfo)
 
-  /**
-   * Notifies a task info update on the given task from [FreeformTaskListener].
-   *
-   * This is used to propagate task info changes since not all task changes are propagated from
-   * [TransitionObserver] in [onTaskChanging]. It is recommended to use [onTaskChanging] instead of
-   * this method where possible.
-   */
-  fun onNonTransitionTaskChanging(taskInfo: RunningTaskInfo)
+    /**
+     * Notifies a task info update on the given task from [FreeformTaskListener].
+     *
+     * This is used to propagate task info changes since not all task changes are propagated from
+     * [TransitionObserver] in [onTaskChanging]. It is recommended to use [onTaskChanging] instead
+     * of this method where possible.
+     */
+    fun onNonTransitionTaskChanging(taskInfo: RunningTaskInfo)
 
-  /** Notifies a task moving to the front. */
-  fun onTaskMovingToFront(taskInfo: RunningTaskInfo)
+    /** Notifies a task moving to the front. */
+    fun onTaskMovingToFront(taskInfo: RunningTaskInfo)
 
-  /** Notifies a task moving to the back. */
-  fun onTaskMovingToBack(taskInfo: RunningTaskInfo)
+    /** Notifies a task moving to the back. */
+    fun onTaskMovingToBack(taskInfo: RunningTaskInfo)
 
-  /** Notifies a task is closing. */
-  fun onTaskClosing(taskInfo: RunningTaskInfo)
+    /** Notifies a task is closing. */
+    fun onTaskClosing(taskInfo: RunningTaskInfo)
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 0d89f75..17483dd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -40,6 +40,7 @@
 import android.window.WindowContainerTransaction;
 
 import com.android.internal.protolog.ProtoLog;
+import com.android.window.flags.Flags;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
 import com.android.wm.shell.common.split.SplitScreenUtils;
@@ -419,7 +420,9 @@
         for (int i = 0; i < info.getRootCount(); ++i) {
             out.addRoot(info.getRoot(i));
         }
-        out.setAnimationOptions(info.getAnimationOptions());
+        if (!Flags.moveAnimationOptionsToChange()) {
+            out.setAnimationOptions(info.getAnimationOptions());
+        }
         return out;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 5437167..ec58292 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -561,13 +561,12 @@
 
                 final TransitionInfo.AnimationOptions options;
                 if (Flags.moveAnimationOptionsToChange()) {
-                    options = info.getAnimationOptions();
-                } else {
                     options = change.getAnimationOptions();
+                } else {
+                    options = info.getAnimationOptions();
                 }
                 if (options != null) {
-                    attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions(),
-                            cornerRadius);
+                    attachThumbnail(animations, onAnimFinish, change, options, cornerRadius);
                 }
             }
         }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
index e83f5c7..1c4b9bf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
@@ -347,7 +347,7 @@
             wct = wct,
             displayId = DEFAULT_DISPLAY,
             excludeTaskId = task.taskId
-        )?.invoke(transition)
+        ).asExit()?.runOnTransitionStart?.invoke(transition)
 
         assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
@@ -402,7 +402,8 @@
             immersive = true
         )
 
-        controller.exitImmersiveIfApplicable(wct, task)?.invoke(transition)
+        controller.exitImmersiveIfApplicable(wct, task)
+            .asExit()?.runOnTransitionStart?.invoke(transition)
 
         assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
@@ -412,23 +413,36 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-    fun exitImmersiveIfApplicable_byTask_notInImmersive_doesNotAddPendingExitOnRun() {
+    fun exitImmersiveIfApplicable_byTask_notInImmersive_doesNotExit() {
         val task = createFreeformTask()
         whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
         val wct = WindowContainerTransaction()
-        val transition = Binder()
         desktopRepository.setTaskInFullImmersiveState(
-            displayId = DEFAULT_DISPLAY,
+            displayId = task.displayId,
             taskId = task.taskId,
             immersive = false
         )
 
-        controller.exitImmersiveIfApplicable(wct, task)?.invoke(transition)
+        val result = controller.exitImmersiveIfApplicable(wct, task)
 
-        assertThat(controller.pendingExternalExitTransitions.any { exit ->
-            exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
-                    && exit.taskId == task.taskId
-        }).isFalse()
+        assertThat(result).isEqualTo(DesktopImmersiveController.ExitResult.NoExit)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun exitImmersiveIfApplicable_byDisplay_notInImmersive_doesNotExit() {
+        val task = createFreeformTask()
+        whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+        val wct = WindowContainerTransaction()
+        desktopRepository.setTaskInFullImmersiveState(
+            displayId = task.displayId,
+            taskId = task.taskId,
+            immersive = false
+        )
+
+        val result = controller.exitImmersiveIfApplicable(wct, task.displayId)
+
+        assertThat(result).isEqualTo(DesktopImmersiveController.ExitResult.NoExit)
     }
 
     @Test
@@ -631,7 +645,8 @@
             taskId = task.taskId,
             immersive = true
         )
-        controller.exitImmersiveIfApplicable(wct, task)?.invoke(Binder())
+        controller.exitImmersiveIfApplicable(wct, task)
+            .asExit()?.runOnTransitionStart?.invoke(Binder())
 
         controller.moveTaskToNonImmersive(task)
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index be0663c..868883d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -31,6 +31,8 @@
 import android.testing.TestableLooper.RunWithLooper
 import android.view.SurfaceControl
 import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TransitionType
 import android.window.TransitionInfo
 import android.window.WindowContainerTransaction
 import androidx.test.filters.SmallTest
@@ -41,6 +43,7 @@
 import com.android.wm.shell.TestRunningTaskInfoBuilder
 import com.android.wm.shell.freeform.FreeformTaskTransitionHandler
 import com.android.wm.shell.transition.Transitions
+import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
@@ -51,6 +54,8 @@
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argThat
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
@@ -71,6 +76,7 @@
     @Mock lateinit var desktopRepository: DesktopRepository
     @Mock lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler
     @Mock lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler
+    @Mock lateinit var desktopImmersiveController: DesktopImmersiveController
     @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
     @Mock lateinit var mockHandler: Handler
     @Mock lateinit var closingTaskLeash: SurfaceControl
@@ -86,6 +92,7 @@
                 desktopRepository,
                 freeformTaskTransitionHandler,
                 closeDesktopTaskTransitionHandler,
+                desktopImmersiveController,
                 interactionJankMonitor,
                 mockHandler
             )
@@ -146,7 +153,7 @@
         val transition = mock<IBinder>()
         val transitionInfo =
             createTransitionInfo(
-                changeMode = WindowManager.TRANSIT_OPEN,
+                changeMode = TRANSIT_OPEN,
                 task = createTask(WINDOWING_MODE_FREEFORM)
             )
         whenever(freeformTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any()))
@@ -164,7 +171,9 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS)
     fun startAnimation_withClosingDesktopTask_callsCloseTaskHandler() {
+        val wct = WindowContainerTransaction()
         val transition = mock<IBinder>()
         val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM))
         whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2)
@@ -172,6 +181,9 @@
                 closeDesktopTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any())
             )
             .thenReturn(true)
+        whenever(transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, mixedHandler))
+            .thenReturn(transition)
+        mixedHandler.startRemoveTransition(wct)
 
         val started = mixedHandler.startAnimation(
             transition = transition,
@@ -187,12 +199,17 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS)
     fun startAnimation_withClosingLastDesktopTask_dispatchesTransition() {
+        val wct = WindowContainerTransaction()
         val transition = mock<IBinder>()
         val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM))
         whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(1)
         whenever(transitions.dispatchTransition(any(), any(), any(), any(), any(), any()))
             .thenReturn(mock())
+        whenever(transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, mixedHandler))
+            .thenReturn(transition)
+        mixedHandler.startRemoveTransition(wct)
 
         mixedHandler.startAnimation(
             transition = transition,
@@ -220,6 +237,176 @@
             )
     }
 
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun startLaunchTransition_immersiveMixDisabled_doesNotUseMixedHandler() {
+        val wct = WindowContainerTransaction()
+        val task = createTask(WINDOWING_MODE_FREEFORM)
+        whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull()))
+            .thenReturn(Binder())
+
+        mixedHandler.startLaunchTransition(
+            transitionType = TRANSIT_OPEN,
+            wct = wct,
+            taskId = task.taskId,
+            exitingImmersiveTask = null
+        )
+
+        verify(transitions).startTransition(TRANSIT_OPEN, wct, /* handler= */ null)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun startLaunchTransition_immersiveMixEnabled_usesMixedHandler() {
+        val wct = WindowContainerTransaction()
+        val task = createTask(WINDOWING_MODE_FREEFORM)
+        whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull()))
+            .thenReturn(Binder())
+
+        mixedHandler.startLaunchTransition(
+            transitionType = TRANSIT_OPEN,
+            wct = wct,
+            taskId = task.taskId,
+            exitingImmersiveTask = null
+        )
+
+        verify(transitions).startTransition(TRANSIT_OPEN, wct, mixedHandler)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun startAndAnimateLaunchTransition_withoutImmersiveChange_dispatchesAllChangesToLeftOver() {
+        val wct = WindowContainerTransaction()
+        val launchingTask = createTask(WINDOWING_MODE_FREEFORM)
+        val transition = Binder()
+        whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull()))
+            .thenReturn(transition)
+
+        mixedHandler.startLaunchTransition(
+            transitionType = TRANSIT_OPEN,
+            wct = wct,
+            taskId = launchingTask.taskId,
+            exitingImmersiveTask = null,
+        )
+        val launchTaskChange = createChange(launchingTask)
+        val otherChange = createChange(createTask(WINDOWING_MODE_FREEFORM))
+        mixedHandler.startAnimation(
+            transition,
+            createTransitionInfo(
+                TRANSIT_OPEN,
+                listOf(launchTaskChange, otherChange)
+            ),
+            SurfaceControl.Transaction(),
+            SurfaceControl.Transaction(),
+        ) { }
+
+        verify(transitions).dispatchTransition(
+            eq(transition),
+            argThat { info ->
+                info.changes.contains(launchTaskChange) && info.changes.contains(otherChange)
+            },
+            any(),
+            any(),
+            any(),
+            eq(mixedHandler),
+        )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun startAndAnimateLaunchTransition_withImmersiveChange_mixesAnimations() {
+        val wct = WindowContainerTransaction()
+        val launchingTask = createTask(WINDOWING_MODE_FREEFORM)
+        val immersiveTask = createTask(WINDOWING_MODE_FREEFORM)
+        val transition = Binder()
+        whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull()))
+            .thenReturn(transition)
+
+        mixedHandler.startLaunchTransition(
+            transitionType = TRANSIT_OPEN,
+            wct = wct,
+            taskId = launchingTask.taskId,
+            exitingImmersiveTask = immersiveTask.taskId,
+        )
+        val launchTaskChange = createChange(launchingTask)
+        val immersiveChange = createChange(immersiveTask)
+        mixedHandler.startAnimation(
+            transition,
+            createTransitionInfo(
+                TRANSIT_OPEN,
+                listOf(launchTaskChange, immersiveChange)
+            ),
+            SurfaceControl.Transaction(),
+            SurfaceControl.Transaction(),
+        ) { }
+
+        verify(desktopImmersiveController)
+            .animateResizeChange(eq(immersiveChange), any(), any(), any())
+        verify(transitions).dispatchTransition(
+            eq(transition),
+            argThat { info ->
+                info.changes.contains(launchTaskChange) && !info.changes.contains(immersiveChange)
+            },
+            any(),
+            any(),
+            any(),
+            eq(mixedHandler),
+        )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun startAndAnimateLaunchTransition_removesPendingMixedTransition() {
+        val wct = WindowContainerTransaction()
+        val launchingTask = createTask(WINDOWING_MODE_FREEFORM)
+        val transition = Binder()
+        whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull()))
+            .thenReturn(transition)
+
+        mixedHandler.startLaunchTransition(
+            transitionType = TRANSIT_OPEN,
+            wct = wct,
+            taskId = launchingTask.taskId,
+            exitingImmersiveTask = null,
+        )
+        val launchTaskChange = createChange(launchingTask)
+        mixedHandler.startAnimation(
+            transition,
+            createTransitionInfo(
+                TRANSIT_OPEN,
+                listOf(launchTaskChange)
+            ),
+            SurfaceControl.Transaction(),
+            SurfaceControl.Transaction(),
+        ) { }
+
+        assertThat(mixedHandler.pendingMixedTransitions).isEmpty()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun startAndAnimateLaunchTransition_aborted_removesPendingMixedTransition() {
+        val wct = WindowContainerTransaction()
+        val launchingTask = createTask(WINDOWING_MODE_FREEFORM)
+        val transition = Binder()
+        whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull()))
+            .thenReturn(transition)
+
+        mixedHandler.startLaunchTransition(
+            transitionType = TRANSIT_OPEN,
+            wct = wct,
+            taskId = launchingTask.taskId,
+            exitingImmersiveTask = null,
+        )
+        mixedHandler.onTransitionConsumed(
+            transition = transition,
+            aborted = true,
+            finishTransaction = SurfaceControl.Transaction()
+        )
+
+        assertThat(mixedHandler.pendingMixedTransitions).isEmpty()
+    }
+
     private fun createTransitionInfo(
         type: Int = WindowManager.TRANSIT_CLOSE,
         changeMode: Int = WindowManager.TRANSIT_CLOSE,
@@ -235,6 +422,18 @@
             )
         }
 
+    private fun createTransitionInfo(
+        @TransitionType type: Int,
+        changes: List<TransitionInfo.Change> = emptyList()
+    ): TransitionInfo = TransitionInfo(type, /* flags= */ 0).apply {
+        changes.forEach { change -> addChange(change) }
+    }
+
+    private fun createChange(task: RunningTaskInfo): TransitionInfo.Change =
+        TransitionInfo.Change(task.token, SurfaceControl()).apply {
+            taskInfo = task
+        }
+
     private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo =
         TestRunningTaskInfoBuilder()
             .setActivityType(ACTIVITY_TYPE_STANDARD)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index b358686..051079c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -196,6 +196,7 @@
   @Mock lateinit var transitions: Transitions
   @Mock lateinit var keyguardManager: KeyguardManager
   @Mock lateinit var mReturnToDragStartAnimator: ReturnToDragStartAnimator
+  @Mock lateinit var desktopMixedTransitionHandler: DesktopMixedTransitionHandler
   @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler
   @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler
   @Mock lateinit var dragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler
@@ -288,6 +289,12 @@
     val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
     tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
     whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
+    whenever(mMockDesktopImmersiveController
+      .exitImmersiveIfApplicable(any(), any<RunningTaskInfo>()))
+      .thenReturn(DesktopImmersiveController.ExitResult.NoExit)
+    whenever(mMockDesktopImmersiveController
+      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull()))
+      .thenReturn(DesktopImmersiveController.ExitResult.NoExit)
 
     controller = createController()
     controller.setSplitScreenController(splitScreenController)
@@ -323,6 +330,7 @@
         transitions,
         keyguardManager,
         mReturnToDragStartAnimator,
+        desktopMixedTransitionHandler,
         enterDesktopTransitionHandler,
         exitDesktopTransitionHandler,
         dragAndDropTransitionHandler,
@@ -1389,7 +1397,7 @@
 
     controller.moveTaskToFront(task1, remoteTransition = null)
 
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT)
+    val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT)
     assertThat(wct.hierarchyOps).hasSize(1)
     wct.assertReorderAt(index = 0, task1)
   }
@@ -1398,10 +1406,16 @@
   fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() {
     setUpHomeTask()
     val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() }
+    whenever(desktopMixedTransitionHandler.startLaunchTransition(
+      eq(TRANSIT_TO_FRONT),
+      any(),
+      eq(freeformTasks[0].taskId),
+      anyOrNull()
+    )).thenReturn(Binder())
 
     controller.moveTaskToFront(freeformTasks[0], remoteTransition = null)
 
-    val wct = getLatestWct(type = TRANSIT_TO_FRONT)
+    val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT)
     assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize
     wct.assertReorderAt(0, freeformTasks[0], toTop = true)
     wct.assertReorderAt(1, freeformTasks[1], toTop = false)
@@ -1443,7 +1457,7 @@
 
     controller.moveTaskToFront(task.taskId, remoteTransition = null)
 
-    val wct = getLatestWct(type = TRANSIT_OPEN)
+    val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN)
     assertThat(wct.hierarchyOps).hasSize(1)
     wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
   }
@@ -1453,10 +1467,13 @@
     val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
     val task = createTaskInfo(1001)
     whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null)
+    whenever(desktopMixedTransitionHandler
+      .startLaunchTransition(eq(TRANSIT_OPEN), any(), eq(task.taskId), anyOrNull()))
+      .thenReturn(Binder())
 
     controller.moveTaskToFront(task.taskId, remoteTransition = null)
 
-    val wct = getLatestWct(type = TRANSIT_OPEN)
+    val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN)
     assertThat(wct.hierarchyOps.size).isEqualTo(2) // launch + minimize
     wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
     wct.assertReorderAt(1, freeformTasks[0], toTop = false)
@@ -1791,7 +1808,10 @@
     whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
       .thenReturn(transition)
     whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task)))
-      .thenReturn(runOnTransit)
+      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+        exitingTask = task.taskId,
+        runOnTransitionStart = runOnTransit,
+      ))
 
     controller.minimizeTask(task)
 
@@ -2894,6 +2914,123 @@
   }
 
   @Test
+  fun onDesktopDragEnd_fullscreenIndicator_dragToExitDesktop() {
+    val task = setUpFreeformTask(bounds = Rect(0, 0, 100, 100))
+    val spyController = spy(controller)
+    val mockSurface = mock(SurfaceControl::class.java)
+    val mockDisplayLayout = mock(DisplayLayout::class.java)
+    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+    whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
+    whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
+    whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
+      (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+    }
+    whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(false)
+    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+      .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
+
+    // Drag move the task to the top edge
+    spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
+    spyController.onDragPositioningEnd(
+      task,
+      mockSurface,
+      Point(100, 50), /* position */
+      PointF(200f, 300f), /* inputCoordinate */
+      Rect(100, 50, 500, 1000), /* currentDragBounds */
+      Rect(0, 50, 2000, 2000) /* validDragArea */,
+      Rect() /* dragStartBounds */,
+      motionEvent,
+      desktopWindowDecoration)
+
+    // Assert the task exits desktop mode
+    val wct = getLatestExitDesktopWct()
+    assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+        .isEqualTo(WINDOWING_MODE_UNDEFINED)
+  }
+
+  @Test
+  fun onDesktopDragEnd_fullscreenIndicator_dragToMaximize() {
+    val task = setUpFreeformTask(bounds = Rect(0, 0, 100, 100))
+    val spyController = spy(controller)
+    val mockSurface = mock(SurfaceControl::class.java)
+    val mockDisplayLayout = mock(DisplayLayout::class.java)
+    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+    whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
+    whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
+    whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
+      (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+    }
+    whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(true)
+    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+      .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
+
+    // Drag move the task to the top edge
+    spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
+    spyController.onDragPositioningEnd(
+      task,
+      mockSurface,
+      Point(100, 50), /* position */
+      PointF(200f, 300f), /* inputCoordinate */
+      Rect(100, 50, 500, 1000), /* currentDragBounds */
+      Rect(0, 50, 2000, 2000) /* validDragArea */,
+      Rect() /* dragStartBounds */,
+      motionEvent,
+      desktopWindowDecoration)
+
+    // Assert bounds set to stable bounds
+    val wct = getLatestToggleResizeDesktopTaskWct()
+    assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
+  }
+
+  @Test
+  fun onDesktopDragEnd_fullscreenIndicator_dragToMaximize_noBoundsChange() {
+    val task = setUpFreeformTask(bounds = STABLE_BOUNDS)
+    val spyController = spy(controller)
+    val mockSurface = mock(SurfaceControl::class.java)
+    val mockDisplayLayout = mock(DisplayLayout::class.java)
+    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+    tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+    whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
+    whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
+    whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i ->
+      (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+    }
+    whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(true)
+    whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+    whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull()))
+      .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
+
+    // Drag move the task to the top edge
+    val currentDragBounds = Rect(100, 50, 500, 1000)
+    spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
+    spyController.onDragPositioningEnd(
+      task,
+      mockSurface,
+      Point(100, 50), /* position */
+      PointF(200f, 300f), /* inputCoordinate */
+      currentDragBounds, /* currentDragBounds */
+      Rect(0, 50, 2000, 2000) /* validDragArea */,
+      Rect() /* dragStartBounds */,
+      motionEvent,
+      desktopWindowDecoration)
+
+    // Assert that task is NOT updated via WCT
+    verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
+    // Assert that task leash is updated via Surface Animations
+    verify(mReturnToDragStartAnimator).start(
+      eq(task.taskId),
+      eq(mockSurface),
+      eq(currentDragBounds),
+      eq(STABLE_BOUNDS),
+      eq(true)
+    )
+  }
+
+  @Test
   fun enterSplit_freeformTaskIsMovedToSplit() {
     val task1 = setUpFreeformTask()
     val task2 = setUpFreeformTask()
@@ -3101,7 +3238,10 @@
       .thenReturn(transition)
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId)))
-      .thenReturn(runOnStartTransit)
+      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+        exitingTask = immersiveTask.taskId,
+        runOnTransitionStart = runOnStartTransit,
+      ))
 
     runOpenInstance(immersiveTask, freeformTask.taskId)
 
@@ -3502,7 +3642,11 @@
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)).thenReturn(runOnStartTransit)
+      .exitImmersiveIfApplicable(wct, task.displayId, task.taskId))
+      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+        exitingTask = 5,
+        runOnTransitionStart = runOnStartTransit,
+      ))
     whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
 
     controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
@@ -3519,7 +3663,11 @@
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
-      .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)).thenReturn(runOnStartTransit)
+      .exitImmersiveIfApplicable(wct, task.displayId, task.taskId))
+      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+        exitingTask = 5,
+        runOnTransitionStart = runOnStartTransit,
+      ))
     whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
 
     controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
@@ -3536,8 +3684,12 @@
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)))
-      .thenReturn(runOnStartTransit)
-    whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
+      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+        exitingTask = 5,
+        runOnTransitionStart = runOnStartTransit,
+      ))
+    whenever(desktopMixedTransitionHandler
+      .startLaunchTransition(any(), any(), anyInt(), anyOrNull())).thenReturn(transition)
 
     controller.moveTaskToFront(task.taskId, remoteTransition = null)
 
@@ -3553,8 +3705,13 @@
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)))
-      .thenReturn(runOnStartTransit)
-    whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
+      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+        exitingTask = 5,
+        runOnTransitionStart = runOnStartTransit,
+      ))
+    whenever(desktopMixedTransitionHandler
+      .startLaunchTransition(any(), any(), eq(task.taskId), anyOrNull()))
+      .thenReturn(transition)
 
     controller.moveTaskToFront(task.taskId, remoteTransition = null)
 
@@ -3936,6 +4093,16 @@
     return arg.value
   }
 
+  private fun getLatestDesktopMixedTaskWct(
+    @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
+  ): WindowContainerTransaction {
+    val arg: ArgumentCaptor<WindowContainerTransaction> =
+      ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+    verify(desktopMixedTransitionHandler)
+      .startLaunchTransition(eq(type), capture(arg), anyInt(), anyOrNull())
+    return arg.value
+  }
+
   private fun getLatestEnterDesktopWct(): WindowContainerTransaction {
     val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
     verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any())
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
index 43377d8..6870446 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
@@ -16,7 +16,6 @@
 
 package com.google.android.appfunctions.sidecar;
 
-import android.Manifest;
 import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -41,8 +40,6 @@
  * <p>This class wraps {@link android.app.appfunctions.AppFunctionManager} functionalities and
  * exposes it here as a sidecar library (avoiding direct dependency on the platform API).
  */
-// TODO(b/357551503): Implement get and set enabled app function APIs.
-// TODO(b/367329899): Add sidecar library to Android B builds.
 public final class AppFunctionManager {
     /**
      * The default state of the app function. Call {@link #setAppFunctionEnabled} with this to reset
@@ -70,9 +67,9 @@
     @IntDef(
             prefix = {"APP_FUNCTION_STATE_"},
             value = {
-                    APP_FUNCTION_STATE_DEFAULT,
-                    APP_FUNCTION_STATE_ENABLED,
-                    APP_FUNCTION_STATE_DISABLED
+                APP_FUNCTION_STATE_DEFAULT,
+                APP_FUNCTION_STATE_ENABLED,
+                APP_FUNCTION_STATE_DISABLED
             })
     @Retention(RetentionPolicy.SOURCE)
     public @interface EnabledState {}
@@ -102,6 +99,9 @@
      * <p>Proxies request and response to the underlying {@link
      * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and
      * response in the appropriate type required by the function.
+     *
+     * <p>See {@link android.app.appfunctions.AppFunctionManager#executeAppFunction} for the
+     * documented behaviour of this method.
      */
     public void executeAppFunction(
             @NonNull ExecuteAppFunctionRequest sidecarRequest,
@@ -128,24 +128,8 @@
     /**
      * Returns a boolean through a callback, indicating whether the app function is enabled.
      *
-     * <p>* This method can only check app functions owned by the caller, or those where the caller
-     * has visibility to the owner package and holds either the {@link
-     * Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link
-     * Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission.
-     *
-     * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors:
-     *
-     * <ul>
-     *   <li>{@link IllegalArgumentException}, if the function is not found or the caller does not
-     *       have access to it.
-     * </ul>
-     *
-     * @param functionIdentifier the identifier of the app function to check (unique within the
-     *     target package) and in most cases, these are automatically generated by the AppFunctions
-     *     SDK
-     * @param targetPackage the package name of the app function's owner
-     * @param executor the executor to run the request
-     * @param callback the callback to receive the function enabled check result
+     * <p>See {@link android.app.appfunctions.AppFunctionManager#isAppFunctionEnabled} for the
+     * documented behaviour of this method.
      */
     public void isAppFunctionEnabled(
             @NonNull String functionIdentifier,
@@ -158,20 +142,8 @@
     /**
      * Sets the enabled state of the app function owned by the calling package.
      *
-     * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors:
-     *
-     * <ul>
-     *   <li>{@link IllegalArgumentException}, if the function is not found or the caller does not
-     *       have access to it.
-     * </ul>
-     *
-     * @param functionIdentifier the identifier of the app function to enable (unique within the
-     *     calling package). In most cases, identifiers are automatically generated by the
-     *     AppFunctions SDK
-     * @param newEnabledState the new state of the app function
-     * @param executor the executor to run the callback
-     * @param callback the callback to receive the result of the function enablement. The call was
-     *     successful if no exception was thrown.
+     * <p>See {@link android.app.appfunctions.AppFunctionManager#setAppFunctionEnabled} for the
+     * documented behavoir of this method.
      */
     // Constants in @EnabledState should always mirror those in
     // android.app.appfunctions.AppFunctionManager.
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java
index fa6d2ff..593c521 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java
@@ -34,8 +34,8 @@
     @NonNull private final String mTargetPackageName;
 
     /**
-     * Returns the unique string identifier of the app function to be executed. TODO(b/357551503):
-     * Document how callers can get the available function identifiers.
+     * The unique string identifier of the app function to be executed. This identifier is used to
+     * execute a specific app function.
      */
     @NonNull private final String mFunctionIdentifier;
 
@@ -49,8 +49,6 @@
      *
      * <p>The document may have missing parameters. Developers are advised to implement defensive
      * handling measures.
-     *
-     * <p>TODO(b/357551503): Document how function parameters can be obtained for function execution
      */
     @NonNull private final GenericDocument mParameters;
 
@@ -71,7 +69,19 @@
         return mTargetPackageName;
     }
 
-    /** Returns the unique string identifier of the app function to be executed. */
+    /**
+     * Returns the unique string identifier of the app function to be executed.
+     *
+     * <p>When there is a package change or the device starts up, the metadata of available
+     * functions is indexed by AppSearch. AppSearch stores the indexed information as {@code
+     * AppFunctionStaticMetadata} document.
+     *
+     * <p>The ID can be obtained by querying the {@code AppFunctionStaticMetadata} documents from
+     * AppSearch.
+     *
+     * <p>If the {@code functionId} provided is invalid, the caller will get an invalid argument
+     * response.
+     */
     @NonNull
     public String getFunctionIdentifier() {
         return mFunctionIdentifier;
@@ -83,6 +93,12 @@
      *
      * <p>The bundle may have missing parameters. Developers are advised to implement defensive
      * handling measures.
+     *
+     * <p>Similar to {@link #getFunctionIdentifier()} the parameters required by a function can be
+     * obtained by querying AppSearch for the corresponding {@code AppFunctionStaticMetadata}. This
+     * metadata will contain enough information for the caller to resolve the required parameters
+     * either using information from the metadata itself or using the AppFunction SDK for function
+     * callers.
      */
     @NonNull
     public GenericDocument getParameters() {
@@ -128,10 +144,7 @@
         @NonNull
         public ExecuteAppFunctionRequest build() {
             return new ExecuteAppFunctionRequest(
-                    mTargetPackageName,
-                    mFunctionIdentifier,
-                    mExtras,
-                    mParameters);
+                    mTargetPackageName, mFunctionIdentifier, mExtras, mParameters);
         }
     }
 }
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
index 969e5d5..d5dfaeb 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
@@ -56,14 +56,15 @@
     /** The caller does not have the permission to execute an app function. */
     public static final int RESULT_DENIED = 1;
 
-    /** An unknown error occurred while processing the call in the AppFunctionService. */
+    /**
+     * An unknown error occurred while processing the call in the AppFunctionService.
+     *
+     * <p>This error is thrown when the service is connected in the remote application but an
+     * unexpected error is thrown from the bound application.
+     */
     public static final int RESULT_APP_UNKNOWN_ERROR = 2;
 
-    /**
-     * An internal error occurred within AppFunctionManagerService.
-     *
-     * <p>This error may be considered similar to {@link IllegalStateException}
-     */
+    /** An internal unexpected error coming from the system. */
     public static final int RESULT_INTERNAL_ERROR = 3;
 
     /**
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 4627be3..8250a53 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -1161,7 +1161,7 @@
         final IAudioService service = getService();
         try {
             service.setMasterMute(mute, flags, getContext().getOpPackageName(),
-                    UserHandle.getCallingUserId(), getContext().getAttributionTag());
+                    getContext().getUserId(), getContext().getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -3278,7 +3278,7 @@
         final IAudioService service = getService();
         try {
             service.setMicrophoneMute(on, getContext().getOpPackageName(),
-                    UserHandle.getCallingUserId(), getContext().getAttributionTag());
+                    getContext().getUserId(), getContext().getAttributionTag());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/media/java/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/IMediaQualityManager.aidl
new file mode 100644
index 0000000..4f1210a7
--- /dev/null
+++ b/media/java/android/media/quality/IMediaQualityManager.aidl
@@ -0,0 +1,24 @@
+/*
+ * 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.quality;
+
+/**
+ * Interface for Media Quality Manager
+ * @hide
+ */
+interface IMediaQualityManager {
+}
\ No newline at end of file
diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java
new file mode 100644
index 0000000..86dbcdb
--- /dev/null
+++ b/media/java/android/media/quality/MediaQualityManager.java
@@ -0,0 +1,44 @@
+/*
+ * 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.quality;
+
+import android.annotation.FlaggedApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.media.tv.flags.Flags;
+
+/**
+ * Expose TV setting APIs for the application to use
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_MEDIA_QUALITY_FW)
+@SystemService(Context.MEDIA_QUALITY_SERVICE)
+public class MediaQualityManager {
+    // TODO: unhide the APIs for api review
+    private static final String TAG = "MediaQualityManager";
+
+    private final IMediaQualityManager mService;
+    private final Context mContext;
+
+    /**
+     * @hide
+     */
+    public MediaQualityManager(Context context, IMediaQualityManager service) {
+        mContext = context;
+        mService = service;
+    }
+}
diff --git a/media/java/android/media/quality/OWNERS b/media/java/android/media/quality/OWNERS
new file mode 100644
index 0000000..2f69027
--- /dev/null
+++ b/media/java/android/media/quality/OWNERS
@@ -0,0 +1 @@
+file:/services/core/java/com/android/server/media/quality/OWNERS
\ No newline at end of file
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
index 6992005..94d373b 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
@@ -17,7 +17,15 @@
 package com.android.settingslib.metadata
 
 /** A node in preference hierarchy that is associated with [PreferenceMetadata]. */
-open class PreferenceHierarchyNode internal constructor(val metadata: PreferenceMetadata)
+open class PreferenceHierarchyNode internal constructor(val metadata: PreferenceMetadata) {
+    /**
+     * Preference order in the hierarchy.
+     *
+     * When a preference appears on different screens, different order values could be specified.
+     */
+    var order: Int? = null
+        internal set
+}
 
 /**
  * Preference hierarchy describes the structure of preferences recursively.
@@ -30,9 +38,7 @@
     private val children = mutableListOf<PreferenceHierarchyNode>()
 
     /** Adds a preference to the hierarchy. */
-    operator fun PreferenceMetadata.unaryPlus() {
-        children.add(PreferenceHierarchyNode(this))
-    }
+    operator fun PreferenceMetadata.unaryPlus() = +PreferenceHierarchyNode(this)
 
     /**
      * Adds preference screen with given key (as a placeholder) to the hierarchy.
@@ -45,35 +51,67 @@
      *
      * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry]
      */
-    operator fun String.unaryPlus() {
-        children.add(PreferenceHierarchyNode(PreferenceScreenRegistry[this]!!))
-    }
+    operator fun String.unaryPlus() = +PreferenceHierarchyNode(PreferenceScreenRegistry[this]!!)
+
+    operator fun PreferenceHierarchyNode.unaryPlus() = also { children.add(it) }
+
+    /** Specifies preference order in the hierarchy. */
+    infix fun PreferenceHierarchyNode.order(order: Int) = apply { this.order = order }
 
     /** Adds a preference to the hierarchy. */
-    fun add(metadata: PreferenceMetadata) {
-        children.add(PreferenceHierarchyNode(metadata))
+    @JvmOverloads
+    fun add(metadata: PreferenceMetadata, order: Int? = null) {
+        PreferenceHierarchyNode(metadata).also {
+            it.order = order
+            children.add(it)
+        }
     }
 
-    /** Adds a preference to the hierarchy before a specific preference. */
+    /** Adds a preference to the hierarchy before given key. */
     fun addBefore(key: String, metadata: PreferenceMetadata) {
-        var foundIndex = children.indexOfFirst { it.metadata.key == key }
-        if (foundIndex == -1) foundIndex = children.size
-        children.add(foundIndex, PreferenceHierarchyNode(metadata))
+        val (list, index) = findPreference(key) ?: (children to children.size)
+        list.add(index, PreferenceHierarchyNode(metadata))
     }
 
-    /** Adds a preference to the hierarchy after a specific preference. */
+    /** Adds a preference group to the hierarchy before given key. */
+    fun addGroupBefore(key: String, metadata: PreferenceMetadata): PreferenceHierarchy {
+        val (list, index) = findPreference(key) ?: (children to children.size)
+        return PreferenceHierarchy(metadata).also { list.add(index, it) }
+    }
+
+    /** Adds a preference to the hierarchy after given key. */
     fun addAfter(key: String, metadata: PreferenceMetadata) {
-        var foundIndex = children.indexOfFirst { it.metadata.key == key }
-        if (foundIndex == -1) foundIndex = children.size else foundIndex++
-        children.add(foundIndex, PreferenceHierarchyNode(metadata))
+        val (list, index) = findPreference(key) ?: (children to children.size - 1)
+        list.add(index + 1, PreferenceHierarchyNode(metadata))
+    }
+
+    /** Adds a preference group to the hierarchy after given key. */
+    fun addGroupAfter(key: String, metadata: PreferenceMetadata): PreferenceHierarchy {
+        val (list, index) = findPreference(key) ?: (children to children.size - 1)
+        return PreferenceHierarchy(metadata).also { list.add(index + 1, it) }
+    }
+
+    private fun findPreference(key: String): Pair<MutableList<PreferenceHierarchyNode>, Int>? {
+        children.forEachIndexed { index, node ->
+            if (node.metadata.key == key) return children to index
+            if (node is PreferenceHierarchy) {
+                val result = node.findPreference(key)
+                if (result != null) return result
+            }
+        }
+        return null
     }
 
     /** Adds a preference group to the hierarchy. */
     operator fun PreferenceGroup.unaryPlus() = PreferenceHierarchy(this).also { children.add(it) }
 
     /** Adds a preference group and returns its preference hierarchy. */
-    fun addGroup(metadata: PreferenceGroup): PreferenceHierarchy =
-        PreferenceHierarchy(metadata).also { children.add(it) }
+    @JvmOverloads
+    fun addGroup(metadata: PreferenceGroup, order: Int? = null): PreferenceHierarchy =
+        PreferenceHierarchy(metadata).also {
+            this.order = order
+            children.add(it)
+        }
 
     /**
      * Adds preference screen with given key (as a placeholder) to the hierarchy.
@@ -91,7 +129,7 @@
     }
 
     /** Extensions to add more preferences to the hierarchy. */
-    operator fun plusAssign(init: PreferenceHierarchy.() -> Unit) = init(this)
+    operator fun PreferenceHierarchy.plusAssign(init: PreferenceHierarchy.() -> Unit) = init(this)
 
     /** Traversals preference hierarchy and applies given action. */
     fun forEach(action: (PreferenceHierarchyNode) -> Unit) {
@@ -117,17 +155,17 @@
         return null
     }
 
-    /** Returns all the [PreferenceMetadata]s appear in the hierarchy. */
-    fun getAllPreferences(): List<PreferenceMetadata> =
-        mutableListOf<PreferenceMetadata>().also { getAllPreferences(it) }
+    /** Returns all the [PreferenceHierarchyNode]s appear in the hierarchy. */
+    fun getAllPreferences(): List<PreferenceHierarchyNode> =
+        mutableListOf<PreferenceHierarchyNode>().also { getAllPreferences(it) }
 
-    private fun getAllPreferences(result: MutableList<PreferenceMetadata>) {
-        result.add(metadata)
+    private fun getAllPreferences(result: MutableList<PreferenceHierarchyNode>) {
+        result.add(this)
         for (child in children) {
             if (child is PreferenceHierarchy) {
                 child.getAllPreferences(result)
             } else {
-                result.add(child.metadata)
+                result.add(child)
             }
         }
     }
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
index 33f2dbf..10087a7 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
@@ -19,10 +19,10 @@
 import android.content.Context
 import android.content.Intent
 import android.os.Bundle
-import androidx.fragment.app.Fragment
 import androidx.annotation.AnyThread
 import androidx.annotation.DrawableRes
 import androidx.annotation.StringRes
+import androidx.fragment.app.Fragment
 
 /**
  * Interface provides preference metadata (title, summary, icon, etc.).
@@ -134,9 +134,6 @@
     /** Returns preference intent. */
     fun intent(context: Context): Intent? = null
 
-    /** Returns preference order. */
-    fun order(context: Context): Int? = null
-
     /**
      * Returns the preference title.
      *
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
index bcfef67..72cf403 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
@@ -80,7 +80,6 @@
             preference.isVisible =
                 (this as? PreferenceAvailabilityProvider)?.isAvailable(context) != false
             preference.isPersistent = isPersistent(context)
-            metadata.order(context)?.let { preference.order = it }
             // PreferenceRegistry will notify dependency change, so we do not need to set
             // dependency here. This simplifies dependency management and avoid the
             // IllegalStateException when call Preference.setDependency
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt
index 43f2cb6..87c289f 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt
@@ -16,14 +16,33 @@
 
 package com.android.settingslib.preference
 
+import androidx.preference.Preference
 import com.android.settingslib.metadata.MainSwitchPreference
 import com.android.settingslib.metadata.PreferenceGroup
+import com.android.settingslib.metadata.PreferenceHierarchyNode
 import com.android.settingslib.metadata.PreferenceMetadata
 import com.android.settingslib.metadata.SwitchPreference
 
 /** Factory to map [PreferenceMetadata] to [PreferenceBinding]. */
 interface PreferenceBindingFactory {
 
+    /**
+     * Binds [Preference] and its associated [PreferenceMetadata] with given [PreferenceBinding]
+     * (`getPreferenceBinding(metadata)` is used if [preferenceBinding] is `null`).
+     *
+     * Subclass could override this callback to handle common binding logic. For instance,
+     * restricted preference with policy transparency.
+     */
+    fun bind(
+        preference: Preference,
+        node: PreferenceHierarchyNode,
+        preferenceBinding: PreferenceBinding? = null,
+    ) {
+        val binding = (preferenceBinding ?: getPreferenceBinding(node.metadata)) ?: return
+        binding.bind(preference, node.metadata)
+        node.order?.let { preference.order = it }
+    }
+
     /** Returns the [PreferenceBinding] associated with the [PreferenceMetadata]. */
     fun getPreferenceBinding(metadata: PreferenceMetadata): PreferenceBinding?
 }
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
index 21621a8..5f4b88f 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
@@ -129,7 +129,7 @@
     }
 
     protected fun getPreferenceKeysInHierarchy(): Set<String> =
-        preferenceScreenBindingHelper?.getPreferences()?.map { it.key }?.toSet() ?: setOf()
+        preferenceScreenBindingHelper?.getPreferences()?.map { it.metadata.key }?.toSet() ?: setOf()
 
     companion object {
         private const val TAG = "PreferenceFragment"
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt
index 5ef7823..c26ba18 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt
@@ -21,7 +21,6 @@
 import com.android.settingslib.datastore.KeyValueStore
 import com.android.settingslib.metadata.PersistentPreference
 import com.android.settingslib.metadata.PreferenceHierarchy
-import com.android.settingslib.metadata.PreferenceMetadata
 
 /** Inflates [PreferenceHierarchy] into given [PreferenceGroup] recursively. */
 fun PreferenceGroup.inflatePreferenceHierarchy(
@@ -29,12 +28,11 @@
     hierarchy: PreferenceHierarchy,
     storages: MutableMap<KeyValueStore, PreferenceDataStore> = mutableMapOf(),
 ) {
-    fun PreferenceMetadata.preferenceBinding() = preferenceBindingFactory.getPreferenceBinding(this)
-
-    hierarchy.metadata.let { it.preferenceBinding()?.bind(this, it) }
+    preferenceBindingFactory.bind(this, hierarchy)
     hierarchy.forEach {
         val metadata = it.metadata
-        val preferenceBinding = metadata.preferenceBinding() ?: return@forEach
+        val preferenceBinding =
+            preferenceBindingFactory.getPreferenceBinding(metadata) ?: return@forEach
         val widget = preferenceBinding.createWidget(context)
         if (it is PreferenceHierarchy) {
             val preferenceGroup = widget as PreferenceGroup
@@ -42,7 +40,7 @@
             addPreference(preferenceGroup)
             preferenceGroup.inflatePreferenceHierarchy(preferenceBindingFactory, it)
         } else {
-            preferenceBinding.bind(widget, metadata)
+            preferenceBindingFactory.bind(widget, it, preferenceBinding)
             (metadata as? PersistentPreference<*>)?.storage(context)?.let { storage ->
                 widget.preferenceDataStore =
                     storages.getOrPut(storage) { PreferenceDataStoreAdapter(storage) }
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
index ad4cc33..7af9c81 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -30,6 +30,7 @@
 import com.android.settingslib.datastore.KeyedObserver
 import com.android.settingslib.metadata.PersistentPreference
 import com.android.settingslib.metadata.PreferenceHierarchy
+import com.android.settingslib.metadata.PreferenceHierarchyNode
 import com.android.settingslib.metadata.PreferenceLifecycleContext
 import com.android.settingslib.metadata.PreferenceLifecycleProvider
 import com.android.settingslib.metadata.PreferenceMetadata
@@ -67,7 +68,7 @@
             ) = fragment.startActivityForResult(intent, requestCode, options)
         }
 
-    private val preferences: ImmutableMap<String, PreferenceMetadata>
+    private val preferences: ImmutableMap<String, PreferenceHierarchyNode>
     private val dependencies: ImmutableMultimap<String, String>
     private val lifecycleAwarePreferences: Array<PreferenceLifecycleProvider>
     private val storages = mutableSetOf<KeyedObservable<String>>()
@@ -82,27 +83,29 @@
         }
 
     init {
-        val preferencesBuilder = ImmutableMap.builder<String, PreferenceMetadata>()
+        val preferencesBuilder = ImmutableMap.builder<String, PreferenceHierarchyNode>()
         val dependenciesBuilder = ImmutableMultimap.builder<String, String>()
         val lifecycleAwarePreferences = mutableListOf<PreferenceLifecycleProvider>()
         fun PreferenceMetadata.addDependency(dependency: PreferenceMetadata) {
             dependenciesBuilder.put(key, dependency.key)
         }
 
-        fun PreferenceMetadata.add() {
-            preferencesBuilder.put(key, this)
-            dependencyOfEnabledState(context)?.addDependency(this)
-            if (this is PreferenceLifecycleProvider) lifecycleAwarePreferences.add(this)
-            if (this is PersistentPreference<*>) storages.add(storage(context))
+        fun PreferenceHierarchyNode.addNode() {
+            metadata.let {
+                preferencesBuilder.put(it.key, this)
+                it.dependencyOfEnabledState(context)?.addDependency(it)
+                if (it is PreferenceLifecycleProvider) lifecycleAwarePreferences.add(it)
+                if (it is PersistentPreference<*>) storages.add(it.storage(context))
+            }
         }
 
         fun PreferenceHierarchy.addPreferences() {
-            metadata.add()
+            addNode()
             forEach {
                 if (it is PreferenceHierarchy) {
                     it.addPreferences()
                 } else {
-                    it.metadata.add()
+                    it.addNode()
                 }
             }
         }
@@ -122,7 +125,7 @@
 
         // bind preference to update UI
         preferenceScreen.findPreference<Preference>(key)?.let {
-            preferenceBindingFactory.bind(it, preferences[key])
+            preferences[key]?.let { node -> preferenceBindingFactory.bind(it, node) }
         }
 
         // check reason to avoid potential infinite loop
@@ -217,15 +220,15 @@
         ) =
             preferenceScreen.bindRecursively(
                 preferenceBindingFactory,
-                preferenceHierarchy.getAllPreferences().associateBy { it.key },
+                preferenceHierarchy.getAllPreferences().associateBy { it.metadata.key },
             )
 
         private fun PreferenceGroup.bindRecursively(
             preferenceBindingFactory: PreferenceBindingFactory,
-            preferences: Map<String, PreferenceMetadata>,
+            preferences: Map<String, PreferenceHierarchyNode>,
             storages: MutableMap<KeyValueStore, PreferenceDataStore> = mutableMapOf(),
         ) {
-            preferenceBindingFactory.bind(this, preferences[key])
+            preferences[key]?.let { preferenceBindingFactory.bind(this, it) }
             val count = preferenceCount
             for (index in 0 until count) {
                 val preference = getPreference(index)
@@ -233,7 +236,7 @@
                     preference.bindRecursively(preferenceBindingFactory, preferences, storages)
                 } else {
                     preferences[preference.key]?.let {
-                        preferenceBindingFactory.getPreferenceBinding(it)?.bind(preference, it)
+                        preferenceBindingFactory.bind(preference, it)
                         (it as? PersistentPreference<*>)?.storage(context)?.let { storage ->
                             preference.preferenceDataStore =
                                 storages.getOrPut(storage) { PreferenceDataStoreAdapter(storage) }
@@ -242,10 +245,5 @@
                 }
             }
         }
-
-        private fun PreferenceBindingFactory.bind(
-            preference: Preference,
-            metadata: PreferenceMetadata?,
-        ) = metadata?.let { getPreferenceBinding(it)?.bind(preference, it) }
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 8845d2e..8641f70 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -322,18 +322,27 @@
     }
 
     private void updatePreferredTransport() {
-        if (mProfiles.stream().noneMatch(p -> p instanceof LeAudioProfile)
-                || mProfiles.stream().noneMatch(p -> p instanceof HidProfile)) {
+        LeAudioProfile leAudioProfile =
+                (LeAudioProfile)
+                        mProfiles.stream()
+                                .filter(p -> p instanceof LeAudioProfile)
+                                .findFirst()
+                                .orElse(null);
+        HidProfile hidProfile =
+                (HidProfile)
+                        mProfiles.stream()
+                                .filter(p -> p instanceof HidProfile)
+                                .findFirst()
+                                .orElse(null);
+        if (leAudioProfile == null || hidProfile == null) {
             return;
         }
         // Both LeAudioProfile and HidProfile are connectable.
-        if (!mProfileManager
-                .getHidProfile()
-                .setPreferredTransport(
-                        mDevice,
-                        mProfileManager.getLeAudioProfile().isEnabled(mDevice)
-                                ? BluetoothDevice.TRANSPORT_LE
-                                : BluetoothDevice.TRANSPORT_BREDR)) {
+        if (!hidProfile.setPreferredTransport(
+                mDevice,
+                leAudioProfile.isEnabled(mDevice)
+                        ? BluetoothDevice.TRANSPORT_LE
+                        : BluetoothDevice.TRANSPORT_BREDR)) {
             Log.w(TAG, "Fail to set preferred transport");
         }
     }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index 70cb2ef..30f8a79 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -1952,9 +1952,6 @@
 
     @Test
     public void leAudioHidDevice_leAudioEnabled_setPreferredTransportToLE() {
-
-        when(mProfileManager.getHidProfile()).thenReturn(mHidProfile);
-        when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
         when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true);
 
         updateProfileStatus(mHidProfile, BluetoothProfile.STATE_CONNECTED);
@@ -1965,8 +1962,6 @@
 
     @Test
     public void leAudioHidDevice_leAudioDisabled_setPreferredTransportToBredr() {
-        when(mProfileManager.getHidProfile()).thenReturn(mHidProfile);
-        when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
         when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(false);
 
         updateProfileStatus(mLeAudioProfile, BluetoothProfile.STATE_CONNECTED);
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index 3b72df7..1a8c7f8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -30,6 +30,8 @@
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
 import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -60,14 +62,15 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.scale
 import androidx.compose.ui.graphics.asImageBitmap
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.pointer.PointerInputChange
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
@@ -123,8 +126,8 @@
     dialogFactory: BouncerDialogFactory,
     modifier: Modifier = Modifier,
 ) {
-    val isSideBySideSupported by viewModel.isSideBySideSupported.collectAsStateWithLifecycle()
-    val layout = calculateLayout(isSideBySideSupported = isSideBySideSupported)
+    val isOneHandedModeSupported by viewModel.isOneHandedModeSupported.collectAsStateWithLifecycle()
+    val layout = calculateLayout(isOneHandedModeSupported = isOneHandedModeSupported)
 
     BouncerContent(layout, viewModel, dialogFactory, modifier)
 }
@@ -137,6 +140,7 @@
     dialogFactory: BouncerDialogFactory,
     modifier: Modifier,
 ) {
+    val scale by viewModel.scale.collectAsStateWithLifecycle()
     Box(
         // Allows the content within each of the layouts to react to the appearance and
         // disappearance of the IME, which is also known as the software keyboard.
@@ -144,7 +148,7 @@
         // Despite the keyboard only being part of the password bouncer, adding it at this level is
         // both necessary to properly handle the keyboard in all layouts and harmless in cases when
         // the keyboard isn't used (like the PIN or pattern auth methods).
-        modifier = modifier.imePadding().onKeyEvent(viewModel::onKeyEvent)
+        modifier = modifier.imePadding().onKeyEvent(viewModel::onKeyEvent).scale(scale)
     ) {
         when (layout) {
             BouncerSceneLayout.STANDARD_BOUNCER -> StandardLayout(viewModel = viewModel)
@@ -300,28 +304,54 @@
     viewModel: BouncerSceneContentViewModel,
     modifier: Modifier = Modifier,
 ) {
-    val layoutDirection = LocalLayoutDirection.current
-    val isLeftToRight = layoutDirection == LayoutDirection.Ltr
-    val (isSwapped, setSwapped) = rememberSaveable(isLeftToRight) { mutableStateOf(!isLeftToRight) }
+    val isLeftToRight = LocalLayoutDirection.current == LayoutDirection.Ltr
+    val isInputPreferredOnLeftSide by
+        viewModel.isInputPreferredOnLeftSide.collectAsStateWithLifecycle()
+    // Swaps the order of user switcher and bouncer input area
+    // Default layout is assumed as user switcher followed by bouncer input area in the direction
+    // of layout.
+    val isSwapped = isLeftToRight == isInputPreferredOnLeftSide
     val isHeightExpanded =
         LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Expanded
     val authMethod by viewModel.authMethodViewModel.collectAsStateWithLifecycle()
 
     var swapAnimationEnd by remember { mutableStateOf(false) }
 
+    fun wasEventOnNonInputHalfOfScreen(x: Float, totalWidth: Int): Boolean {
+        // Default layout is assumed as user switcher followed by bouncer input area in
+        // the direction of layout. Swapped layout means that bouncer input area is first, followed
+        // by user switcher in the direction of layout.
+        val halfWidth = totalWidth / 2
+        return if (x > halfWidth) {
+            isLeftToRight && isSwapped
+        } else {
+            isLeftToRight && !isSwapped
+        }
+    }
+
     Row(
         modifier =
             modifier
-                .pointerInput(Unit) {
+                .pointerInput(isSwapped, isInputPreferredOnLeftSide) {
                     detectTapGestures(
                         onDoubleTap = { offset ->
                             // Depending on where the user double tapped, switch the elements such
                             // that the non-swapped element is closer to the side that was double
                             // tapped.
-                            setSwapped(offset.x < size.width / 2)
+                            viewModel.onDoubleTap(
+                                wasEventOnNonInputHalfOfScreen(offset.x, size.width)
+                            )
                         }
                     )
                 }
+                .pointerInput(isSwapped, isInputPreferredOnLeftSide) {
+                    awaitEachGesture {
+                        val downEvent: PointerInputChange = awaitFirstDown()
+                        viewModel.onDown(
+                            wasEventOnNonInputHalfOfScreen(downEvent.position.x, size.width)
+                        )
+                    }
+                }
                 .testTag("BesideUserSwitcherLayout")
                 .motionTestValues {
                     swapAnimationEnd exportAs BouncerMotionTestKeys.swapAnimationEnd
@@ -726,7 +756,8 @@
 /** Renders the UI of the user switcher that's displayed on large screens next to the bouncer UI. */
 @Composable
 private fun UserSwitcher(viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier) {
-    if (!viewModel.isUserSwitcherVisible) {
+    val isUserSwitcherVisible by viewModel.isUserSwitcherVisible.collectAsStateWithLifecycle()
+    if (!isUserSwitcherVisible) {
         // Take up the same space as the user switcher normally would, but with nothing inside it.
         Box(modifier = modifier)
         return
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt
index 1c3d93c..eb62d33 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt
@@ -26,19 +26,17 @@
 
 /**
  * Returns the [BouncerSceneLayout] that should be used by the bouncer scene. If
- * [isSideBySideSupported] is `false`, then [BouncerSceneLayout.BESIDE_USER_SWITCHER] is replaced by
- * [BouncerSceneLayout.STANDARD_BOUNCER].
+ * [isOneHandedModeSupported] is `false`, then [BouncerSceneLayout.BESIDE_USER_SWITCHER] is replaced
+ * by [BouncerSceneLayout.STANDARD_BOUNCER].
  */
 @Composable
-fun calculateLayout(
-    isSideBySideSupported: Boolean,
-): BouncerSceneLayout {
+fun calculateLayout(isOneHandedModeSupported: Boolean): BouncerSceneLayout {
     val windowSizeClass = LocalWindowSizeClass.current
 
     return calculateLayoutInternal(
         width = windowSizeClass.widthSizeClass.toEnum(),
         height = windowSizeClass.heightSizeClass.toEnum(),
-        isSideBySideSupported = isSideBySideSupported,
+        isOneHandedModeSupported = isOneHandedModeSupported,
     )
 }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 361b078..521b346 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.bouncer.domain.interactor
 
+import android.content.testableContext
+import android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.uiEventLoggerFake
@@ -26,16 +28,22 @@
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
+import com.android.systemui.authentication.shared.model.BouncerInputSide
+import com.android.systemui.bouncer.data.repository.bouncerRepository
 import com.android.systemui.bouncer.shared.logging.BouncerUiEvent
+import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
 import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER
+import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
+import com.android.systemui.util.settings.fakeGlobalSettings
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -58,7 +66,8 @@
     private val authenticationInteractor = kosmos.authenticationInteractor
     private val uiEventLoggerFake = kosmos.uiEventLoggerFake
 
-    private lateinit var underTest: BouncerInteractor
+    private val underTest: BouncerInteractor by lazy { kosmos.bouncerInteractor }
+    private val testableResources by lazy { kosmos.testableContext.orCreateTestableResources }
 
     @Before
     fun setUp() {
@@ -70,8 +79,6 @@
         overrideResource(R.string.kg_wrong_pin, MESSAGE_WRONG_PIN)
         overrideResource(R.string.kg_wrong_password, MESSAGE_WRONG_PASSWORD)
         overrideResource(R.string.kg_wrong_pattern, MESSAGE_WRONG_PATTERN)
-
-        underTest = kosmos.bouncerInteractor
     }
 
     @Test
@@ -116,7 +123,7 @@
             assertThat(
                     underTest.authenticate(
                         FakeAuthenticationRepository.DEFAULT_PIN,
-                        tryAutoConfirm = true
+                        tryAutoConfirm = true,
                     )
                 )
                 .isEqualTo(AuthenticationResult.SUCCEEDED)
@@ -141,7 +148,7 @@
             assertThat(
                     underTest.authenticate(
                         FakeAuthenticationRepository.DEFAULT_PIN,
-                        tryAutoConfirm = true
+                        tryAutoConfirm = true,
                     )
                 )
                 .isEqualTo(AuthenticationResult.SKIPPED)
@@ -209,7 +216,7 @@
             val tooShortPattern =
                 FakeAuthenticationRepository.PATTERN.subList(
                     0,
-                    kosmos.fakeAuthenticationRepository.minPatternLength - 1
+                    kosmos.fakeAuthenticationRepository.minPatternLength - 1,
                 )
             assertThat(underTest.authenticate(tooShortPattern))
                 .isEqualTo(AuthenticationResult.SKIPPED)
@@ -292,6 +299,77 @@
             assertThat(isFaceAuthRunning).isFalse()
         }
 
+    @Test
+    fun verifyOneHandedModeUsesTheConfigValue() =
+        testScope.runTest {
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin
+            )
+            testableResources.addOverride(R.bool.can_use_one_handed_bouncer, false)
+            val oneHandedModelSupported by collectLastValue(underTest.isOneHandedModeSupported)
+
+            assertThat(oneHandedModelSupported).isFalse()
+
+            testableResources.addOverride(R.bool.can_use_one_handed_bouncer, true)
+            kosmos.fakeConfigurationRepository.onAnyConfigurationChange()
+            runCurrent()
+
+            assertThat(oneHandedModelSupported).isTrue()
+
+            testableResources.removeOverride(R.bool.can_use_one_handed_bouncer)
+        }
+
+    @Test
+    fun verifyPreferredInputSideUsesTheSettingValue_Left() =
+        testScope.runTest {
+            val preferredInputSide by collectLastValue(underTest.preferredBouncerInputSide)
+            kosmos.bouncerRepository.setPreferredBouncerInputSide(BouncerInputSide.LEFT)
+            runCurrent()
+
+            assertThat(preferredInputSide).isEqualTo(BouncerInputSide.LEFT)
+        }
+
+    @Test
+    fun verifyPreferredInputSideUsesTheSettingValue_Right() =
+        testScope.runTest {
+            val preferredInputSide by collectLastValue(underTest.preferredBouncerInputSide)
+            underTest.setPreferredBouncerInputSide(BouncerInputSide.RIGHT)
+            runCurrent()
+
+            assertThat(preferredInputSide).isEqualTo(BouncerInputSide.RIGHT)
+
+            underTest.setPreferredBouncerInputSide(BouncerInputSide.LEFT)
+            runCurrent()
+
+            assertThat(preferredInputSide).isEqualTo(BouncerInputSide.LEFT)
+        }
+
+    @Test
+    fun preferredInputSide_defaultsToRight_whenUserSwitcherIsEnabled() =
+        testScope.runTest {
+            testableResources.addOverride(R.bool.config_enableBouncerUserSwitcher, true)
+            kosmos.fakeFeatureFlagsClassic.set(FULL_SCREEN_USER_SWITCHER, true)
+            kosmos.bouncerRepository.preferredBouncerInputSide.value = null
+            val preferredInputSide by collectLastValue(underTest.preferredBouncerInputSide)
+
+            assertThat(preferredInputSide).isEqualTo(BouncerInputSide.RIGHT)
+            testableResources.removeOverride(R.bool.config_enableBouncerUserSwitcher)
+        }
+
+    @Test
+    fun preferredInputSide_defaultsToLeft_whenUserSwitcherIsNotEnabledAndOneHandedModeIsEnabled() =
+        testScope.runTest {
+            testableResources.addOverride(R.bool.config_enableBouncerUserSwitcher, false)
+            kosmos.fakeFeatureFlagsClassic.set(FULL_SCREEN_USER_SWITCHER, true)
+            testableResources.addOverride(R.bool.can_use_one_handed_bouncer, true)
+            kosmos.fakeGlobalSettings.putInt(ONE_HANDED_KEYGUARD_SIDE, -1)
+            val preferredInputSide by collectLastValue(underTest.preferredBouncerInputSide)
+
+            assertThat(preferredInputSide).isEqualTo(BouncerInputSide.LEFT)
+            testableResources.removeOverride(R.bool.config_enableBouncerUserSwitcher)
+            testableResources.removeOverride(R.bool.can_use_one_handed_bouncer)
+        }
+
     companion object {
         private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN"
         private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt
index 923687b..3ede841 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt
@@ -25,10 +25,10 @@
 import com.google.common.truth.Truth.assertThat
 import java.util.Locale
 import org.junit.Test
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4
-import platform.test.runner.parameterized.Parameter
-import platform.test.runner.parameterized.Parameters
 import org.junit.runner.RunWith
+import platform.test.runner.parameterized.Parameter
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
@@ -41,6 +41,7 @@
             height = SizeClass.EXPANDED,
             naturallyHeld = Vertically,
         )
+
     data object Tablet :
         Device(
             name = "tablet",
@@ -48,6 +49,7 @@
             height = SizeClass.MEDIUM,
             naturallyHeld = Horizontally,
         )
+
     data object Folded :
         Device(
             name = "folded",
@@ -55,6 +57,7 @@
             height = SizeClass.MEDIUM,
             naturallyHeld = Vertically,
         )
+
     data object Unfolded :
         Device(
             name = "unfolded",
@@ -64,6 +67,7 @@
             widthWhenUnnaturallyHeld = SizeClass.MEDIUM,
             heightWhenUnnaturallyHeld = SizeClass.MEDIUM,
         )
+
     data object TallerFolded :
         Device(
             name = "taller folded",
@@ -71,6 +75,7 @@
             height = SizeClass.EXPANDED,
             naturallyHeld = Vertically,
         )
+
     data object TallerUnfolded :
         Device(
             name = "taller unfolded",
@@ -131,7 +136,7 @@
                                 TestCase(
                                     device = device,
                                     held = device.naturallyHeld,
-                                    isSideBySideSupported = false,
+                                    isOneHandedModeSupported = false,
                                     expected = STANDARD_BOUNCER,
                                 )
                             )
@@ -151,7 +156,7 @@
                                 TestCase(
                                     device = device,
                                     held = device.naturallyHeld.flip(),
-                                    isSideBySideSupported = false,
+                                    isOneHandedModeSupported = false,
                                     expected = STANDARD_BOUNCER,
                                 )
                             )
@@ -170,7 +175,7 @@
                         calculateLayoutInternal(
                             width = device.width(whenHeld = held),
                             height = device.height(whenHeld = held),
-                            isSideBySideSupported = isSideBySideSupported,
+                            isOneHandedModeSupported = isOneHandedModeSupported,
                         )
                     )
                     .isEqualTo(expected)
@@ -182,7 +187,7 @@
         val device: Device,
         val held: Held,
         val expected: BouncerSceneLayout,
-        val isSideBySideSupported: Boolean = true,
+        val isOneHandedModeSupported: Boolean = true,
     ) {
         override fun toString(): String {
             return buildString {
@@ -190,8 +195,8 @@
                 append(" width: ${device.width(held).name.lowercase(Locale.US)}")
                 append(" height: ${device.height(held).name.lowercase(Locale.US)}")
                 append(" when held $held")
-                if (!isSideBySideSupported) {
-                    append(" (side-by-side not supported)")
+                if (!isOneHandedModeSupported) {
+                    append(" (one-handed-mode not supported)")
                 }
             }
         }
@@ -242,11 +247,13 @@
     sealed class Held {
         abstract fun flip(): Held
     }
+
     data object Vertically : Held() {
         override fun flip(): Held {
             return Horizontally
         }
     }
+
     data object Horizontally : Held() {
         override fun flip(): Held {
             return Vertically
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModelTest.kt
index 9bddcd2..3bf4460 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
+import android.content.testableContext
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -35,6 +36,7 @@
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.res.R
 import com.android.systemui.scene.domain.startable.sceneContainerStartable
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -166,21 +168,35 @@
         }
 
     @Test
-    fun isSideBySideSupported() =
+    fun isOneHandedModeSupported() =
         testScope.runTest {
-            val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported)
+            val isOneHandedModeSupported by collectLastValue(underTest.isOneHandedModeSupported)
             kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            kosmos.testableContext.orCreateTestableResources.addOverride(
+                R.bool.config_enableBouncerUserSwitcher,
+                true,
+            )
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
-            assertThat(isSideBySideSupported).isTrue()
+            assertThat(isOneHandedModeSupported).isTrue()
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password)
-            assertThat(isSideBySideSupported).isTrue()
+            assertThat(isOneHandedModeSupported).isTrue()
 
             kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+            kosmos.testableContext.orCreateTestableResources.addOverride(
+                R.bool.can_use_one_handed_bouncer,
+                true,
+            )
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
-            assertThat(isSideBySideSupported).isTrue()
+            assertThat(isOneHandedModeSupported).isTrue()
 
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password)
-            assertThat(isSideBySideSupported).isFalse()
+            assertThat(isOneHandedModeSupported).isFalse()
+            kosmos.testableContext.orCreateTestableResources.removeOverride(
+                R.bool.config_enableBouncerUserSwitcher
+            )
+            kosmos.testableContext.orCreateTestableResources.removeOverride(
+                R.bool.can_use_one_handed_bouncer
+            )
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
index d97909a1..e149687 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
@@ -19,25 +19,24 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.app.tracing.coroutines.launchTraced
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
-import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
-import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.keyguard.shared.model.DismissAction
 import com.android.systemui.keyguard.shared.model.KeyguardDone
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.data.repository.fakePowerRepository
-import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.scene.data.repository.Idle
@@ -45,12 +44,12 @@
 import com.android.systemui.scene.data.repository.setSceneTransition
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -76,25 +75,13 @@
         MockitoAnnotations.initMocks(this)
 
         dismissInteractor = kosmos.keyguardDismissInteractor
-        underTest =
-            KeyguardDismissActionInteractor(
-                repository = keyguardRepository,
-                transitionInteractor = kosmos.keyguardTransitionInteractor,
-                dismissInteractor = dismissInteractor,
-                applicationScope = testScope.backgroundScope,
-                deviceUnlockedInteractor = { kosmos.deviceUnlockedInteractor },
-                powerInteractor = kosmos.powerInteractor,
-                alternateBouncerInteractor = kosmos.alternateBouncerInteractor,
-                shadeInteractor = { kosmos.shadeInteractor },
-                keyguardInteractor = { kosmos.keyguardInteractor },
-                sceneInteractor = { kosmos.sceneInteractor },
-            )
+        underTest = kosmos.keyguardDismissActionInteractor
     }
 
     @Test
     fun updateDismissAction_onRepoChange() =
         testScope.runTest {
-            val dismissAction by collectLastValue(underTest.dismissAction)
+            val dismissAction by collectLastValue(keyguardRepository.dismissAction)
 
             val newDismissAction =
                 DismissAction.RunImmediately(
@@ -152,11 +139,16 @@
         }
 
     @Test
-    fun executeDismissAction_dismissKeyguardRequestWithImmediateDismissAction_biometricAuthed() =
+    fun dismissActionExecuted_ImmediateDismissAction_biometricAuthed() =
         testScope.runTest {
-            val executeDismissAction by collectLastValue(underTest.executeDismissAction)
+            val keyguardDoneTiming by collectLastValue(kosmos.keyguardRepository.keyguardDone)
+            var wasDismissActionInvoked = false
+            startInteractor()
 
-            val onDismissAction = { KeyguardDone.IMMEDIATE }
+            val onDismissAction = {
+                wasDismissActionInvoked = true
+                KeyguardDone.IMMEDIATE
+            }
             keyguardRepository.setDismissAction(
                 DismissAction.RunImmediately(
                     onDismissAction = onDismissAction,
@@ -166,16 +158,48 @@
                 )
             )
             kosmos.fakeKeyguardBouncerRepository.setKeyguardAuthenticatedBiometrics(true)
-            assertThat(executeDismissAction).isEqualTo(onDismissAction)
+            runCurrent()
+
+            assertThat(wasDismissActionInvoked).isTrue()
+            assertThat(keyguardDoneTiming).isEqualTo(KeyguardDone.IMMEDIATE)
+            assertThat(keyguardRepository.dismissAction.value).isEqualTo(DismissAction.None)
         }
 
     @Test
-    fun executeDismissAction_dismissKeyguardRequestWithoutImmediateDismissAction() =
+    fun dismissActionExecuted_LaterKeyguardDoneTimingIsStored_biometricAuthed() =
         testScope.runTest {
-            val executeDismissAction by collectLastValue(underTest.executeDismissAction)
+            val keyguardDoneTiming by collectLastValue(kosmos.keyguardRepository.keyguardDone)
+            var wasDismissActionInvoked = false
+            startInteractor()
+
+            val onDismissAction = {
+                wasDismissActionInvoked = true
+                KeyguardDone.LATER
+            }
+            keyguardRepository.setDismissAction(
+                DismissAction.RunImmediately(
+                    onDismissAction = onDismissAction,
+                    onCancelAction = {},
+                    message = "message",
+                    willAnimateOnLockscreen = true,
+                )
+            )
+            kosmos.fakeKeyguardBouncerRepository.setKeyguardAuthenticatedBiometrics(true)
+            runCurrent()
+
+            assertThat(wasDismissActionInvoked).isTrue()
+            assertThat(keyguardDoneTiming).isEqualTo(KeyguardDone.LATER)
+            assertThat(keyguardRepository.dismissAction.value).isEqualTo(DismissAction.None)
+        }
+
+    @Test
+    fun dismissActionExecuted_WithoutImmediateDismissAction() =
+        testScope.runTest {
+            var wasDismissActionInvoked = false
+            startInteractor()
 
             // WHEN a keyguard action will run after the keyguard is gone
-            val onDismissAction = {}
+            val onDismissAction = { wasDismissActionInvoked = true }
             keyguardRepository.setDismissAction(
                 DismissAction.RunAfterKeyguardGone(
                     dismissAction = onDismissAction,
@@ -184,33 +208,39 @@
                     willAnimateOnLockscreen = true,
                 )
             )
-            assertThat(executeDismissAction).isNull()
+            assertThat(wasDismissActionInvoked).isFalse()
 
             kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
                 SuccessFingerprintAuthenticationStatus(0, true)
             )
             kosmos.setSceneTransition(Idle(Scenes.Gone))
             kosmos.sceneInteractor.changeScene(Scenes.Gone, "")
+            runCurrent()
 
-            assertThat(executeDismissAction).isNotNull()
+            assertThat(wasDismissActionInvoked).isTrue()
+            assertThat(keyguardRepository.dismissAction.value).isEqualTo(DismissAction.None)
         }
 
     @Test
     fun resetDismissAction() =
         testScope.runTest {
             kosmos.setSceneTransition(Idle(Scenes.Bouncer))
-            val resetDismissAction by collectLastValue(underTest.resetDismissAction)
+            var wasOnCancelInvoked = false
+            startInteractor()
             keyguardRepository.setDismissAction(
                 DismissAction.RunAfterKeyguardGone(
                     dismissAction = {},
-                    onCancelAction = {},
+                    onCancelAction = { wasOnCancelInvoked = true },
                     message = "message",
                     willAnimateOnLockscreen = true,
                 )
             )
-            assertThat(resetDismissAction).isNull()
+            assertThat(wasOnCancelInvoked).isFalse()
             kosmos.setSceneTransition(Idle(Scenes.Lockscreen))
-            assertThat(resetDismissAction).isEqualTo(Unit)
+            runCurrent()
+
+            assertThat(wasOnCancelInvoked).isTrue()
+            assertThat(keyguardRepository.dismissAction.value).isEqualTo(DismissAction.None)
         }
 
     @Test
@@ -220,21 +250,25 @@
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.None
             )
-            val resetDismissAction by collectLastValue(underTest.resetDismissAction)
-            keyguardRepository.setDismissAction(
+            var wasOnCancelInvoked = false
+
+            val dismissAction =
                 DismissAction.RunAfterKeyguardGone(
                     dismissAction = {},
-                    onCancelAction = {},
+                    onCancelAction = { wasOnCancelInvoked = true },
                     message = "message",
                     willAnimateOnLockscreen = true,
                 )
-            )
-            assertThat(resetDismissAction).isNull()
+            keyguardRepository.setDismissAction(dismissAction)
+            assertThat(wasOnCancelInvoked).isFalse()
 
             kosmos.setSceneTransition(
                 Transition(from = Scenes.Bouncer, to = Scenes.Shade, progress = flowOf(1f))
             )
-            assertThat(resetDismissAction).isNull()
+            runCurrent()
+
+            assertThat(wasOnCancelInvoked).isFalse()
+            assertThat(keyguardRepository.dismissAction.value).isEqualTo(dismissAction)
         }
 
     @Test
@@ -244,29 +278,34 @@
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.None
             )
-            val resetDismissAction by collectLastValue(underTest.resetDismissAction)
+            var wasOnCancelInvoked = false
+            startInteractor()
+
             keyguardRepository.setDismissAction(
                 DismissAction.RunAfterKeyguardGone(
                     dismissAction = {},
-                    onCancelAction = {},
+                    onCancelAction = { wasOnCancelInvoked = true },
                     message = "message",
                     willAnimateOnLockscreen = true,
                 )
             )
-            assertThat(resetDismissAction).isNull()
+            assertThat(wasOnCancelInvoked).isFalse()
             kosmos.fakePowerRepository.updateWakefulness(
                 rawState = WakefulnessState.ASLEEP,
                 lastWakeReason = WakeSleepReason.POWER_BUTTON,
                 lastSleepReason = WakeSleepReason.TIMEOUT,
                 powerButtonLaunchGestureTriggered = false,
             )
-            assertThat(resetDismissAction).isEqualTo(Unit)
+            runCurrent()
+
+            assertThat(wasOnCancelInvoked).isTrue()
+            assertThat(keyguardRepository.dismissAction.value).isEqualTo(DismissAction.None)
         }
 
     @Test
     fun setDismissAction_callsCancelRunnableOnPreviousDismissAction() =
         testScope.runTest {
-            val dismissAction by collectLastValue(underTest.dismissAction)
+            val dismissAction by collectLastValue(keyguardRepository.dismissAction)
             var previousDismissActionCancelCalled = false
             keyguardRepository.setDismissAction(
                 DismissAction.RunImmediately(
@@ -294,27 +333,6 @@
         }
 
     @Test
-    fun handleDismissAction() =
-        testScope.runTest {
-            val dismissAction by collectLastValue(underTest.dismissAction)
-            underTest.handleDismissAction()
-            assertThat(dismissAction).isEqualTo(DismissAction.None)
-        }
-
-    @Test
-    fun setKeyguardDone() =
-        testScope.runTest {
-            val keyguardDoneTiming by collectLastValue(dismissInteractor.keyguardDone)
-            runCurrent()
-
-            underTest.setKeyguardDone(KeyguardDone.LATER)
-            assertThat(keyguardDoneTiming).isEqualTo(KeyguardDone.LATER)
-
-            underTest.setKeyguardDone(KeyguardDone.IMMEDIATE)
-            assertThat(keyguardDoneTiming).isEqualTo(KeyguardDone.IMMEDIATE)
-        }
-
-    @Test
     @EnableSceneContainer
     fun dismissAction_executesBeforeItsReset_sceneContainerOn_swipeAuth_fromQsScene() =
         testScope.runTest {
@@ -324,11 +342,11 @@
                 MutableStateFlow<ObservableTransitionState>(
                     ObservableTransitionState.Idle(currentScene!!)
                 )
+            startInteractor()
+
             kosmos.sceneInteractor.setTransitionState(transitionState)
-            val executeDismissAction by collectLastValue(underTest.executeDismissAction)
-            val resetDismissAction by collectLastValue(underTest.resetDismissAction)
-            assertThat(executeDismissAction).isNull()
-            assertThat(resetDismissAction).isNull()
+            var wasDismissActionInvoked = false
+            var wasCancelActionInvoked = false
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.None
             )
@@ -338,20 +356,23 @@
             transitionState.value = ObservableTransitionState.Idle(Scenes.QuickSettings)
             assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
 
-            assertThat(executeDismissAction).isNull()
-            assertThat(resetDismissAction).isNull()
+            assertThat(wasDismissActionInvoked).isFalse()
+            assertThat(wasCancelActionInvoked).isFalse()
 
             val dismissAction =
                 DismissAction.RunImmediately(
-                    onDismissAction = { KeyguardDone.LATER },
-                    onCancelAction = {},
+                    onDismissAction = {
+                        wasDismissActionInvoked = true
+                        KeyguardDone.LATER
+                    },
+                    onCancelAction = { wasCancelActionInvoked = true },
                     message = "message",
                     willAnimateOnLockscreen = true,
                 )
             underTest.setDismissAction(dismissAction)
-            // Should still be null because the transition to Gone has not yet happened.
-            assertThat(executeDismissAction).isNull()
-            assertThat(resetDismissAction).isNull()
+            // Should still not be run because the transition to Gone has not yet happened.
+            assertThat(wasDismissActionInvoked).isFalse()
+            assertThat(wasCancelActionInvoked).isFalse()
 
             transitionState.value =
                 ObservableTransitionState.Transition.ChangeScene(
@@ -366,8 +387,8 @@
                     isInPreviewStage = flowOf(false),
                 )
             runCurrent()
-            assertThat(executeDismissAction).isNull()
-            assertThat(resetDismissAction).isNull()
+            assertThat(wasDismissActionInvoked).isFalse()
+            assertThat(wasCancelActionInvoked).isFalse()
 
             transitionState.value =
                 ObservableTransitionState.Transition.ChangeScene(
@@ -384,7 +405,17 @@
             kosmos.sceneInteractor.changeScene(Scenes.Gone, "")
             assertThat(currentScene).isEqualTo(Scenes.Gone)
             runCurrent()
-            assertThat(executeDismissAction).isNotNull()
-            assertThat(resetDismissAction).isNull()
+
+            assertThat(wasDismissActionInvoked).isTrue()
+            assertThat(wasCancelActionInvoked).isFalse()
         }
+
+    private fun TestScope.startInteractor() {
+        testScope.backgroundScope.launchTraced(
+            "KeyguardDismissActionInteractorTest#startInteractor"
+        ) {
+            underTest.activate()
+        }
+        runCurrent()
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 21a317a..b2794d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -71,6 +71,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
@@ -82,8 +83,8 @@
 import com.android.systemui.flags.DisableSceneContainer;
 import com.android.systemui.flags.EnableSceneContainer;
 import com.android.systemui.keyguard.DismissCallbackRegistry;
-import com.android.systemui.keyguard.domain.interactor.KeyguardDismissTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardDismissTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.keyguard.shared.model.TransitionState;
@@ -170,6 +171,7 @@
     @Mock private DeviceEntryInteractor mDeviceEntryInteractor;
     @Mock private SceneInteractor mSceneInteractor;
     @Mock private DismissCallbackRegistry mDismissCallbackRegistry;
+    @Mock private BouncerInteractor mBouncerInteractor;
 
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback
@@ -241,7 +243,8 @@
                         mock(StatusBarKeyguardViewManagerInteractor.class),
                         mExecutor,
                         () -> mDeviceEntryInteractor,
-                        mDismissCallbackRegistry) {
+                        mDismissCallbackRegistry,
+                        () -> mBouncerInteractor) {
                     @Override
                     public ViewRootImpl getViewRootImpl() {
                         return mViewRootImpl;
@@ -748,7 +751,8 @@
                         mock(StatusBarKeyguardViewManagerInteractor.class),
                         mExecutor,
                         () -> mDeviceEntryInteractor,
-                        mDismissCallbackRegistry) {
+                        mDismissCallbackRegistry,
+                        () -> mBouncerInteractor) {
                     @Override
                     public ViewRootImpl getViewRootImpl() {
                         return mViewRootImpl;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index beba0f0..1914867 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -172,7 +172,7 @@
         mVolumeController.setDeviceInteractive(false);
         when(mWakefullnessLifcycle.getWakefulness()).thenReturn(
                 WakefulnessLifecycle.WAKEFULNESS_AWAKE);
-        mVolumeController.onVolumeChangedW(0, AudioManager.FLAG_SHOW_UI);
+        mVolumeController.onVolumeChangedW(0, AudioManager.FLAG_SHOW_UI, true);
         verify(mCallback, never()).onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED, false,
                 LOCK_TASK_MODE_NONE);
     }
@@ -182,7 +182,7 @@
         mVolumeController.setDeviceInteractive(true);
         when(mWakefullnessLifcycle.getWakefulness()).thenReturn(
                 WakefulnessLifecycle.WAKEFULNESS_AWAKE);
-        mVolumeController.onVolumeChangedW(0, AudioManager.FLAG_SHOW_UI);
+        mVolumeController.onVolumeChangedW(0, AudioManager.FLAG_SHOW_UI, true);
         verify(mCallback, times(1)).onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED, false,
                 LOCK_TASK_MODE_NONE);
     }
@@ -192,11 +192,11 @@
         mVolumeController.setDeviceInteractive(true);
         when(mWakefullnessLifcycle.getWakefulness()).thenReturn(
                 WakefulnessLifecycle.WAKEFULNESS_AWAKE);
-        mVolumeController.onVolumeChangedW(0, AudioManager.FLAG_SHOW_UI);
+        mVolumeController.onVolumeChangedW(0, AudioManager.FLAG_SHOW_UI, true);
         mVolumeController.setDeviceInteractive(false);
         when(mWakefullnessLifcycle.getWakefulness()).thenReturn(
                 WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP);
-        mVolumeController.onVolumeChangedW(0, AudioManager.FLAG_SHOW_UI);
+        mVolumeController.onVolumeChangedW(0, AudioManager.FLAG_SHOW_UI, true);
         verify(mCallback, times(1)).onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED, false,
                 LOCK_TASK_MODE_NONE);
     }
@@ -210,7 +210,7 @@
                 AudioManager.DEVICE_OUT_BLE_HEADSET);
 
         mVolumeController.onVolumeChangedW(
-                AudioManager.STREAM_VOICE_CALL, AudioManager.FLAG_SHOW_UI);
+                AudioManager.STREAM_VOICE_CALL, AudioManager.FLAG_SHOW_UI, true);
 
         verify(mCallback, times(1)).onStateChanged(any());
     }
@@ -224,7 +224,7 @@
                 AudioManager.DEVICE_OUT_BLUETOOTH_A2DP);
 
         mVolumeController.onVolumeChangedW(
-                AudioManager.STREAM_VOICE_CALL, AudioManager.FLAG_SHOW_UI);
+                AudioManager.STREAM_VOICE_CALL, AudioManager.FLAG_SHOW_UI, true);
 
         verify(mCallback, never()).onStateChanged(any());
     }
@@ -241,14 +241,16 @@
                 .thenReturn(AudioManager.DEVICE_NONE);
 
         mVolumeController.mInAudioSharing = true;
-        mVolumeController.onVolumeChangedW(AudioManager.STREAM_MUSIC, AudioManager.FLAG_SHOW_UI);
+        mVolumeController.onVolumeChangedW(
+                AudioManager.STREAM_MUSIC, AudioManager.FLAG_SHOW_UI, true);
         verify(mCallback).onStateChanged(stateCaptor.capture());
         assertThat(stateCaptor.getValue().states.contains(AudioManager.STREAM_MUSIC)).isTrue();
         assertThat(stateCaptor.getValue().states.get(AudioManager.STREAM_MUSIC).routedToBluetooth)
                 .isTrue();
 
         mVolumeController.mInAudioSharing = false;
-        mVolumeController.onVolumeChangedW(AudioManager.STREAM_MUSIC, AudioManager.FLAG_SHOW_UI);
+        mVolumeController.onVolumeChangedW(
+                AudioManager.STREAM_MUSIC, AudioManager.FLAG_SHOW_UI, true);
         verify(mCallback, times(2)).onStateChanged(stateCaptor.capture());
         assertThat(stateCaptor.getValue().states.contains(AudioManager.STREAM_MUSIC)).isTrue();
         assertThat(stateCaptor.getValue().states.get(AudioManager.STREAM_MUSIC).routedToBluetooth)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTestKt.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTestKt.kt
index 98cea9d..76b7b8f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTestKt.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTestKt.kt
@@ -18,12 +18,15 @@
 
 import android.app.activityManager
 import android.app.keyguardManager
+import android.content.Intent
 import android.content.applicationContext
 import android.content.packageManager
+import android.content.testableContext
 import android.media.AudioManager
 import android.media.IVolumeController
 import android.os.Handler
 import android.os.looper
+import android.os.testableLooper
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
@@ -34,6 +37,8 @@
 import com.android.settingslib.volume.data.model.VolumeControllerEvent
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.broadcast.broadcastDispatcherContext
 import com.android.systemui.dump.dumpManager
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.keyguard.wakefulnessLifecycle
@@ -81,10 +86,11 @@
             audioRepository.init()
             threadFactory =
                 FakeThreadFactory(FakeExecutor(fakeSystemClock)).apply { setLooper(looper) }
+            broadcastDispatcherContext = testableContext
             underTest =
                 VolumeDialogControllerImpl(
                         applicationContext,
-                        mock {},
+                        broadcastDispatcher,
                         mock {
                             on { ringerMode }.thenReturn(mock<RingerModeLiveData> {})
                             on { ringerModeInternal }.thenReturn(mock<RingerModeLiveData> {})
@@ -112,6 +118,23 @@
         }
 
     @Test
+    fun broadcastEvent_sendsChangesOnce() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(audioManager.getLastAudibleStreamVolume(any())).thenReturn(1)
+                broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                    applicationContext,
+                    Intent(AudioManager.ACTION_VOLUME_CHANGED).apply {
+                        putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, AudioManager.STREAM_SYSTEM)
+                    },
+                )
+                testableLooper.processAllMessages()
+
+                verify(callbacks) { 1 * { onStateChanged(any()) } }
+            }
+        }
+
+    @Test
     @EnableFlags(Flags.FLAG_USE_VOLUME_CONTROLLER)
     fun useVolumeControllerEnabled_listensToVolumeController() =
         testVolumeController { stream: Int, flags: Int ->
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/BouncerInputSide.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/BouncerInputSide.kt
new file mode 100644
index 0000000..e52898d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/BouncerInputSide.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.authentication.shared.model
+
+import android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT
+import android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT
+import com.android.systemui.authentication.shared.model.BouncerInputSide.LEFT
+import com.android.systemui.authentication.shared.model.BouncerInputSide.RIGHT
+
+/** Denotes which side of the bouncer the input area appears, applicable to large screen devices. */
+enum class BouncerInputSide(val settingValue: Int) {
+    LEFT(ONE_HANDED_KEYGUARD_SIDE_LEFT),
+    RIGHT(ONE_HANDED_KEYGUARD_SIDE_RIGHT),
+}
+
+/** Map the setting value to [BouncerInputSide] enum. */
+fun Int.toBouncerInputSide(): BouncerInputSide? {
+    return when (this) {
+        ONE_HANDED_KEYGUARD_SIDE_LEFT -> LEFT
+        ONE_HANDED_KEYGUARD_SIDE_RIGHT -> RIGHT
+        else -> null
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
index 94e0854..f424de9 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
@@ -16,19 +16,66 @@
 
 package com.android.systemui.bouncer.data.repository
 
+import android.content.Context
+import android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE
+import com.android.systemui.authentication.shared.model.BouncerInputSide
+import com.android.systemui.authentication.shared.model.toBouncerInputSide
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
+import com.android.systemui.res.R
+import com.android.systemui.util.settings.GlobalSettings
 import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
 
 /** Provides access to bouncer-related application state. */
 @SysUISingleton
 class BouncerRepository
 @Inject
 constructor(
+    @Application private val applicationContext: Context,
+    private val globalSettings: GlobalSettings,
     private val flags: FeatureFlagsClassic,
 ) {
+    val scale: MutableStateFlow<Float> = MutableStateFlow(1.0f)
+
     /** Whether the user switcher should be displayed within the bouncer UI on large screens. */
-    val isUserSwitcherVisible: Boolean
-        get() = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)
+    val isUserSwitcherEnabledInConfig: Boolean
+        get() =
+            applicationContext.resources.getBoolean(R.bool.config_enableBouncerUserSwitcher) &&
+                flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)
+
+    /** Whether the one handed bouncer is supported for this device. */
+    val isOneHandedBouncerSupportedInConfig: Boolean
+        get() = applicationContext.resources.getBoolean(R.bool.can_use_one_handed_bouncer)
+
+    /**
+     * Preferred side of the screen where the input area on the bouncer should be. This is
+     * applicable for large screen devices (foldables and tablets).
+     */
+    val preferredBouncerInputSide: MutableStateFlow<BouncerInputSide?> =
+        MutableStateFlow(getPreferredInputSideSetting())
+
+    /** X coordinate of the last recorded touch position on the lockscreen. */
+    val lastRecordedLockscreenTouchPosition = MutableStateFlow<Float?>(null)
+
+    /** Save the preferred bouncer input side. */
+    fun setPreferredBouncerInputSide(inputSide: BouncerInputSide) {
+        globalSettings.putInt(ONE_HANDED_KEYGUARD_SIDE, inputSide.settingValue)
+        // used to only trigger another emission on the flow.
+        preferredBouncerInputSide.value = inputSide
+    }
+
+    /**
+     * Record the x coordinate of the last touch position on the lockscreen. This will be used to
+     * determine which side of the bouncer the input area should be shown.
+     */
+    fun recordLockscreenTouchPosition(x: Float) {
+        lastRecordedLockscreenTouchPosition.value = x
+    }
+
+    fun getPreferredInputSideSetting(): BouncerInputSide? {
+        return globalSettings.getInt(ONE_HANDED_KEYGUARD_SIDE, -1).toBouncerInputSide()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 92fcf39..e178c09 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -25,16 +25,19 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim
+import com.android.systemui.authentication.shared.model.BouncerInputSide
 import com.android.systemui.bouncer.data.repository.BouncerRepository
 import com.android.systemui.bouncer.shared.logging.BouncerUiEvent
 import com.android.systemui.classifier.FalsingClassifier
 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
 import com.android.systemui.log.SessionTracker
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.scene.domain.interactor.SceneBackInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -43,6 +46,8 @@
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
 
@@ -60,6 +65,7 @@
     private val uiEventLogger: UiEventLogger,
     private val sessionTracker: SessionTracker,
     sceneBackInteractor: SceneBackInteractor,
+    private val configurationInteractor: ConfigurationInteractor,
 ) {
     private val _onIncorrectBouncerInput = MutableSharedFlow<Unit>()
     val onIncorrectBouncerInput: SharedFlow<Unit> = _onIncorrectBouncerInput
@@ -78,8 +84,47 @@
         authenticationInteractor.isPinEnhancedPrivacyEnabled
 
     /** Whether the user switcher should be displayed within the bouncer UI on large screens. */
-    val isUserSwitcherVisible: Boolean
-        get() = repository.isUserSwitcherVisible
+    val isUserSwitcherVisible: Flow<Boolean> =
+        authenticationInteractor.authenticationMethod.map { authMethod ->
+            when (authMethod) {
+                Sim -> false
+                else -> repository.isUserSwitcherEnabledInConfig
+            }
+        }
+
+    /**
+     * Whether one handed bouncer mode is supported on large screen devices. This allows user to
+     * double tap on the half of the screen to bring the bouncer input to that side of the screen.
+     */
+    val isOneHandedModeSupported: Flow<Boolean> =
+        combine(
+            isUserSwitcherVisible,
+            authenticationInteractor.authenticationMethod,
+            configurationInteractor.onAnyConfigurationChange,
+        ) { userSwitcherVisible, authMethod, _ ->
+            userSwitcherVisible ||
+                (repository.isOneHandedBouncerSupportedInConfig && (authMethod !is Password))
+        }
+
+    /**
+     * Preferred side of the screen where the input area on the bouncer should be. This is
+     * applicable for large screen devices (foldables and tablets).
+     */
+    val preferredBouncerInputSide: Flow<BouncerInputSide?> =
+        combine(
+            configurationInteractor.onAnyConfigurationChange,
+            repository.preferredBouncerInputSide,
+        ) { _, _ ->
+            // always read the setting as that can change outside of this
+            // repository (tests/manual testing)
+            val preferredInputSide = repository.getPreferredInputSideSetting()
+            when {
+                preferredInputSide != null -> preferredInputSide
+                repository.isUserSwitcherEnabledInConfig -> BouncerInputSide.RIGHT
+                repository.isOneHandedBouncerSupportedInConfig -> BouncerInputSide.LEFT
+                else -> null
+            }
+        }
 
     private val _onImeHiddenByUser = MutableSharedFlow<Unit>()
     /** Emits a [Unit] each time the IME (keyboard) is hidden by the user. */
@@ -93,6 +138,12 @@
             }
             .map {}
 
+    /** X coordinate of the last recorded touch position on the lockscreen. */
+    val lastRecordedLockscreenTouchPosition = repository.lastRecordedLockscreenTouchPosition
+
+    /** Value between 0-1 that specifies by how much the bouncer UI should be scaled down. */
+    val scale: StateFlow<Float> = repository.scale.asStateFlow()
+
     /** The scene to show when bouncer is dismissed. */
     val dismissDestination: Flow<SceneKey> =
         sceneBackInteractor.backScene
@@ -129,6 +180,37 @@
         )
     }
 
+    /** Update the preferred input side for the bouncer. */
+    fun setPreferredBouncerInputSide(inputSide: BouncerInputSide) {
+        repository.setPreferredBouncerInputSide(inputSide)
+    }
+
+    /**
+     * Record the x coordinate of the last touch position on the lockscreen. This will be used to
+     * determine which side of the bouncer the input area should be shown.
+     */
+    fun recordKeyguardTouchPosition(x: Float) {
+        // todo (b/375245685) investigate why this is not working as expected when it is
+        //  wired up with SBKVM
+        repository.recordLockscreenTouchPosition(x)
+    }
+
+    fun onBackEventProgressed(progress: Float) {
+        // this is applicable only for compose bouncer without flexiglass
+        SceneContainerFlag.assertInLegacyMode()
+        repository.scale.value = (mapBackEventProgressToScale(progress))
+    }
+
+    fun onBackEventCancelled() {
+        // this is applicable only for compose bouncer without flexiglass
+        SceneContainerFlag.assertInLegacyMode()
+        repository.scale.value = DEFAULT_SCALE
+    }
+
+    fun resetScale() {
+        repository.scale.value = DEFAULT_SCALE
+    }
+
     /**
      * Attempts to authenticate based on the given user input.
      *
@@ -180,7 +262,7 @@
             } else if (authResult == AuthenticationResult.FAILED) {
                 uiEventLogger.log(
                     BouncerUiEvent.BOUNCER_PASSWORD_FAILURE,
-                    sessionTracker.getSessionId(SESSION_KEYGUARD)
+                    sessionTracker.getSessionId(SESSION_KEYGUARD),
                 )
             }
         }
@@ -192,4 +274,15 @@
     suspend fun onImeHiddenByUser() {
         _onImeHiddenByUser.emit(Unit)
     }
+
+    private fun mapBackEventProgressToScale(progress: Float): Float {
+        // TODO(b/263819310): Update the interpolator to match spec.
+        return MIN_BACK_SCALE + (1 - MIN_BACK_SCALE) * (1 - progress)
+    }
+
+    companion object {
+        // How much the view scales down to during back gestures.
+        private const val MIN_BACK_SCALE: Float = 0.9f
+        private const val DEFAULT_SCALE: Float = 1.0f
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt
index 7f97718..554dd69 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt
@@ -45,7 +45,7 @@
 fun calculateLayoutInternal(
     width: SizeClass,
     height: SizeClass,
-    isSideBySideSupported: Boolean,
+    isOneHandedModeSupported: Boolean,
 ): BouncerSceneLayout {
     return when (height) {
         SizeClass.COMPACT -> BouncerSceneLayout.SPLIT_BOUNCER
@@ -61,6 +61,6 @@
                 SizeClass.MEDIUM -> BouncerSceneLayout.BELOW_USER_SWITCHER
                 SizeClass.EXPANDED -> BouncerSceneLayout.BESIDE_USER_SWITCHER
             }
-    }.takeIf { it != BouncerSceneLayout.BESIDE_USER_SWITCHER || isSideBySideSupported }
+    }.takeIf { it != BouncerSceneLayout.BESIDE_USER_SWITCHER || isOneHandedModeSupported }
         ?: BouncerSceneLayout.STANDARD_BOUNCER
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
index cc79311..47d91374 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationWipeModel
+import com.android.systemui.authentication.shared.model.BouncerInputSide
 import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
@@ -77,8 +78,8 @@
     val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> =
         _userSwitcherDropdown.asStateFlow()
 
-    val isUserSwitcherVisible: Boolean
-        get() = bouncerInteractor.isUserSwitcherVisible
+    private val _isUserSwitcherVisible = MutableStateFlow(false)
+    val isUserSwitcherVisible: StateFlow<Boolean> = _isUserSwitcherVisible.asStateFlow()
 
     /** View-model for the current UI, based on the current authentication method. */
     private val _authMethodViewModel = MutableStateFlow<AuthMethodBouncerViewModel?>(null)
@@ -118,17 +119,19 @@
      */
     val actionButton: StateFlow<BouncerActionButtonModel?> = _actionButton.asStateFlow()
 
-    private val _isSideBySideSupported =
-        MutableStateFlow(isSideBySideSupported(authMethodViewModel.value))
+    private val _isOneHandedModeSupported = MutableStateFlow(false)
     /**
-     * Whether the "side-by-side" layout is supported.
+     * Whether the one-handed mode is supported.
      *
      * When presented on its own, without a user switcher (e.g. not on communal devices like
      * tablets, for example), some authentication method UIs don't do well if they're shown in the
      * side-by-side layout; these need to be shown with the standard layout so they can take up as
      * much width as possible.
      */
-    val isSideBySideSupported: StateFlow<Boolean> = _isSideBySideSupported.asStateFlow()
+    val isOneHandedModeSupported: StateFlow<Boolean> = _isOneHandedModeSupported.asStateFlow()
+
+    private val _isInputPreferredOnLeftSide = MutableStateFlow(false)
+    val isInputPreferredOnLeftSide = _isInputPreferredOnLeftSide.asStateFlow()
 
     private val _isFoldSplitRequired =
         MutableStateFlow(isFoldSplitRequired(authMethodViewModel.value))
@@ -138,11 +141,15 @@
      */
     val isFoldSplitRequired: StateFlow<Boolean> = _isFoldSplitRequired.asStateFlow()
 
+    /** How much the bouncer UI should be scaled. */
+    val scale: StateFlow<Float> = bouncerInteractor.scale
+
     private val _isInputEnabled =
         MutableStateFlow(authenticationInteractor.lockoutEndTimestamp == null)
     private val isInputEnabled: StateFlow<Boolean> = _isInputEnabled.asStateFlow()
 
     override suspend fun onActivated(): Nothing {
+        bouncerInteractor.resetScale()
         coroutineScope {
             launch { message.activate() }
             launch {
@@ -200,9 +207,41 @@
             launch { actionButtonInteractor.actionButton.collect { _actionButton.value = it } }
 
             launch {
-                authMethodViewModel
-                    .map { authMethod -> isSideBySideSupported(authMethod) }
-                    .collect { _isSideBySideSupported.value = it }
+                combine(
+                        bouncerInteractor.isOneHandedModeSupported,
+                        bouncerInteractor.lastRecordedLockscreenTouchPosition,
+                        ::Pair,
+                    )
+                    .collect { (isOneHandedModeSupported, lastRecordedNotificationTouchPosition) ->
+                        _isOneHandedModeSupported.value = isOneHandedModeSupported
+                        if (
+                            isOneHandedModeSupported &&
+                                lastRecordedNotificationTouchPosition != null
+                        ) {
+                            bouncerInteractor.setPreferredBouncerInputSide(
+                                if (
+                                    lastRecordedNotificationTouchPosition <
+                                        applicationContext.resources.displayMetrics.widthPixels / 2
+                                ) {
+                                    BouncerInputSide.LEFT
+                                } else {
+                                    BouncerInputSide.RIGHT
+                                }
+                            )
+                        }
+                    }
+            }
+
+            launch {
+                bouncerInteractor.isUserSwitcherVisible.collect {
+                    _isUserSwitcherVisible.value = it
+                }
+            }
+
+            launch {
+                bouncerInteractor.preferredBouncerInputSide.collect {
+                    _isInputPreferredOnLeftSide.value = it == BouncerInputSide.LEFT
+                }
             }
 
             launch {
@@ -221,10 +260,6 @@
         }
     }
 
-    private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean {
-        return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel
-    }
-
     private fun isFoldSplitRequired(authMethod: AuthMethodBouncerViewModel?): Boolean {
         return authMethod !is PasswordBouncerViewModel
     }
@@ -335,6 +370,29 @@
     }
 
     /**
+     * Notifies that double tap gesture was detected on the bouncer.
+     * [wasEventOnNonInputHalfOfScreen] is true when it happens on the side of the bouncer where the
+     * input UI is not present.
+     */
+    fun onDoubleTap(wasEventOnNonInputHalfOfScreen: Boolean) {
+        if (!wasEventOnNonInputHalfOfScreen) return
+        if (_isInputPreferredOnLeftSide.value) {
+            bouncerInteractor.setPreferredBouncerInputSide(BouncerInputSide.RIGHT)
+        } else {
+            bouncerInteractor.setPreferredBouncerInputSide(BouncerInputSide.LEFT)
+        }
+    }
+
+    /**
+     * Notifies that onDown was detected on the bouncer. [wasEventOnNonInputHalfOfScreen] is true
+     * when it happens on the side of the bouncer where the input UI is not present.
+     */
+    fun onDown(wasEventOnNonInputHalfOfScreen: Boolean) {
+        if (!wasEventOnNonInputHalfOfScreen) return
+        bouncerInteractor.onDown()
+    }
+
+    /**
      * Notifies that a key event has occurred.
      *
      * @return `true` when the [KeyEvent] was consumed as user input on bouncer; `false` otherwise.
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
index 6baffdd..2a3729b 100644
--- a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
@@ -34,6 +34,8 @@
 import com.android.systemui.education.ui.viewmodel.ContextualEduToastViewModel
 import com.android.systemui.education.ui.viewmodel.ContextualEduViewModel
 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_CONTEXTUAL_EDU
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_KEY
 import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -99,7 +101,7 @@
                 CHANNEL_ID,
                 context.getString(com.android.internal.R.string.android_system_label),
                 // Make it as silent notification
-                NotificationManager.IMPORTANCE_LOW
+                NotificationManager.IMPORTANCE_LOW,
             )
         notificationManager.createNotificationChannel(channel)
     }
@@ -114,7 +116,7 @@
         val extras = Bundle()
         extras.putString(
             Notification.EXTRA_SUBSTITUTE_APP_NAME,
-            context.getString(com.android.internal.R.string.android_system_label)
+            context.getString(com.android.internal.R.string.android_system_label),
         )
 
         val notification =
@@ -131,7 +133,7 @@
             TAG,
             NOTIFICATION_ID,
             notification,
-            UserHandle.of(model.userId)
+            UserHandle.of(model.userId),
         )
     }
 
@@ -140,12 +142,16 @@
             Intent(context, KeyboardTouchpadTutorialActivity::class.java).apply {
                 addCategory(Intent.CATEGORY_DEFAULT)
                 flags = Intent.FLAG_ACTIVITY_NEW_TASK
+                putExtra(
+                    INTENT_TUTORIAL_ENTRY_POINT_KEY,
+                    INTENT_TUTORIAL_ENTRY_POINT_CONTEXTUAL_EDU,
+                )
             }
         return PendingIntent.getActivity(
             context,
             /* requestCode= */ 0,
             intent,
-            PendingIntent.FLAG_IMMUTABLE
+            PendingIntent.FLAG_IMMUTABLE,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
index 258232b..21090c1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
@@ -17,7 +17,9 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import com.android.keyguard.logging.KeyguardLogger
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -27,28 +29,31 @@
 import com.android.systemui.keyguard.shared.model.KeyguardDone
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.log.core.LogLevel
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sampleFilter
-import com.android.systemui.util.kotlin.sample
 import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterNot
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 /** Encapsulates business-logic for actions to run when the keyguard is dismissed. */
 @ExperimentalCoroutinesApi
@@ -66,10 +71,10 @@
     shadeInteractor: Lazy<ShadeInteractor>,
     keyguardInteractor: Lazy<KeyguardInteractor>,
     sceneInteractor: Lazy<SceneInteractor>,
-) {
-    val dismissAction: Flow<DismissAction> = repository.dismissAction
-
-    val onCancel: Flow<Runnable> = dismissAction.map { it.onCancelAction }
+    private val keyguardLogger: KeyguardLogger,
+    private val primaryBouncerInteractor: PrimaryBouncerInteractor,
+) : ExclusiveActivatable() {
+    private val dismissAction: Flow<DismissAction> = repository.dismissAction
 
     // TODO (b/268240415): use message in alt + primary bouncer message
     // message to show to the user about the dismiss action, else empty string
@@ -90,10 +95,24 @@
             )
 
     private val finishedTransitionToGone: Flow<Unit> =
-        transitionInteractor
-            .isFinishedIn(scene = Scenes.Gone, stateWithoutSceneContainer = GONE)
-            .filter { it }
-            .map {}
+        if (SceneContainerFlag.isEnabled) {
+            // Using sceneInteractor instead of transitionInteractor because of a race
+            // condition that forms between transitionInteractor (transitionState) and
+            // isOnShadeWhileUnlocked where the latter emits false before the former emits
+            // true, causing the merge to not emit until it's too late.
+            sceneInteractor
+                .get()
+                .currentScene
+                .map { it == Scenes.Gone }
+                .distinctUntilChanged()
+                .filter { it }
+                .map {}
+        } else {
+            transitionInteractor
+                .isFinishedIn(scene = Scenes.Gone, stateWithoutSceneContainer = GONE)
+                .filter { it }
+                .map {}
+        }
 
     /**
      * True if the any variation of the notification shade or quick settings is showing AND the
@@ -125,30 +144,8 @@
             }
         }
 
-    val executeDismissAction: Flow<() -> KeyguardDone> =
-        merge(
-                if (SceneContainerFlag.isEnabled) {
-                    // Using currentScene instead of finishedTransitionToGone because of a race
-                    // condition that forms between finishedTransitionToGone and
-                    // isOnShadeWhileUnlocked where the latter emits false before the former emits
-                    // true, causing the merge to not emit until it's too late.
-                    sceneInteractor
-                        .get()
-                        .currentScene
-                        .map { it == Scenes.Gone }
-                        .distinctUntilChanged()
-                        .filter { it }
-                } else {
-                    finishedTransitionToGone
-                },
-                isOnShadeWhileUnlocked.filter { it }.map {},
-                dismissInteractor.dismissKeyguardRequestWithImmediateDismissAction,
-            )
-            .sample(dismissAction)
-            .filterNot { it is DismissAction.None }
-            .map { it.onDismissAction }
-
-    val resetDismissAction: Flow<Unit> =
+    /** Flow that emits whenever we need to reset the dismiss action */
+    private val resetDismissAction: Flow<Unit> =
         combine(
                 if (SceneContainerFlag.isEnabled) {
                     // Using currentScene instead of isFinishedIn because of a race condition that
@@ -205,13 +202,62 @@
         repository.setDismissAction(dismissAction)
     }
 
-    fun handleDismissAction() {
-        if (ComposeBouncerFlags.isUnexpectedlyInLegacyMode()) return
+    /** Launch any relevant coroutines that are required by this interactor. */
+    override suspend fun onActivated(): Nothing {
+        coroutineScope {
+            launch {
+                merge(finishedTransitionToGone, isOnShadeWhileUnlocked.filter { it }.map {})
+                    .collect {
+                        log("finishedTransitionToGone")
+                        runDismissAction()
+                    }
+            }
+
+            launch {
+                dismissInteractor.dismissKeyguardRequestWithImmediateDismissAction.collect {
+                    log("eventsThatRequireKeyguardDismissal")
+                    runDismissAction()
+                }
+            }
+
+            launch {
+                resetDismissAction.collect {
+                    log("resetDismissAction")
+                    repository.dismissAction.value.onCancelAction.run()
+                    clearDismissAction()
+                }
+            }
+
+            launch { repository.dismissAction.collect { log("updatedDismissAction=$it") } }
+            awaitCancellation()
+        }
+    }
+
+    /** Run the dismiss action and starts the dismiss keyguard transition. */
+    private suspend fun runDismissAction() {
+        val dismissAction = repository.dismissAction.value
+        var keyguardDoneTiming: KeyguardDone = KeyguardDone.IMMEDIATE
+        if (dismissAction != DismissAction.None) {
+            keyguardDoneTiming = dismissAction.onDismissAction.invoke()
+            dismissInteractor.setKeyguardDone(keyguardDoneTiming)
+            clearDismissAction()
+        }
+        if (!SceneContainerFlag.isEnabled) {
+            // This is required to reset some state flows in the repository which ideally should be
+            // sharedFlows but are not due to performance concerns.
+            primaryBouncerInteractor.notifyKeyguardAuthenticatedHandled()
+        }
+    }
+
+    private fun clearDismissAction() {
         repository.setDismissAction(DismissAction.None)
     }
 
-    suspend fun setKeyguardDone(keyguardDoneTiming: KeyguardDone) {
-        if (ComposeBouncerFlags.isUnexpectedlyInLegacyMode()) return
-        dismissInteractor.setKeyguardDone(keyguardDoneTiming)
+    private fun log(message: String) {
+        keyguardLogger.log(TAG, LogLevel.DEBUG, message)
+    }
+
+    companion object {
+        private const val TAG = "KeyguardDismissAction"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardDismissActionBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardDismissActionBinder.kt
index 87befc0..f1a316c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardDismissActionBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardDismissActionBinder.kt
@@ -15,14 +15,11 @@
  */
 package com.android.systemui.keyguard.ui.binder
 
-import com.android.keyguard.logging.KeyguardLogger
 import com.android.systemui.CoreStartable
 import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInteractor
-import com.android.systemui.log.core.LogLevel
-import com.android.systemui.util.kotlin.sample
 import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -37,7 +34,6 @@
 constructor(
     private val interactorLazy: Lazy<KeyguardDismissActionInteractor>,
     @Application private val scope: CoroutineScope,
-    private val keyguardLogger: KeyguardLogger,
 ) : CoreStartable {
 
     override fun start() {
@@ -45,31 +41,6 @@
             return
         }
 
-        val interactor = interactorLazy.get()
-        scope.launch {
-            interactor.executeDismissAction.collect {
-                log("executeDismissAction")
-                interactor.setKeyguardDone(it())
-                interactor.handleDismissAction()
-            }
-        }
-
-        scope.launch {
-            interactor.resetDismissAction.sample(interactor.onCancel).collect {
-                log("resetDismissAction")
-                it.run()
-                interactor.handleDismissAction()
-            }
-        }
-
-        scope.launch { interactor.dismissAction.collect { log("updatedDismissAction=$it") } }
-    }
-
-    private fun log(message: String) {
-        keyguardLogger.log(TAG, LogLevel.DEBUG, message)
-    }
-
-    companion object {
-        private const val TAG = "KeyguardDismissAction"
+        scope.launch { interactorLazy.get().activate() }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt
index 55d7b1d..2bdee67 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt
@@ -42,7 +42,7 @@
     context: Context,
     newController: MediaController,
     new: MediaData,
-    old: MediaData?
+    old: MediaData?,
 ): Boolean {
     if (old == null || !mediaControlsPostsOptimization()) return false
 
@@ -71,7 +71,7 @@
 /** Returns whether actions lists are equal. */
 fun areCustomActionListsEqual(
     first: List<PlaybackState.CustomAction>?,
-    second: List<PlaybackState.CustomAction>?
+    second: List<PlaybackState.CustomAction>?,
 ): Boolean {
     // Same object, or both null
     if (first === second) {
@@ -94,7 +94,7 @@
 
 private fun areCustomActionsEqual(
     firstAction: PlaybackState.CustomAction,
-    secondAction: PlaybackState.CustomAction
+    secondAction: PlaybackState.CustomAction,
 ): Boolean {
     if (
         firstAction.action != secondAction.action ||
@@ -139,7 +139,7 @@
     context: Context,
     newController: MediaController,
     new: MediaData,
-    old: MediaData
+    old: MediaData,
 ): Boolean {
     val oldState = MediaController(context, old.token!!).playbackState
     return if (
@@ -150,8 +150,7 @@
         var same = true
         new.actions.asSequence().zip(old.actions.asSequence()).forEach {
             if (
-                it.first.actionIntent?.intent?.filterEquals(it.second.actionIntent?.intent) !=
-                    true ||
+                it.first.actionIntent?.intent != it.second.actionIntent?.intent ||
                     it.first.icon != it.second.icon ||
                     it.first.contentDescription != it.second.contentDescription
             ) {
@@ -164,7 +163,7 @@
         oldState?.actions == newController.playbackState?.actions &&
             areCustomActionListsEqual(
                 oldState?.customActions,
-                newController.playbackState?.customActions
+                newController.playbackState?.customActions,
             )
     } else {
         false
@@ -172,8 +171,5 @@
 }
 
 private fun areClickIntentsEqual(newIntent: PendingIntent?, oldIntent: PendingIntent?): Boolean {
-    if ((newIntent == null && oldIntent == null) || newIntent === oldIntent) return true
-    if (newIntent == null || oldIntent == null) return false
-
-    return newIntent.intent?.filterEquals(oldIntent.intent) == true
+    return newIntent == oldIntent
 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
index c70a523..96c0cac 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
@@ -25,12 +25,10 @@
 import static android.app.StatusBarManager.WindowVisibleState;
 import static android.app.StatusBarManager.windowStateToString;
 import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
-import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR;
 import static android.view.InsetsSource.FLAG_SUPPRESS_SCRIM;
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
-import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
 
@@ -1836,11 +1834,6 @@
     private InsetsFrameProvider[] getInsetsFrameProvider(int insetsHeight, Context userContext) {
         final InsetsFrameProvider navBarProvider =
                 new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.navigationBars());
-        if (!ENABLE_HIDE_IME_CAPTION_BAR) {
-            navBarProvider.setInsetsSizeOverrides(new InsetsFrameProvider.InsetsSizeOverride[] {
-                    new InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, null)
-            });
-        }
         if (insetsHeight != -1 && !mEdgeBackGestureHandler.isButtonForcedVisible()) {
             navBarProvider.setInsetsSize(Insets.of(0, 0, 0, insetsHeight));
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 92b609e..9cda199 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -59,6 +59,7 @@
 import com.android.systemui.animation.back.FlingOnBackAnimationCallback;
 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
@@ -165,6 +166,7 @@
     private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
     private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     private final AlternateBouncerInteractor mAlternateBouncerInteractor;
+    private final Lazy<BouncerInteractor> mBouncerInteractor;
     private final BouncerView mPrimaryBouncerView;
     private final Lazy<ShadeController> mShadeController;
     private final Lazy<SceneInteractor> mSceneInteractorLazy;
@@ -252,6 +254,9 @@
 
         @Override
         public void onBackProgressedCompat(@NonNull BackEvent event) {
+            if (ComposeBouncerFlags.INSTANCE.isOnlyComposeBouncerEnabled()) {
+                mBouncerInteractor.get().onBackEventProgressed(event.getProgress());
+            }
             if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) {
                 mPrimaryBouncerView.getDelegate().getBackCallback().onBackProgressed(event);
             }
@@ -259,6 +264,9 @@
 
         @Override
         public void onBackCancelledCompat() {
+            if (ComposeBouncerFlags.INSTANCE.isOnlyComposeBouncerEnabled()) {
+                mBouncerInteractor.get().onBackEventCancelled();
+            }
             if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) {
                 mPrimaryBouncerView.getDelegate().getBackCallback().onBackCancelled();
             }
@@ -400,7 +408,8 @@
             StatusBarKeyguardViewManagerInteractor statusBarKeyguardViewManagerInteractor,
             @Main DelayableExecutor executor,
             Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy,
-            DismissCallbackRegistry dismissCallbackRegistry
+            DismissCallbackRegistry dismissCallbackRegistry,
+            Lazy<BouncerInteractor> bouncerInteractor
     ) {
         mContext = context;
         mExecutor = executor;
@@ -424,6 +433,7 @@
         mFoldAodAnimationController = sysUIUnfoldComponent
                 .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
         mAlternateBouncerInteractor = alternateBouncerInteractor;
+        mBouncerInteractor = bouncerInteractor;
         mIsBackAnimationEnabled = predictiveBackAnimateBouncer();
         mUdfpsOverlayInteractor = udfpsOverlayInteractor;
         mActivityStarter = activityStarter;
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index bbd8f3dc..b705872 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -552,7 +552,7 @@
                 && mShowVolumeDialog;
     }
 
-    boolean onVolumeChangedW(int stream, int flags) {
+    boolean onVolumeChangedW(int stream, int flags, boolean sendChanges) {
         final boolean showUI = shouldShowUI(flags);
         final boolean fromKey = (flags & AudioManager.FLAG_FROM_KEY) != 0;
         final boolean showVibrateHint = (flags & AudioManager.FLAG_SHOW_VIBRATE_HINT) != 0;
@@ -564,7 +564,7 @@
         int lastAudibleStreamVolume = getAudioManagerStreamVolume(stream);
         changed |= updateStreamLevelW(stream, lastAudibleStreamVolume);
         changed |= checkRoutedToBluetoothW(showUI ? AudioManager.STREAM_MUSIC : stream);
-        if (changed) {
+        if (changed && sendChanges) {
             mCallbacks.onStateChanged(mState);
         }
         if (showUI) {
@@ -950,7 +950,7 @@
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case VOLUME_CHANGED: onVolumeChangedW(msg.arg1, msg.arg2); break;
+                case VOLUME_CHANGED: onVolumeChangedW(msg.arg1, msg.arg2, true); break;
                 case DISMISS_REQUESTED: onDismissRequestedW(msg.arg1); break;
                 case GET_STATE: onGetStateW(); break;
                 case SET_RINGER_MODE: onSetRingerModeW(msg.arg1, msg.arg2 != 0); break;
@@ -1307,7 +1307,7 @@
                 if (D.BUG) Log.d(TAG, "onReceive VOLUME_CHANGED_ACTION stream=" + stream
                         + " oldLevel=" + oldLevel);
                 if (stream != STREAM_UNKNOWN) {
-                    changed |= onVolumeChangedW(stream, 0);
+                    changed |= onVolumeChangedW(stream, 0, false);
                 }
             } else if (action.equals(AudioManager.STREAM_DEVICES_CHANGED_ACTION)) {
                 final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
@@ -1320,7 +1320,7 @@
                         + stream + " devices=" + devices + " oldDevices=" + oldDevices);
                 if (stream != STREAM_UNKNOWN) {
                     changed |= checkRoutedToBluetoothW(stream);
-                    changed |= onVolumeChangedW(stream, 0);
+                    changed |= onVolumeChangedW(stream, 0, false);
                 }
             } else if (action.equals(AudioManager.STREAM_MUTE_CHANGED_ACTION)) {
                 final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
index 6e36d42b..9ace8e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.bouncer.ui.composable
 
 import android.app.AlertDialog
+import android.content.testableContext
 import android.platform.test.annotations.MotionTest
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
@@ -38,8 +39,10 @@
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.motion.createSysUiComposeMotionTestRule
+import com.android.systemui.res.R
 import com.android.systemui.scene.domain.startable.sceneContainerStartable
 import com.android.systemui.testKosmos
+import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -76,6 +79,17 @@
         kosmos.sceneContainerStartable.start()
         kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
         kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+        kosmos.testableContext.orCreateTestableResources.addOverride(
+            R.bool.config_enableBouncerUserSwitcher,
+            true,
+        )
+    }
+
+    @After
+    fun teardown() {
+        kosmos.testableContext.orCreateTestableResources.removeOverride(
+            R.bool.config_enableBouncerUserSwitcher
+        )
     }
 
     @Composable
diff --git a/packages/SystemUI/tests/utils/src/android/os/LooperKosmos.kt b/packages/SystemUI/tests/utils/src/android/os/LooperKosmos.kt
index 4303365..3f79c59 100644
--- a/packages/SystemUI/tests/utils/src/android/os/LooperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/os/LooperKosmos.kt
@@ -21,11 +21,12 @@
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testCase
 
-val Kosmos.looper by Fixture {
-    val testableLooper = TestableLooper.get(testCase)
-    checkNotNull(testableLooper) {
+val Kosmos.testableLooper: TestableLooper by Fixture {
+    checkNotNull(TestableLooper.get(testCase)) {
         "TestableLooper is null, make sure the test class is annotated with RunWithLooper"
     }
+}
+val Kosmos.looper: Looper by Fixture {
     checkNotNull(testableLooper.looper) {
         "TestableLooper.getLooper() is returning null, make sure the test class is annotated " +
             "with RunWithLooper"
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryKosmos.kt
index c0f8638..61698f4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryKosmos.kt
@@ -16,12 +16,16 @@
 
 package com.android.systemui.bouncer.data.repository
 
+import android.content.applicationContext
 import com.android.systemui.flags.featureFlagsClassic
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.settings.fakeGlobalSettings
 
 val Kosmos.bouncerRepository by Fixture {
     BouncerRepository(
+        applicationContext = applicationContext,
         flags = featureFlagsClassic,
+        globalSettings = fakeGlobalSettings,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
index 4394847..d27ecce 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.bouncer.data.repository.bouncerRepository
 import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -39,5 +40,6 @@
         uiEventLogger = uiEventLogger,
         sessionTracker = sessionTracker,
         sceneBackInteractor = sceneBackInteractor,
+        configurationInteractor = configurationInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/BroadcastDispatcherKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/BroadcastDispatcherKosmos.kt
index 7207948..b2289a1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/BroadcastDispatcherKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/BroadcastDispatcherKosmos.kt
@@ -16,19 +16,21 @@
 
 package com.android.systemui.broadcast
 
+import com.android.systemui.SysuiTestableContext
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.util.mockito.mock
 
+var Kosmos.broadcastDispatcherContext: SysuiTestableContext by Kosmos.Fixture { mock() }
 val Kosmos.broadcastDispatcher by
     Kosmos.Fixture {
         FakeBroadcastDispatcher(
-            context = mock(),
+            context = broadcastDispatcherContext,
             mainExecutor = mock(),
             broadcastRunningLooper = mock(),
             broadcastRunningExecutor = mock(),
             dumpManager = mock(),
             logger = mock(),
             userTracker = mock(),
-            shouldFailOnLeakedReceiver = false
+            shouldFailOnLeakedReceiver = false,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
index 2f13ba4..bd841ab 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
@@ -16,11 +16,14 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import com.android.keyguard.logging.KeyguardLogger
 import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -40,5 +43,7 @@
             shadeInteractor = { shadeInteractor },
             keyguardInteractor = { keyguardInteractor },
             sceneInteractor = { sceneInteractor },
+            keyguardLogger = KeyguardLogger(logcatLogBuffer("keyguard-logger-for-test")),
+            primaryBouncerInteractor = primaryBouncerInteractor,
         )
     }
diff --git a/packages/Vcn/TEST_MAPPING b/packages/Vcn/TEST_MAPPING
new file mode 100644
index 0000000..bde88fe
--- /dev/null
+++ b/packages/Vcn/TEST_MAPPING
@@ -0,0 +1,10 @@
+{
+  "postsubmit": [
+    {
+      "name": "FrameworksVcnTests"
+    },
+    {
+      "name": "CtsVcnTestCases"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
index 30a653d..66a6890 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
@@ -38,7 +38,6 @@
 import org.junit.runner.manipulation.Filter;
 import org.junit.runner.manipulation.Filterable;
 import org.junit.runner.manipulation.NoTestsRemainException;
-import org.junit.runner.notification.Failure;
 import org.junit.runner.notification.RunNotifier;
 import org.junit.runners.BlockJUnit4ClassRunner;
 import org.junit.runners.Suite;
@@ -94,7 +93,7 @@
     /** Keeps track of the runner on the current thread. */
     private static final ThreadLocal<RavenwoodAwareTestRunner> sCurrentRunner = new ThreadLocal<>();
 
-    static RavenwoodAwareTestRunner getCurrentRunner() {
+    private static RavenwoodAwareTestRunner getCurrentRunner() {
         var runner = sCurrentRunner.get();
         if (runner == null) {
             throw new RuntimeException("Current test runner not set!");
@@ -102,11 +101,9 @@
         return runner;
     }
 
-    private final Class<?> mTestJavaClass;
+    final Class<?> mTestJavaClass;
+    private final Runner mRealRunner;
     private TestClass mTestClass = null;
-    private Runner mRealRunner = null;
-    private Description mDescription = null;
-    private Throwable mExceptionInConstructor = null;
 
     /**
      * Stores internal states / methods associated with this runner that's only needed in
@@ -114,50 +111,37 @@
      */
     final RavenwoodRunnerState mState = new RavenwoodRunnerState(this);
 
-    public TestClass getTestClass() {
-        return mTestClass;
-    }
-
     /**
      * Constructor.
      */
     public RavenwoodAwareTestRunner(Class<?> testClass) {
         RavenwoodRuntimeEnvironmentController.globalInitOnce();
         mTestJavaClass = testClass;
-        try {
-            /*
-             * If the class has @DisabledOnRavenwood, then we'll delegate to
-             * ClassSkippingTestRunner, which simply skips it.
-             *
-             * We need to do it before instantiating TestClass for b/367694651.
-             */
-            if (!RavenwoodEnablementChecker.shouldRunClassOnRavenwood(testClass, true)) {
-                mRealRunner = new ClassSkippingTestRunner(testClass);
-                mDescription = mRealRunner.getDescription();
-                return;
-            }
 
-            mTestClass = new TestClass(testClass);
-
-            Log.v(TAG, "RavenwoodAwareTestRunner starting for " + testClass.getCanonicalName());
-
-            onRunnerInitializing();
-
-            mRealRunner = instantiateRealRunner(mTestClass);
-            mDescription = mRealRunner.getDescription();
-        } catch (Throwable th) {
-            // If we throw in the constructor, Tradefed may not report it and just ignore the class,
-            // so record it and throw it when the test actually started.
-            Log.e(TAG, "Fatal: Exception detected in constructor", th);
-            mExceptionInConstructor = new RuntimeException("Exception detected in constructor",
-                    th);
-            mDescription = Description.createTestDescription(testClass, "Constructor");
-
-            // This is for testing if tradefed is fixed.
-            if ("1".equals(System.getenv("RAVENWOOD_THROW_EXCEPTION_IN_TEST_RUNNER"))) {
-                throw th;
-            }
+        /*
+         * If the class has @DisabledOnRavenwood, then we'll delegate to
+         * ClassSkippingTestRunner, which simply skips it.
+         *
+         * We need to do it before instantiating TestClass for b/367694651.
+         */
+        if (!RavenwoodEnablementChecker.shouldRunClassOnRavenwood(testClass, true)) {
+            mRealRunner = new ClassSkippingTestRunner(testClass);
+            return;
         }
+
+        mTestClass = new TestClass(testClass);
+
+        Log.v(TAG, "RavenwoodAwareTestRunner starting for " + testClass.getCanonicalName());
+
+        // This is needed to make AndroidJUnit4ClassRunner happy.
+        InstrumentationRegistry.registerInstance(null, Bundle.EMPTY);
+
+        // Hook point to allow more customization.
+        runAnnotatedMethodsOnRavenwood(RavenwoodTestRunnerInitializing.class, null);
+
+        mRealRunner = instantiateRealRunner(mTestClass);
+
+        mState.enterTestRunner();
     }
 
     @Override
@@ -165,23 +149,11 @@
         return mRealRunner;
     }
 
-    /**
-     * Run the bare minimum setup to initialize the wrapped runner.
-     */
-    // This method is called by the ctor, so never make it virtual.
-    private void onRunnerInitializing() {
-        // This is needed to make AndroidJUnit4ClassRunner happy.
-        InstrumentationRegistry.registerInstance(null, Bundle.EMPTY);
-
-        // Hook point to allow more customization.
-        runAnnotatedMethodsOnRavenwood(RavenwoodTestRunnerInitializing.class, null);
-    }
-
     private void runAnnotatedMethodsOnRavenwood(Class<? extends Annotation> annotationClass,
             Object instance) {
         Log.v(TAG, "runAnnotatedMethodsOnRavenwood() " + annotationClass.getName());
 
-        for (var method : getTestClass().getAnnotatedMethods(annotationClass)) {
+        for (var method : mTestClass.getAnnotatedMethods(annotationClass)) {
             ensureIsPublicVoidMethod(method.getMethod(), /* isStatic=*/ instance == null);
 
             var methodDesc = method.getDeclaringClass().getName() + "."
@@ -195,11 +167,6 @@
     }
 
     @Override
-    public Description getDescription() {
-        return mDescription;
-    }
-
-    @Override
     public void run(RunNotifier realNotifier) {
         final var notifier = new RavenwoodRunNotifier(realNotifier);
         final var description = getDescription();
@@ -216,10 +183,6 @@
             dumpDescription(description);
         }
 
-        if (maybeReportExceptionFromConstructor(notifier)) {
-            return;
-        }
-
         // TODO(b/365976974): handle nested classes better
         final boolean skipRunnerHook =
                 mRealRunnerTakesRunnerBuilder && mRealRunner instanceof Suite;
@@ -228,7 +191,7 @@
         try {
             if (!skipRunnerHook) {
                 try {
-                    mState.enterTestClass(description);
+                    mState.enterTestClass();
                 } catch (Throwable th) {
                     notifier.reportBeforeTestFailure(description, th);
                     return;
@@ -251,18 +214,6 @@
         }
     }
 
-    /** Throw the exception detected in the constructor, if any. */
-    private boolean maybeReportExceptionFromConstructor(RunNotifier notifier) {
-        if (mExceptionInConstructor == null) {
-            return false;
-        }
-        notifier.fireTestStarted(mDescription);
-        notifier.fireTestFailure(new Failure(mDescription, mExceptionInConstructor));
-        notifier.fireTestFinished(mDescription);
-
-        return true;
-    }
-
     private Statement wrapWithHooks(Statement base, Description description, Scope scope,
             Order order) {
         return new Statement() {
@@ -338,7 +289,7 @@
             mState.enterTestMethod(description);
         }
 
-        final var classDescription = mState.getClassDescription();
+        final var classDescription = getDescription();
 
         // Class-level annotations are checked by the runner already, so we only check
         // method-level annotations here.
@@ -360,7 +311,7 @@
     private boolean onAfter(Description description, Scope scope, Order order, Throwable th) {
         Log.v(TAG, "onAfter: description=" + description + ", " + scope + ", " + order + ", " + th);
 
-        final var classDescription = mState.getClassDescription();
+        final var classDescription = getDescription();
 
         if (scope == Scope.Instance && order == Order.Outer) {
             // End of a test method.
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
index ead4a84..ec00e8f 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
@@ -52,37 +52,65 @@
         mRunner = runner;
     }
 
-    private Description mClassDescription;
+    /**
+     * The RavenwoodConfig used to configure the current Ravenwood environment.
+     * This can either come from mConfig or mRule.
+     */
+    private RavenwoodConfig mCurrentConfig;
+    /**
+     * The RavenwoodConfig declared in the test class
+     */
+    private RavenwoodConfig mConfig;
+    /**
+     * The RavenwoodRule currently in effect, declared in the test class
+     */
+    private RavenwoodRule mRule;
+    private boolean mHasRavenwoodRule;
     private Description mMethodDescription;
 
-    private RavenwoodConfig mCurrentConfig;
-    private RavenwoodRule mCurrentRule;
-    private boolean mHasRavenwoodRule;
-
-    public Description getClassDescription() {
-        return mClassDescription;
+    public RavenwoodConfig getConfig() {
+        return mCurrentConfig;
     }
 
-    public void enterTestClass(Description classDescription) {
-        Log.i(TAG, "enterTestClass: description=" + classDescription);
-        mClassDescription = classDescription;
+    public void enterTestRunner() {
+        Log.i(TAG, "enterTestRunner: " + mRunner);
 
-        mHasRavenwoodRule = hasRavenwoodRule(mRunner.getTestClass().getJavaClass());
-        mCurrentConfig = extractConfiguration(mRunner.getTestClass().getJavaClass());
+        mHasRavenwoodRule = hasRavenwoodRule(mRunner.mTestJavaClass);
+        mConfig = extractConfiguration(mRunner.mTestJavaClass);
+
+        if (mConfig != null) {
+            if (mHasRavenwoodRule) {
+                fail("RavenwoodConfig and RavenwoodRule cannot be used in the same class."
+                        + " Suggest migrating to RavenwoodConfig.");
+            }
+            mCurrentConfig = mConfig;
+        } else if (!mHasRavenwoodRule) {
+            // If no RavenwoodConfig and no RavenwoodRule, use a default config
+            mCurrentConfig = new RavenwoodConfig.Builder().build();
+        }
 
         if (mCurrentConfig != null) {
-            RavenwoodRuntimeEnvironmentController.init(mCurrentConfig);
+            RavenwoodRuntimeEnvironmentController.init(mRunner);
+        }
+    }
+
+    public void enterTestClass() {
+        Log.i(TAG, "enterTestClass: " + mRunner.mTestJavaClass.getName());
+
+        if (mCurrentConfig != null) {
+            RavenwoodRuntimeEnvironmentController.init(mRunner);
         }
     }
 
     public void exitTestClass() {
-        Log.i(TAG, "exitTestClass: description=" + mClassDescription);
-        if (mCurrentConfig != null) {
-            try {
+        Log.i(TAG, "exitTestClass: " + mRunner.mTestJavaClass.getName());
+        try {
+            if (mCurrentConfig != null) {
                 RavenwoodRuntimeEnvironmentController.reset();
-            } finally {
-                mClassDescription = null;
             }
+        } finally {
+            mConfig = null;
+            mRule = null;
         }
     }
 
@@ -92,6 +120,7 @@
 
     public void exitTestMethod() {
         mMethodDescription = null;
+        RavenwoodRuntimeEnvironmentController.reinit();
     }
 
     public void enterRavenwoodRule(RavenwoodRule rule) {
@@ -99,48 +128,32 @@
             fail("If you have a RavenwoodRule in your test, make sure the field type is"
                     + " RavenwoodRule so Ravenwood can detect it.");
         }
-        if (mCurrentConfig != null) {
-            fail("RavenwoodConfig and RavenwoodRule cannot be used in the same class."
-                    + " Suggest migrating to RavenwoodConfig.");
-        }
-        if (mCurrentRule != null) {
+        if (mRule != null) {
             fail("Multiple nesting RavenwoodRule's are detected in the same class,"
                     + " which is not supported.");
         }
-        mCurrentRule = rule;
-        RavenwoodRuntimeEnvironmentController.init(rule.getConfiguration());
+        mRule = rule;
+        if (mCurrentConfig == null) {
+            mCurrentConfig = rule.getConfiguration();
+        }
+        RavenwoodRuntimeEnvironmentController.init(mRunner);
     }
 
     public void exitRavenwoodRule(RavenwoodRule rule) {
-        if (mCurrentRule != rule) {
-            return; // This happens if the rule did _not_ take effect somehow.
+        if (mRule != rule) {
+            fail("RavenwoodRule did not take effect.");
         }
-
-        try {
-            RavenwoodRuntimeEnvironmentController.reset();
-        } finally {
-            mCurrentRule = null;
-        }
+        mRule = null;
     }
 
     /**
      * @return a configuration from a test class, if any.
      */
     @Nullable
-    private RavenwoodConfig extractConfiguration(Class<?> testClass) {
+    private static RavenwoodConfig extractConfiguration(Class<?> testClass) {
         var field = findConfigurationField(testClass);
         if (field == null) {
-            if (mHasRavenwoodRule) {
-                // Should be handled by RavenwoodRule
-                return null;
-            }
-
-            // If no RavenwoodConfig and no RavenwoodRule, return a default config
-            return new RavenwoodConfig.Builder().build();
-        }
-        if (mHasRavenwoodRule) {
-            fail("RavenwoodConfig and RavenwoodRule cannot be used in the same class."
-                    + " Suggest migrating to RavenwoodConfig.");
+            return null;
         }
 
         try {
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index de4357c..9002e40 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -137,7 +137,7 @@
         return res;
     }
 
-    private static RavenwoodConfig sConfig;
+    private static RavenwoodAwareTestRunner sRunner;
     private static RavenwoodSystemProperties sProps;
     private static boolean sInitialized = false;
 
@@ -171,6 +171,10 @@
         // Redirect stdout/stdin to liblog.
         RuntimeInit.redirectLogStreams();
 
+        // Touch some references early to ensure they're <clinit>'ed
+        Objects.requireNonNull(Build.TYPE);
+        Objects.requireNonNull(Build.VERSION.SDK);
+
         if (RAVENWOOD_VERBOSE_LOGGING) {
             RavenwoodCommonUtils.log(TAG, "Force enabling verbose logging");
             try {
@@ -191,12 +195,19 @@
     /**
      * Initialize the environment.
      */
-    public static void init(RavenwoodConfig config) {
+    public static void init(RavenwoodAwareTestRunner runner) {
         if (RAVENWOOD_VERBOSE_LOGGING) {
-            Log.i(TAG, "init() called here", new RuntimeException("STACKTRACE"));
+            Log.i(TAG, "init() called here: " + runner, new RuntimeException("STACKTRACE"));
         }
+        if (sRunner == runner) {
+            return;
+        }
+        if (sRunner != null) {
+            reset();
+        }
+        sRunner = runner;
         try {
-            initInner(config);
+            initInner(runner.mState.getConfig());
         } catch (Exception th) {
             Log.e(TAG, "init() failed", th);
             reset();
@@ -205,10 +216,6 @@
     }
 
     private static void initInner(RavenwoodConfig config) throws IOException {
-        if (sConfig != null) {
-            throw new RavenwoodRuntimeException("Internal error: init() called without reset()");
-        }
-        sConfig = config;
         if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
             maybeThrowPendingUncaughtException(false);
             Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler);
@@ -216,7 +223,7 @@
 
         android.os.Process.init$ravenwood(config.mUid, config.mPid);
         sOriginalIdentityToken = Binder.clearCallingIdentity();
-        Binder.restoreCallingIdentity(packBinderIdentityToken(false, config.mUid, config.mPid));
+        reinit();
         setSystemProperties(config.mSystemProperties);
 
         ServiceManager.init$ravenwood();
@@ -269,9 +276,7 @@
         config.mInstContext = instContext;
         config.mTargetContext = targetContext;
 
-        final Supplier<Resources> systemResourcesLoader = () -> {
-            return config.mState.loadResources(null);
-        };
+        final Supplier<Resources> systemResourcesLoader = () -> config.mState.loadResources(null);
 
         config.mState.mSystemServerContext =
                 new RavenwoodContext(ANDROID_PACKAGE_NAME, main, systemResourcesLoader);
@@ -288,10 +293,14 @@
                     RavenwoodRuntimeEnvironmentController::dumpStacks,
                     TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
         }
+    }
 
-        // Touch some references early to ensure they're <clinit>'ed
-        Objects.requireNonNull(Build.TYPE);
-        Objects.requireNonNull(Build.VERSION.SDK);
+    /**
+     * Partially re-initialize after each test method invocation
+     */
+    public static void reinit() {
+        var config = sRunner.mState.getConfig();
+        Binder.restoreCallingIdentity(packBinderIdentityToken(false, config.mUid, config.mPid));
     }
 
     /**
@@ -301,11 +310,11 @@
         if (RAVENWOOD_VERBOSE_LOGGING) {
             Log.i(TAG, "reset() called here", new RuntimeException("STACKTRACE"));
         }
-        if (sConfig == null) {
+        if (sRunner == null) {
             throw new RavenwoodRuntimeException("Internal error: reset() already called");
         }
-        var config = sConfig;
-        sConfig = null;
+        var config = sRunner.mState.getConfig();
+        sRunner = null;
 
         if (ENABLE_TIMEOUT_STACKS) {
             sPendingTimeout.cancel(false);
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerBase.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerBase.java
index 7c72f6b..31a1416 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerBase.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerBase.java
@@ -74,7 +74,7 @@
     }
 
     @Override
-    public Description getDescription() {
+    public final Description getDescription() {
         return getRealRunner().getDescription();
     }
 
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 773dba1..3d6ac0f 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -24,6 +24,8 @@
 import android.content.Context;
 import android.platform.test.annotations.DisabledOnRavenwood;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import com.android.ravenwood.common.RavenwoodCommonUtils;
 
 import org.junit.rules.TestRule;
@@ -219,8 +221,7 @@
      */
     @Deprecated
     public Context getContext() {
-        return Objects.requireNonNull(mConfiguration.mInstContext,
-                "Context is only available during @Test execution");
+        return InstrumentationRegistry.getInstrumentation().getContext();
     }
 
     /**
@@ -230,8 +231,7 @@
      */
     @Deprecated
     public Instrumentation getInstrumentation() {
-        return Objects.requireNonNull(mConfiguration.mInstrumentation,
-                "Instrumentation is only available during @Test execution");
+        return InstrumentationRegistry.getInstrumentation();
     }
 
     @Override
diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java
index 637f069..4e21f86 100644
--- a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java
+++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java
@@ -16,12 +16,15 @@
 package com.android.ravenwoodtest.bivalenttest.ravenizer;
 
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 
 import android.platform.test.annotations.DisabledOnRavenwood;
 import android.platform.test.annotations.RavenwoodTestRunnerInitializing;
 import android.platform.test.ravenwood.RavenwoodRule;
 import android.util.Log;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import junitparams.JUnitParamsRunner;
 import junitparams.Parameters;
 
@@ -30,6 +33,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.List;
+
 /**
  * Make sure RavenwoodAwareTestRunnerTest properly delegates to the original runner,
  * and also run the special annotated methods.
@@ -61,13 +66,22 @@
         sCallTracker.incrementMethodCallCount();
     }
 
+    public RavenwoodAwareTestRunnerTest() {
+        // Make sure the environment is already initialized when the constructor is called
+        assertNotNull(InstrumentationRegistry.getInstrumentation());
+    }
+
     @Test
     public void test1() {
         sCallTracker.incrementMethodCallCount();
     }
 
+    public static List<String> testParams() {
+        return List.of("foo", "bar");
+    }
+
     @Test
-    @Parameters({"foo", "bar"})
+    @Parameters(method = "testParams")
     public void testWithParams(String arg) {
         sCallTracker.incrementMethodCallCount();
     }
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerCallbackTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerCallbackTest.java
index bd01313..6720e45 100644
--- a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerCallbackTest.java
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerCallbackTest.java
@@ -337,9 +337,9 @@
     @Expected("""
     testRunStarted: classes
     testSuiteStarted: classes
-    testStarted: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ExceptionFromInnerRunnerConstructorTest)
-    testFailure: Exception detected in constructor
-    testFinished: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ExceptionFromInnerRunnerConstructorTest)
+    testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ExceptionFromInnerRunnerConstructorTest)
+    testFailure: Failed to instantiate class androidx.test.ext.junit.runners.AndroidJUnit4
+    testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ExceptionFromInnerRunnerConstructorTest)
     testSuiteFinished: classes
     testRunFinished: 1,1,0,0
     """)
@@ -439,9 +439,9 @@
     @Expected("""
     testRunStarted: classes
     testSuiteStarted: classes
-    testStarted: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BrokenRunnerTest)
-    testFailure: Exception detected in constructor
-    testFinished: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BrokenRunnerTest)
+    testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BrokenRunnerTest)
+    testFailure: Failed to instantiate class com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BrokenTestRunner
+    testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BrokenRunnerTest)
     testSuiteFinished: classes
     testRunFinished: 1,1,0,0
     """)
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java
index 73ea64f..02d1073 100644
--- a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java
@@ -106,17 +106,11 @@
     @Expected("""
     testRunStarted: classes
     testSuiteStarted: classes
-    testStarted: testMethod1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+    testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
     testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest.sConfig expected to be public static
-    testFinished: testMethod1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
-    testStarted: testMethod2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
-    testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest.sConfig expected to be public static
-    testFinished: testMethod2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
-    testStarted: testMethod3(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
-    testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest.sConfig expected to be public static
-    testFinished: testMethod3(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+    testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
     testSuiteFinished: classes
-    testRunFinished: 3,3,0,0
+    testRunFinished: 1,1,0,0
     """)
     // CHECKSTYLE:ON
     public static class ErrorMustBeReportedFromEachTest {
@@ -145,9 +139,9 @@
     @Expected("""
     testRunStarted: classes
     testSuiteStarted: classes
-    testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateConfigTest)
+    testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateConfigTest)
     testFailure: Class com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest.DuplicateConfigTest has multiple fields with @RavenwoodConfig.Config
-    testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateConfigTest)
+    testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateConfigTest)
     testSuiteFinished: classes
     testRunFinished: 1,1,0,0
     """)
@@ -175,9 +169,9 @@
     @Expected("""
     testRunStarted: classes
     testSuiteStarted: classes
-    testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest)
+    testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest)
     testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest.sConfig expected to be public static
-    testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest)
+    testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest)
     testSuiteFinished: classes
     testRunFinished: 1,1,0,0
     """)
@@ -201,9 +195,9 @@
     @Expected("""
     testRunStarted: classes
     testSuiteStarted: classes
-    testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest)
+    testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest)
     testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest.sConfig expected to be public static
-    testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest)
+    testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest)
     testSuiteFinished: classes
     testRunFinished: 1,1,0,0
     """)
@@ -227,9 +221,9 @@
     @Expected("""
     testRunStarted: classes
     testSuiteStarted: classes
-    testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeConfigTest)
+    testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeConfigTest)
     testFailure: Field com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest.WrongTypeConfigTest.sConfig has @RavenwoodConfig.Config but type is not RavenwoodConfig
-    testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeConfigTest)
+    testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeConfigTest)
     testSuiteFinished: classes
     testRunFinished: 1,1,0,0
     """)
@@ -282,9 +276,9 @@
     @Expected("""
     testRunStarted: classes
     testSuiteStarted: classes
-    testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithInstanceRuleTest)
+    testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithInstanceRuleTest)
     testFailure: RavenwoodConfig and RavenwoodRule cannot be used in the same class. Suggest migrating to RavenwoodConfig.
-    testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithInstanceRuleTest)
+    testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithInstanceRuleTest)
     testSuiteFinished: classes
     testRunFinished: 1,1,0,0
     """)
@@ -311,9 +305,9 @@
     @Expected("""
     testRunStarted: classes
     testSuiteStarted: classes
-    testStarted: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithStaticRuleTest)
-    testFailure: Exception detected in constructor
-    testFinished: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithStaticRuleTest)
+    testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithStaticRuleTest)
+    testFailure: Failed to instantiate class androidx.test.ext.junit.runners.AndroidJUnit4
+    testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithStaticRuleTest)
     testSuiteFinished: classes
     testRunFinished: 1,1,0,0
     """)
@@ -400,9 +394,9 @@
     @Expected("""
     testRunStarted: classes
     testSuiteStarted: classes
-    testStarted: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleInBaseClassTest)
+    testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleInBaseClassTest)
     testFailure: RavenwoodConfig and RavenwoodRule cannot be used in the same class. Suggest migrating to RavenwoodConfig.
-    testFinished: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleInBaseClassTest)
+    testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleInBaseClassTest)
     testSuiteFinished: classes
     testRunFinished: 1,1,0,0
     """)
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 42966a3..de3e2c9 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -4068,8 +4068,9 @@
     }
 
     /**
-     * Evaluate the service connection, return {@code true} if the client will change the state
-     * of the service host process by the given connection.
+     * Evaluate the service connection, return {@code true} if the client will change any state
+     * (ie. ProcessState, oomAdj, capability, etc) of the service host process by the given
+     * connection.
      */
     @GuardedBy("mService")
     boolean evaluateServiceConnectionAdd(ProcessRecord client, ProcessRecord app,
@@ -4077,20 +4078,40 @@
         if (evaluateConnectionPrelude(client, app)) {
             return true;
         }
-        if (app.getSetAdj() <= client.getSetAdj()
-                && app.getSetProcState() <= client.getSetProcState()
-                && ((app.getSetCapability() & client.getSetCapability())
-                        == client.getSetCapability()
-                        || cr.notHasFlag(Context.BIND_INCLUDE_CAPABILITIES
-                                | Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS))) {
-            // The service host process has better states than the client, so no change.
-            return false;
+
+        boolean needDryRun = false;
+        if (app.getSetAdj() > client.getSetAdj()) {
+            // The connection might elevate the importance of the service's oom adj score.
+            needDryRun = true;
+        } else if (app.getSetProcState() > client.getSetProcState()) {
+            // The connection might elevate the importance of the service's process state.
+            needDryRun = true;
+        } else if (cr.hasFlag(Context.BIND_INCLUDE_CAPABILITIES
+                            | Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS)
+                && (app.getSetCapability() & client.getSetCapability())
+                        != client.getSetCapability()) {
+            // The connection might elevate the importance of the service's capabilities.
+            needDryRun = true;
+        } else if (Flags.unfreezeBindPolicyFix()
+                && cr.hasFlag(Context.BIND_WAIVE_PRIORITY
+                            | Context.BIND_ALLOW_OOM_MANAGEMENT)) {
+            // These bind flags can grant the shouldNotFreeze state to the service.
+            needDryRun = true;
+        } else if (Flags.unfreezeBindPolicyFix()
+                && client.mOptRecord.shouldNotFreeze()
+                && !app.mOptRecord.shouldNotFreeze()) {
+            // The shouldNotFreeze state can be propagated and needs to be checked.
+            needDryRun = true;
         }
-        // Take a dry run of the computeServiceHostOomAdjLSP, this would't be expensive
-        // since it's only evaluating one service connection.
-        return computeServiceHostOomAdjLSP(cr, app, client, mInjector.getUptimeMillis(),
-                mService.getTopApp(), false, false, false, OOM_ADJ_REASON_NONE,
-                CACHED_APP_MIN_ADJ, false, true /* dryRun */);
+
+        if (needDryRun) {
+            // Take a dry run of the computeServiceHostOomAdjLSP, this would't be expensive
+            // since it's only evaluating one service connection.
+            return computeServiceHostOomAdjLSP(cr, app, client, mInjector.getUptimeMillis(),
+                    mService.getTopApp(), false, false, false, OOM_ADJ_REASON_NONE,
+                    CACHED_APP_MIN_ADJ, false, true /* dryRun */);
+        }
+        return false;
     }
 
     @GuardedBy("mService")
@@ -4100,17 +4121,26 @@
             return true;
         }
 
-        if (app.getSetAdj() < client.getSetAdj()
-                && app.getSetProcState() < client.getSetProcState()) {
-            // The service host process has better states than the client.
-            if (((app.getSetCapability() & client.getSetCapability()) == PROCESS_CAPABILITY_NONE)
-                    || cr.notHasFlag(Context.BIND_INCLUDE_CAPABILITIES
-                            | Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS)) {
-                // The service host app doesn't get any capabilities from the client.
-                return false;
-            }
+        if (app.getSetAdj() >= client.getSetAdj()) {
+            return true;
+        } else if (app.getSetProcState() >= client.getSetProcState()) {
+            return true;
+        } else if (cr.hasFlag(Context.BIND_INCLUDE_CAPABILITIES
+                            | Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS)
+                && (app.getSetCapability() & client.getSetCapability())
+                            != PROCESS_CAPABILITY_NONE) {
+            return true;
+        } else if (Flags.unfreezeBindPolicyFix()
+                && cr.hasFlag(Context.BIND_WAIVE_PRIORITY
+                            | Context.BIND_ALLOW_OOM_MANAGEMENT)) {
+            return true;
+        } else if (Flags.unfreezeBindPolicyFix()
+                && app.mOptRecord.shouldNotFreeze()
+                && client.mOptRecord.shouldNotFreeze()) {
+            // Process has shouldNotFreeze and it could have gotten it from the client.
+            return true;
         }
-        return true;
+        return false;
     }
 
     @GuardedBy("mService")
@@ -4118,14 +4148,25 @@
         if (evaluateConnectionPrelude(client, app)) {
             return true;
         }
-        if (app.getSetAdj() <= client.getSetAdj()
-                && app.getSetProcState() <= client.getSetProcState()) {
-            // The provider host process has better states than the client, so no change.
-            return false;
+
+        boolean needDryRun = false;
+        if (app.getSetAdj() > client.getSetAdj()) {
+            needDryRun = true;
+        } else if (app.getSetProcState() > client.getSetProcState()) {
+            needDryRun = true;
+        } else if (Flags.unfreezeBindPolicyFix()
+                && client.mOptRecord.shouldNotFreeze()
+                && !app.mOptRecord.shouldNotFreeze()) {
+            needDryRun = true;
         }
-        return computeProviderHostOomAdjLSP(null, app, client, mInjector.getUptimeMillis(),
-                mService.getTopApp(), false, false, false, OOM_ADJ_REASON_NONE, CACHED_APP_MIN_ADJ,
-                false, true /* dryRun */);
+
+        if (needDryRun) {
+            return computeProviderHostOomAdjLSP(null, app, client, mInjector.getUptimeMillis(),
+                    mService.getTopApp(), false, false, false, OOM_ADJ_REASON_NONE,
+                    CACHED_APP_MIN_ADJ,
+                    false, true /* dryRun */);
+        }
+        return false;
     }
 
     @GuardedBy("mService")
@@ -4133,12 +4174,19 @@
         if (evaluateConnectionPrelude(client, app)) {
             return true;
         }
-        if (app.getSetAdj() < client.getSetAdj()
-                && app.getSetProcState() < client.getSetProcState()) {
-            // The provider host process has better states than the client, so no change.
-            return false;
+
+        if (app.getSetAdj() >= client.getSetAdj()) {
+            return true;
+        } else if (app.getSetProcState() >= client.getSetProcState()) {
+            return true;
+        } else if (Flags.unfreezeBindPolicyFix()
+                && app.mOptRecord.shouldNotFreeze()
+                && client.mOptRecord.shouldNotFreeze()) {
+            // Process has shouldNotFreeze and it could have gotten it from the client.
+            return true;
         }
-        return true;
+
+        return false;
     }
 
     private boolean evaluateConnectionPrelude(ProcessRecord client, ProcessRecord app) {
diff --git a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java
index 57a5e3f..980c1f5 100644
--- a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java
+++ b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java
@@ -18,8 +18,8 @@
 
 import android.annotation.IntDef;
 import android.annotation.UptimeMillisLong;
-import android.app.ActivityManagerInternal.OomAdjReason;
 import android.app.ActivityManagerInternal.FrozenProcessListener;
+import android.app.ActivityManagerInternal.OomAdjReason;
 import android.util.Pair;
 import android.util.TimeUtils;
 
@@ -338,7 +338,11 @@
     boolean setShouldNotFreeze(boolean shouldNotFreeze, boolean dryRun,
             @ShouldNotFreezeReason int reason, int adjSeq) {
         if (dryRun) {
-            return mFrozen && !shouldNotFreeze;
+            if (Flags.unfreezeBindPolicyFix()) {
+                return mShouldNotFreeze != shouldNotFreeze;
+            } else {
+                return mFrozen && !shouldNotFreeze;
+            }
         }
         if (Flags.traceUpdateAppFreezeStateLsp()) {
             if (shouldNotFreeze) {
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index d680712..8834820 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -241,3 +241,13 @@
     is_fixed_read_only: true
     bug: "369487976"
 }
+
+flag {
+    name: "unfreeze_bind_policy_fix"
+    namespace: "backstage_power"
+    description: "Make sure shouldNotFreeze state change correctly triggers updates."
+    bug: "375691778"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
index 93b0e66..30c7319 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
@@ -115,7 +115,7 @@
     /** {@see FrameworkStatsLog.BIOMETRIC_ENROLLED}. */
     public void enroll(int statsModality, int statsAction, int statsClient,
             int targetUserId, long latency, boolean enrollSuccessful, float ambientLightLux,
-            int source) {
+            int source, int templateId) {
         FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_ENROLLED,
                 statsModality,
                 targetUserId,
@@ -124,7 +124,27 @@
                 -1, /* sensorId */
                 ambientLightLux,
                 source,
-                -1 /* templateId*/);
+                templateId);
+    }
+
+    /** {@see FrameworkStatsLog.BIOMETRIC_UNENROLLED} */
+    public void unenrolled(int statsModality, int targetUserId, int reason, int templateId) {
+        FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_UNENROLLED,
+                statsModality,
+                targetUserId,
+                reason,
+                templateId);
+    }
+
+    /** {@see FrameworkStatsLog.BIOMETRIC_ENUMERATED} */
+    public void enumerated(int statsModality, int targetUserId, int result, int[] templateIdsHal,
+            int[] templateIdsFramework) {
+        FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_ENUMERATED,
+                statsModality,
+                targetUserId,
+                result,
+                templateIdsHal,
+                templateIdsFramework);
     }
 
     /** {@see FrameworkStatsLog.BIOMETRIC_ERROR_OCCURRED}. */
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
index 9351bc0..08582ca 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
@@ -32,6 +32,8 @@
 import com.android.server.biometrics.AuthenticationStatsCollector;
 import com.android.server.biometrics.Utils;
 
+import java.util.Arrays;
+
 /**
  * Logger for all reported Biometric framework events.
  */
@@ -254,7 +256,7 @@
 
     /** Log enrollment outcome. */
     public void logOnEnrolled(int targetUserId, long latency, boolean enrollSuccessful,
-            int source) {
+            int source, int templateId) {
         if (!mShouldLogMetrics) {
             return;
         }
@@ -265,7 +267,8 @@
                     + ", Client: " + mStatsClient
                     + ", Latency: " + latency
                     + ", Lux: " + mALSProbe.getMostRecentLux()
-                    + ", Success: " + enrollSuccessful);
+                    + ", Success: " + enrollSuccessful
+                    + ", TemplateId: " + templateId);
         } else {
             Slog.v(TAG, "Enroll latency: " + latency);
         }
@@ -275,7 +278,51 @@
         }
 
         mSink.enroll(mStatsModality, mStatsAction, mStatsClient,
-                targetUserId, latency, enrollSuccessful, mALSProbe.getMostRecentLux(), source);
+                targetUserId, latency, enrollSuccessful, mALSProbe.getMostRecentLux(), source,
+                templateId);
+    }
+
+    /** Log un-enrollment. */
+    public void logOnUnEnrolled(int targetUserId, int reason, int templateId) {
+        if (!mShouldLogMetrics) {
+            return;
+        }
+
+        if (DEBUG) {
+            Slog.v(TAG, "UnEnrolled! Modality: " + mStatsModality
+                    + ", User: " + targetUserId
+                    + ", reason: " + reason
+                    + ", templateId: " + templateId);
+        }
+
+        if (shouldSkipLogging()) {
+            return;
+        }
+
+        mSink.unenrolled(mStatsModality, targetUserId, reason, templateId);
+    }
+
+    /** Log enumeration. */
+    public void logOnEnumerated(int targetUserId, int result, int[] templateIdsHal,
+            int[] templateIdsFramework) {
+        if (!mShouldLogMetrics) {
+            return;
+        }
+
+        if (DEBUG) {
+            Slog.v(TAG, "Enumerated! Modality: " + mStatsModality
+                    + ", User: " + targetUserId
+                    + ", result: " + result
+                    + ", templateIdsHal: " + Arrays.toString(templateIdsHal)
+                    + ", templateIdsFramework: " + Arrays.toString(templateIdsFramework));
+        }
+
+        if (shouldSkipLogging()) {
+            return;
+        }
+
+        mSink.enumerated(mStatsModality, targetUserId, result, templateIdsHal,
+                templateIdsFramework);
     }
 
     /** Report unexpected enrollment reported by the HAL. */
diff --git a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
index 32c0bd3..38bf999 100644
--- a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
@@ -95,7 +95,7 @@
             mBiometricUtils.addBiometricForUser(getContext(), getTargetUserId(), identifier);
             getLogger().logOnEnrolled(getTargetUserId(),
                     System.currentTimeMillis() - mEnrollmentStartTimeMs,
-                    true /* enrollSuccessful */, mEnrollReason);
+                    true /* enrollSuccessful */, mEnrollReason, identifier.getBiometricId());
             mCallback.onClientFinished(this, true /* success */);
         }
         notifyUserActivity();
@@ -123,7 +123,7 @@
     public void onError(int error, int vendorCode) {
         getLogger().logOnEnrolled(getTargetUserId(),
                 System.currentTimeMillis() - mEnrollmentStartTimeMs,
-                false /* enrollSuccessful */, mEnrollReason);
+                false /* enrollSuccessful */, mEnrollReason, -1 /* templateId */);
         super.onError(error, vendorCode);
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
index 6c30559..03b382e 100644
--- a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
+import android.hardware.biometrics.BiometricsProtoEnums;
 import android.os.Build;
 import android.os.IBinder;
 import android.util.Slog;
@@ -135,7 +136,7 @@
             Supplier<T> lazyDaemon, IBinder token, int biometricId, int userId, String owner,
             BiometricUtils<S> utils, int sensorId,
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
-            Map<Integer, Long> authenticatorIds);
+            Map<Integer, Long> authenticatorIds, int reason);
 
     protected InternalCleanupClient(@NonNull Context context, @NonNull Supplier<T> lazyDaemon,
             int userId, @NonNull String owner, int sensorId,
@@ -158,7 +159,8 @@
         mCurrentTask = getRemovalClient(getContext(), mLazyDaemon, getToken(),
                 template.mIdentifier.getBiometricId(), template.mUserId,
                 getContext().getPackageName(), mBiometricUtils, getSensorId(),
-                getLogger(), getBiometricContext(), mAuthenticatorIds);
+                getLogger(), getBiometricContext(), mAuthenticatorIds,
+                BiometricsProtoEnums.UNENROLL_REASON_DANGLING_HAL);
 
         getLogger().logUnknownEnrollmentInHal();
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
index 2c2c685..5a47f1be 100644
--- a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
+import android.hardware.biometrics.BiometricsProtoEnums;
 import android.os.IBinder;
 import android.util.Slog;
 
@@ -46,6 +47,10 @@
     // List of templates to remove from the HAL
     private List<BiometricAuthenticator.Identifier> mUnknownHALTemplates = new ArrayList<>();
     private final int mInitialEnrolledSize;
+    private final int[] mEnrolledIdsFrameworkArray;
+    private final List<Integer> mEnrolledIdsHalList = new ArrayList<>();
+    private boolean mIsDanglingFramework;
+    private boolean mIsDanglingHal;
 
     protected InternalEnumerateClient(@NonNull Context context, @NonNull Supplier<T> lazyDaemon,
             @NonNull IBinder token, int userId, @NonNull String owner,
@@ -60,14 +65,26 @@
         mEnrolledList = enrolledList;
         mInitialEnrolledSize = mEnrolledList.size();
         mUtils = utils;
+        // Record ids from frameworks for metrics
+        mEnrolledIdsFrameworkArray = new int[mInitialEnrolledSize];
+        for (int i = 0; i < mInitialEnrolledSize; i++) {
+            mEnrolledIdsFrameworkArray[i] = mEnrolledList.get(i).getBiometricId();
+        }
     }
 
     @Override
     public void onEnumerationResult(BiometricAuthenticator.Identifier identifier,
             int remaining) {
+        if (identifier != null) {
+            // Record ids from hal for metrics
+            mEnrolledIdsHalList.add(identifier.getBiometricId());
+        }
         handleEnumeratedTemplate(identifier);
         if (remaining == 0) {
+            mIsDanglingHal = !mUnknownHALTemplates.isEmpty();
+            mIsDanglingFramework = !mEnrolledList.isEmpty();
             doTemplateCleanup();
+            logEnumerationResult();
             mCallback.onClientFinished(this, true /* success */);
         }
     }
@@ -123,7 +140,9 @@
                     + identifier.getBiometricId() + " " + identifier.getName());
             mUtils.removeBiometricForUser(getContext(),
                     getTargetUserId(), identifier.getBiometricId());
-
+            getLogger().logOnUnEnrolled(getTargetUserId(),
+                    BiometricsProtoEnums.UNENROLL_REASON_DANGLING_FRAMEWORK,
+                    identifier.getBiometricId());
             getLogger().logUnknownEnrollmentInFramework();
         }
 
@@ -134,6 +153,32 @@
         mEnrolledList.clear();
     }
 
+    private void logEnumerationResult() {
+        final int result;
+        if (mIsDanglingFramework && mIsDanglingHal) {
+            result = BiometricsProtoEnums.ENUMERATION_RESULT_DANGLING_BOTH;
+        } else if (mIsDanglingFramework) {
+            result = BiometricsProtoEnums.ENUMERATION_RESULT_DANGLING_FRAMEWORK;
+        } else if (mIsDanglingHal) {
+            result = BiometricsProtoEnums.ENUMERATION_RESULT_DANGLING_HAL;
+        } else {
+            result = BiometricsProtoEnums.ENUMERATION_RESULT_OK;
+        }
+
+        final int[] idsHalArray = listToArray(mEnrolledIdsHalList);
+        getLogger().logOnEnumerated(
+                getTargetUserId(), result, idsHalArray, mEnrolledIdsFrameworkArray);
+    }
+
+    private int[] listToArray(List<Integer> ids) {
+        final int size = ids.size();
+        int[] array = new int[size];
+        for (int i = 0; i < size; i++) {
+            array[i] = ids.get(i);
+        }
+        return array;
+    }
+
     public List<BiometricAuthenticator.Identifier> getUnknownHALTemplates() {
         return mUnknownHALTemplates;
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java
index ad5877a..0259906 100644
--- a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java
@@ -42,17 +42,19 @@
     private final BiometricUtils<S> mBiometricUtils;
     private final Map<Integer, Long> mAuthenticatorIds;
     private final boolean mHasEnrollmentsBeforeStarting;
+    private final int mReason;
 
     public RemovalClient(@NonNull Context context, @NonNull Supplier<T> lazyDaemon,
             @NonNull IBinder token, @NonNull ClientMonitorCallbackConverter listener,
             int userId, @NonNull String owner, @NonNull BiometricUtils<S> utils, int sensorId,
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
-            @NonNull Map<Integer, Long> authenticatorIds) {
+            @NonNull Map<Integer, Long> authenticatorIds, int reason) {
         super(context, lazyDaemon, token, listener, userId, owner, 0 /* cookie */, sensorId,
                 logger, biometricContext, false /* isMandatoryBiometrics */);
         mBiometricUtils = utils;
         mAuthenticatorIds = authenticatorIds;
         mHasEnrollmentsBeforeStarting = !utils.getBiometricsForUser(context, userId).isEmpty();
+        mReason = reason;
     }
 
     @Override
@@ -87,6 +89,7 @@
         Slog.d(TAG, "onRemoved: " + identifier.getBiometricId() + " remaining: " + remaining);
         mBiometricUtils.removeBiometricForUser(getContext(), getTargetUserId(),
                 identifier.getBiometricId());
+        getLogger().logOnUnEnrolled(getTargetUserId(), mReason, identifier.getBiometricId());
 
         try {
             getListener().onRemoved(identifier, remaining);
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java
index c27b7c4..5adc251 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java
@@ -63,12 +63,12 @@
             Supplier<AidlSession> lazyDaemon, IBinder token,
             int biometricId, int userId, String owner, BiometricUtils<Face> utils, int sensorId,
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
-            Map<Integer, Long> authenticatorIds) {
+            Map<Integer, Long> authenticatorIds, int reason) {
         // Internal remove does not need to send results to anyone. Cleanup (enumerate + remove)
         // is all done internally.
         return new FaceRemovalClient(context, lazyDaemon, token,
                 null /* ClientMonitorCallbackConverter */, new int[] {biometricId}, userId, owner,
-                utils, sensorId, logger, biometricContext, authenticatorIds);
+                utils, sensorId, logger, biometricContext, authenticatorIds, reason);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index 5127e68..36e4a7e 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -668,7 +668,8 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN,
                             mAuthenticationStatsCollector),
                     mBiometricContext,
-                    mFaceSensors.get(sensorId).getAuthenticatorIds());
+                    mFaceSensors.get(sensorId).getAuthenticatorIds(),
+                    BiometricsProtoEnums.UNENROLL_REASON_USER_REQUEST);
             scheduleForSensor(sensorId, client, mBiometricStateCallback);
         });
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceRemovalClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceRemovalClient.java
index 0793888..971cfbb 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceRemovalClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceRemovalClient.java
@@ -46,9 +46,9 @@
             int[] biometricIds, int userId, @NonNull String owner,
             @NonNull BiometricUtils<Face> utils, int sensorId,
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
-            @NonNull Map<Integer, Long> authenticatorIds) {
+            @NonNull Map<Integer, Long> authenticatorIds, int reason) {
         super(context, lazyDaemon, token, listener, userId, owner, utils, sensorId,
-                logger, biometricContext, authenticatorIds);
+                logger, biometricContext, authenticatorIds, reason);
         mBiometricIds = biometricIds;
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java
index 40b8a45..77670ec 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java
@@ -71,11 +71,11 @@
             Supplier<AidlSession> lazyDaemon, IBinder token, int biometricId, int userId,
             String owner, BiometricUtils<Fingerprint> utils, int sensorId,
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
-            Map<Integer, Long> authenticatorIds) {
+            Map<Integer, Long> authenticatorIds, int reason) {
         return new FingerprintRemovalClient(context, lazyDaemon, token,
                 null /* ClientMonitorCallbackConverter */, new int[] {biometricId}, userId, owner,
                 utils, sensorId, logger.swapAction(context, BiometricsProtoEnums.ACTION_REMOVE),
-                biometricContext, authenticatorIds);
+                biometricContext, authenticatorIds, reason);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index 456591c..c18d925 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -659,7 +659,8 @@
                     sensorId, createLogger(BiometricsProtoEnums.ACTION_REMOVE,
                     BiometricsProtoEnums.CLIENT_UNKNOWN,
                     mAuthenticationStatsCollector), mBiometricContext,
-                    mFingerprintSensors.get(sensorId).getAuthenticatorIds());
+                    mFingerprintSensors.get(sensorId).getAuthenticatorIds(),
+                    BiometricsProtoEnums.UNENROLL_REASON_USER_REQUEST);
             scheduleForSensor(sensorId, client, mBiometricStateCallback);
         });
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintRemovalClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintRemovalClient.java
index 4f08f6f..c6b955a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintRemovalClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintRemovalClient.java
@@ -47,9 +47,9 @@
             @Nullable ClientMonitorCallbackConverter listener, int[] biometricIds, int userId,
             @NonNull String owner, @NonNull BiometricUtils<Fingerprint> utils, int sensorId,
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
-            @NonNull Map<Integer, Long> authenticatorIds) {
+            @NonNull Map<Integer, Long> authenticatorIds, int reason) {
         super(context, lazyDaemon, token, listener, userId, owner, utils, sensorId,
-                logger, biometricContext, authenticatorIds);
+                logger, biometricContext, authenticatorIds, reason);
         mBiometricIds = biometricIds;
     }
 
diff --git a/services/core/java/com/android/server/input/KeyboardGlyphManager.java b/services/core/java/com/android/server/input/KeyboardGlyphManager.java
index f59d72b..6f19a3f 100644
--- a/services/core/java/com/android/server/input/KeyboardGlyphManager.java
+++ b/services/core/java/com/android/server/input/KeyboardGlyphManager.java
@@ -149,17 +149,17 @@
                 continue;
             }
             final ActivityInfo activityInfo = resolveInfo.activityInfo;
-            KeyGlyphMapData data = getKeyboardGlyphMapsInPackage(pm, activityInfo);
-            if (data == null) {
+            List<KeyGlyphMapData> data = getKeyboardGlyphMapsInPackage(pm, activityInfo);
+            if (data == null || data.isEmpty()) {
                 continue;
             }
-            glyphMaps.add(data);
+            glyphMaps.addAll(data);
         }
         return glyphMaps;
     }
 
     @Nullable
-    private KeyGlyphMapData getKeyboardGlyphMapsInPackage(PackageManager pm,
+    private List<KeyGlyphMapData> getKeyboardGlyphMapsInPackage(PackageManager pm,
             @NonNull ActivityInfo receiver) {
         Bundle metaData = receiver.metaData;
         if (metaData == null) {
@@ -175,6 +175,7 @@
 
         try {
             Resources resources = pm.getResourcesForApplication(receiver.applicationInfo);
+            List<KeyGlyphMapData> glyphMaps = new ArrayList<>();
             try (XmlResourceParser parser = resources.getXml(configResId)) {
                 XmlUtils.beginDocument(parser, TAG_KEYBOARD_GLYPH_MAPS);
 
@@ -193,13 +194,14 @@
                         int vendor = a.getInt(R.styleable.KeyboardGlyphMap_vendorId, -1);
                         int product = a.getInt(R.styleable.KeyboardGlyphMap_productId, -1);
                         if (glyphMapRes != 0 && vendor != -1 && product != -1) {
-                            return new KeyGlyphMapData(receiver.packageName, receiver.name,
-                                    glyphMapRes, vendor, product);
+                            glyphMaps.add(new KeyGlyphMapData(receiver.packageName, receiver.name,
+                                    glyphMapRes, vendor, product));
                         }
                     } finally {
                         a.recycle();
                     }
                 }
+                return glyphMaps;
             }
         } catch (Exception ex) {
             Slog.w(TAG, "Could not parse keyboard glyph map resource from receiver "
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 131b9ba..15d76a2 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -6078,8 +6078,6 @@
         p.println("  mSystemReady=" + mSystemReady);
         p.println("  mInteractive=" + mIsInteractive);
         p.println("  mConcurrentMultiUserModeEnabled=" + mConcurrentMultiUserModeEnabled);
-        p.println("  ENABLE_HIDE_IME_CAPTION_BAR="
-                + InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR);
         final int currentImeUserId;
         synchronized (ImfLock.class) {
             currentImeUserId = mCurrentImeUserId;
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
new file mode 100644
index 0000000..40a27e8
--- /dev/null
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.media.quality;
+
+import android.content.Context;
+import android.media.quality.IMediaQualityManager;
+
+import com.android.server.SystemService;
+
+/**
+ * This service manage picture profile and sound profile for TV setting. Also communicates with the
+ * database to save, update the profiles.
+ */
+public class MediaQualityService extends SystemService {
+
+    private static final boolean DEBUG = false;
+    private static final String TAG = "MediaQualityService";
+    private final Context mContext;
+
+    public MediaQualityService(Context context) {
+        super(context);
+        mContext = context;
+    }
+
+    @Override
+    public void onStart() {
+        publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService());
+    }
+
+    // TODO: Add additional APIs. b/373951081
+    private final class BinderService extends IMediaQualityManager.Stub {
+    }
+}
diff --git a/services/core/java/com/android/server/media/quality/OWNERS b/services/core/java/com/android/server/media/quality/OWNERS
new file mode 100644
index 0000000..e455846
--- /dev/null
+++ b/services/core/java/com/android/server/media/quality/OWNERS
@@ -0,0 +1,2 @@
+shubang@google.com
+haofanw@google.com
\ No newline at end of file
diff --git a/services/core/java/com/android/server/vcn/OWNERS b/services/core/java/com/android/server/vcn/OWNERS
index 2441e77..937699a 100644
--- a/services/core/java/com/android/server/vcn/OWNERS
+++ b/services/core/java/com/android/server/vcn/OWNERS
@@ -1,7 +1,6 @@
 set noparent
 
-benedictwong@google.com
-ckesting@google.com
 evitayan@google.com
-junyin@google.com
 nharold@google.com
+benedictwong@google.com #{LAST_RESORT_SUGGESTION}
+yangji@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java
index 15f86e9..c8d5a03 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperData.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java
@@ -29,6 +29,7 @@
 import android.app.WallpaperColors;
 import android.app.WallpaperManager.ScreenOrientation;
 import android.app.WallpaperManager.SetWallpaperFlags;
+import android.app.wallpaper.WallpaperDescription;
 import android.content.ComponentName;
 import android.graphics.Rect;
 import android.os.RemoteCallbackList;
@@ -77,6 +78,8 @@
 
     /**
      * The component name of the currently set live wallpaper.
+     *
+     * @deprecated
      */
     private ComponentName mWallpaperComponent;
 
@@ -179,6 +182,9 @@
      */
     int mOrientationWhenSet = ORIENTATION_UNKNOWN;
 
+    /** Description of the current wallpaper */
+    private WallpaperDescription mDescription;
+
     WallpaperData(int userId, @SetWallpaperFlags int wallpaperType) {
         this.userId = userId;
         this.mWhich = wallpaperType;
@@ -238,6 +244,14 @@
         this.mWallpaperComponent = componentName;
     }
 
+    WallpaperDescription getDescription() {
+        return mDescription;
+    }
+
+    void setDescription(WallpaperDescription description) {
+        this.mDescription = description;
+    }
+
     @Override
     public String toString() {
         StringBuilder out = new StringBuilder(defaultString(this));
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
index 74ca230..cf76bf0 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wallpaper;
 
+import static android.app.Flags.liveWallpaperContentHandling;
 import static android.app.Flags.removeNextWallpaperComponent;
 import static android.app.WallpaperManager.FLAG_LOCK;
 import static android.app.WallpaperManager.FLAG_SYSTEM;
@@ -30,11 +31,13 @@
 import static com.android.server.wallpaper.WallpaperUtils.makeWallpaperIdLocked;
 import static com.android.window.flags.Flags.multiCrop;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.WallpaperColors;
 import android.app.WallpaperManager;
 import android.app.WallpaperManager.SetWallpaperFlags;
 import android.app.backup.WallpaperBackupHelper;
+import android.app.wallpaper.WallpaperDescription;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -194,9 +197,16 @@
 
         if (loadSystem) {
             if (!success) {
+                // Set safe values that won't cause crashes
                 wallpaper.cropHint.set(0, 0, 0, 0);
                 wpdData.mPadding.set(0, 0, 0, 0);
                 wallpaper.name = "";
+                if (liveWallpaperContentHandling()) {
+                    wallpaper.setDescription(new WallpaperDescription.Builder().setComponent(
+                            mImageWallpaper).build());
+                } else {
+                    wallpaper.setComponent(mImageWallpaper);
+                }
             } else {
                 if (wallpaper.wallpaperId <= 0) {
                     wallpaper.wallpaperId = makeWallpaperIdLocked();
@@ -245,25 +255,11 @@
                         parseWallpaperAttributes(parser, wallpaperToParse, keepDimensionHints);
                     }
 
-                    String comp = parser.getAttributeValue(null, "component");
+                    ComponentName comp = parseComponentName(parser);
                     if (removeNextWallpaperComponent()) {
-                        wallpaperToParse.setComponent(comp != null
-                                ? ComponentName.unflattenFromString(comp)
-                                : null);
-                        if (wallpaperToParse.getComponent() == null
-                                || "android".equals(wallpaperToParse.getComponent()
-                                .getPackageName())) {
-                            wallpaperToParse.setComponent(mImageWallpaper);
-                        }
+                        wallpaperToParse.setComponent(comp);
                     } else {
-                        wallpaperToParse.nextWallpaperComponent = comp != null
-                                ? ComponentName.unflattenFromString(comp)
-                                : null;
-                        if (wallpaperToParse.nextWallpaperComponent == null
-                                || "android".equals(wallpaperToParse.nextWallpaperComponent
-                                .getPackageName())) {
-                            wallpaperToParse.nextWallpaperComponent = mImageWallpaper;
-                        }
+                        wallpaperToParse.nextWallpaperComponent = comp;
                     }
 
                     if (multiCrop()) {
@@ -290,6 +286,17 @@
         return lockWallpaper;
     }
 
+    @NonNull
+    private ComponentName parseComponentName(TypedXmlPullParser parser) {
+        String comp = parser.getAttributeValue(null, "component");
+        ComponentName c = (comp != null) ? ComponentName.unflattenFromString(comp) : null;
+        if (c == null || "android".equals(c.getPackageName())) {
+            c = mImageWallpaper;
+        }
+
+        return c;
+    }
+
     private void ensureSaneWallpaperData(WallpaperData wallpaper) {
         // Only overwrite cropHint if the rectangle is invalid.
         if (wallpaper.cropHint.width() < 0
@@ -332,9 +339,29 @@
         }
     }
 
+    void parseWallpaperDescription(TypedXmlPullParser parser, WallpaperData wallpaper)
+            throws XmlPullParserException, IOException {
+
+        int type = parser.next();
+        if (type == XmlPullParser.START_TAG && "description".equals(parser.getName())) {
+            // Always read the description if it's there - there may be one from a previous save
+            // with content handling enabled even if it's enabled now
+            WallpaperDescription description = WallpaperDescription.restoreFromXml(parser);
+            if (liveWallpaperContentHandling()) {
+                // null component means that wallpaper was last saved without content handling, so
+                // populate description from saved component
+                if (description.getComponent() == null) {
+                    description = description.toBuilder().setComponent(
+                            parseComponentName(parser)).build();
+                }
+                wallpaper.setDescription(description);
+            }
+        }
+    }
+
     @VisibleForTesting
     void parseWallpaperAttributes(TypedXmlPullParser parser, WallpaperData wallpaper,
-            boolean keepDimensionHints) throws XmlPullParserException {
+            boolean keepDimensionHints) throws XmlPullParserException, IOException {
         final int id = parser.getAttributeInt(null, "id", -1);
         if (id != -1) {
             wallpaper.wallpaperId = id;
@@ -355,8 +382,7 @@
                 getAttributeInt(parser, "totalCropTop", 0),
                 getAttributeInt(parser, "totalCropRight", 0),
                 getAttributeInt(parser, "totalCropBottom", 0));
-        ComponentName componentName = removeNextWallpaperComponent() ? wallpaper.getComponent()
-                : wallpaper.nextWallpaperComponent;
+        ComponentName componentName = parseComponentName(parser);
         if (multiCrop() && mImageWallpaper.equals(componentName)) {
             wallpaper.mCropHints = new SparseArray<>();
             for (Pair<Integer, String> pair: screenDimensionPairs()) {
@@ -443,6 +469,15 @@
         }
         wallpaper.name = parser.getAttributeValue(null, "name");
         wallpaper.allowBackup = parser.getAttributeBoolean(null, "backup", false);
+
+        parseWallpaperDescription(parser, wallpaper);
+        if (liveWallpaperContentHandling() && wallpaper.getDescription().getComponent() == null) {
+            // The last save was done before the content handling flag was enabled and has no
+            // WallpaperDescription, so create a default one with the correct component.
+            // CSP: log boot after flag change to false -> true
+            wallpaper.setDescription(
+                    new WallpaperDescription.Builder().setComponent(componentName).build());
+        }
     }
 
     private static int getAttributeInt(TypedXmlPullParser parser, String name, int defValue) {
@@ -610,9 +645,27 @@
             out.attributeBoolean(null, "backup", true);
         }
 
+        writeWallpaperDescription(out, wallpaper);
+
         out.endTag(null, tag);
     }
 
+    void writeWallpaperDescription(TypedXmlSerializer out, WallpaperData wallpaper)
+            throws IOException {
+        if (liveWallpaperContentHandling()) {
+            WallpaperDescription description = wallpaper.getDescription();
+            if (description != null) {
+                String descriptionTag = "description";
+                out.startTag(null, descriptionTag);
+                try {
+                    description.saveToXml(out);
+                } catch (XmlPullParserException e) {
+                    Slog.e(TAG, "Error writing wallpaper description", e);
+                }
+                out.endTag(null, descriptionTag);
+            }
+        }
+    }
     // Restore the named resource bitmap to both source + crop files
     boolean restoreNamedResourceLocked(WallpaperData wallpaper) {
         if (wallpaper.name.length() > 4 && "res:".equals(wallpaper.name.substring(0, 4))) {
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 6cc4b1e..5c53c2a 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -1726,8 +1726,7 @@
                 ActivityRecord currentActivity,
                 ArrayList<ActivityRecord> previousActivity,
                 WindowContainer removedWindowContainer) {
-            final ScheduleAnimationBuilder builder =
-                    new ScheduleAnimationBuilder(backType, adapter, monitor);
+            final ScheduleAnimationBuilder builder = new ScheduleAnimationBuilder(adapter, monitor);
             switch (backType) {
                 case BackNavigationInfo.TYPE_RETURN_TO_HOME:
                     return builder
@@ -1752,7 +1751,6 @@
         }
 
         class ScheduleAnimationBuilder {
-            final int mType;
             final BackAnimationAdapter mBackAnimationAdapter;
             final NavigationMonitor mNavigationMonitor;
             WindowContainer mCloseTarget;
@@ -1760,9 +1758,8 @@
             boolean mIsLaunchBehind;
             TaskSnapshot mSnapshot;
 
-            ScheduleAnimationBuilder(int type, BackAnimationAdapter adapter,
+            ScheduleAnimationBuilder(BackAnimationAdapter adapter,
                     NavigationMonitor monitor) {
-                mType = type;
                 mBackAnimationAdapter = adapter;
                 mNavigationMonitor = monitor;
             }
@@ -1793,7 +1790,36 @@
             }
 
             private Transition prepareTransitionIfNeeded(ActivityRecord[] visibleOpenActivities) {
-                if (mSnapshot == null) {
+                if (Flags.unifyBackNavigationTransition()) {
+                    if (mCloseTarget.asWindowState() != null) {
+                        return null;
+                    }
+                    final ArrayList<ActivityRecord> makeVisibles = new ArrayList<>();
+                    for (int i = visibleOpenActivities.length - 1; i >= 0; --i) {
+                        final ActivityRecord activity = visibleOpenActivities[i];
+                        if (activity.mLaunchTaskBehind || activity.isVisibleRequested()) {
+                            continue;
+                        }
+                        makeVisibles.add(activity);
+                    }
+                    final TransitionController tc = visibleOpenActivities[0].mTransitionController;
+                    final Transition prepareOpen = tc.createTransition(
+                            TRANSIT_PREPARE_BACK_NAVIGATION);
+                    tc.collect(mCloseTarget);
+                    prepareOpen.setBackGestureAnimation(mCloseTarget, true /* isTop */);
+                    for (int i = mOpenTargets.length - 1; i >= 0; --i) {
+                        tc.collect(mOpenTargets[i]);
+                        prepareOpen.setBackGestureAnimation(mOpenTargets[i], false /* isTop */);
+                    }
+                    if (!makeVisibles.isEmpty()) {
+                        setLaunchBehind(visibleOpenActivities);
+                    }
+                    tc.requestStartTransition(prepareOpen,
+                            null /*startTask */, null /* remoteTransition */,
+                            null /* displayChange */);
+                    prepareOpen.setReady(makeVisibles.get(0), true);
+                    return prepareOpen;
+                } else if (mSnapshot == null) {
                     return setLaunchBehind(visibleOpenActivities);
                 }
                 return null;
@@ -1990,6 +2016,7 @@
 
     private static Transition setLaunchBehind(@NonNull ActivityRecord[] activities) {
         final boolean migrateBackTransition = Flags.migratePredictiveBackTransition();
+        final boolean unifyBackNavigationTransition = Flags.unifyBackNavigationTransition();
         final ArrayList<ActivityRecord> affects = new ArrayList<>();
         for (int i = activities.length - 1; i >= 0; --i) {
             final ActivityRecord activity = activities[i];
@@ -2003,8 +2030,8 @@
         }
 
         final TransitionController tc = activities[0].mTransitionController;
-        final Transition prepareOpen = migrateBackTransition && !tc.isCollecting()
-                ? tc.createTransition(TRANSIT_PREPARE_BACK_NAVIGATION) : null;
+        final Transition prepareOpen = migrateBackTransition && !unifyBackNavigationTransition
+                && !tc.isCollecting() ? tc.createTransition(TRANSIT_PREPARE_BACK_NAVIGATION) : null;
 
         DisplayContent commonDisplay = null;
         for (int i = affects.size() - 1; i >= 0; --i) {
diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java
index 4824c16..9f40bed 100644
--- a/services/core/java/com/android/server/wm/Dimmer.java
+++ b/services/core/java/com/android/server/wm/Dimmer.java
@@ -261,6 +261,14 @@
         }
     }
 
+    boolean hasDimState() {
+        return mDimState != null;
+    }
+
+    boolean isDimming() {
+        return mDimState != null && mDimState.isDimming();
+    }
+
     @NonNull
     private DimState obtainDimState(@NonNull WindowState window) {
         if (mDimState == null) {
@@ -276,7 +284,6 @@
         return mDimState != null ? mDimState.mDimSurface : null;
     }
 
-    @Deprecated
     Rect getDimBounds() {
         return mDimState != null ? mDimState.mDimBounds : null;
     }
diff --git a/services/core/java/com/android/server/wm/DimmerAnimationHelper.java b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
index 3999e03..0d0e548 100644
--- a/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
+++ b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
@@ -27,11 +27,13 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.Rect;
 import android.util.Log;
 import android.util.proto.ProtoOutputStream;
 import android.view.SurfaceControl;
 
 import com.android.internal.protolog.ProtoLog;
+import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
 
@@ -153,6 +155,9 @@
                         ? mRequestedProperties.mGeometryParent.getSurfaceControl() : null,
                 mRequestedProperties.mDimmingContainer != startProperties.mDimmingContainer
                         ? mRequestedProperties.mDimmingContainer.getSurfaceControl() : null, t);
+        if (Flags.useTasksDimOnly()) {
+            setBounds(dim, mCurrentProperties.mDimmingContainer, t);
+        }
 
         if (!startProperties.hasSameVisualProperties(mRequestedProperties)) {
             stopCurrentAnimation(dim.mDimSurface);
@@ -253,6 +258,32 @@
         }
     }
 
+    static void setBounds(@NonNull Dimmer.DimState dim, @NonNull WindowState relativeParent,
+                          @NonNull SurfaceControl.Transaction t) {
+        TaskFragment taskFragment = relativeParent.getTaskFragment();
+        Rect taskFragmentBounds = taskFragment != null ? taskFragment.getBounds() : null;
+        Task task = relativeParent.getTask();
+        Rect taskBounds = task != null ? task.getBounds() : null;
+        Rect hostBounds = dim.mHostContainer.getBounds();
+        boolean isEmbedded = taskFragment != null && taskFragment.isEmbedded();
+
+        Rect relativeBounds = new Rect();
+        if (isEmbedded) {
+            // Embedded activities can be dimmed at task or fragment level
+            dim.mDimBounds.set(taskFragment.isDimmingOnParentTask()
+                    ? taskBounds : taskFragmentBounds);
+            relativeBounds.set(dim.mDimBounds);
+            relativeBounds.offset(-taskBounds.left, -taskBounds.top);
+        } else {
+            dim.mDimBounds.set(hostBounds);
+            relativeBounds.set(dim.mDimBounds);
+            relativeBounds.offsetTo(0, 0);
+        }
+
+        t.setWindowCrop(dim.mDimSurface, relativeBounds.width(), relativeBounds.height());
+        t.setPosition(dim.mDimSurface, relativeBounds.left, relativeBounds.top);
+    }
+
     void setCurrentAlphaBlur(@NonNull Dimmer.DimState dim, @NonNull SurfaceControl.Transaction t) {
         final SurfaceControl sc = dim.mDimSurface;
         try {
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index 29ffda7..3b24798 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -46,6 +46,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.ProtoLog;
 import com.android.server.policy.WindowManagerPolicy;
+import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
 import java.util.Comparator;
@@ -53,6 +54,7 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
+
 /**
  * Container for grouping WindowContainer below DisplayContent.
  *
@@ -831,11 +833,14 @@
         void prepareSurfaces() {
             mDimmer.resetDimStates();
             super.prepareSurfaces();
-            final Rect dimBounds = mDimmer.getDimBounds();
-            if (dimBounds != null) {
-                // Bounds need to be relative, as the dim layer is a child.
-                getBounds(dimBounds);
-                dimBounds.offsetTo(0 /* newLeft */, 0 /* newTop */);
+            Rect dimBounds = null;
+            if (!Flags.useTasksDimOnly()) {
+                dimBounds = mDimmer.getDimBounds();
+                if (dimBounds != null) {
+                    // Bounds need to be relative, as the dim layer is a child.
+                    getBounds(dimBounds);
+                    dimBounds.offsetTo(0 /* newLeft */, 0 /* newTop */);
+                }
             }
 
             // If SystemUI is dragging for recents, we want to reset the dim state so any dim layer
@@ -845,7 +850,7 @@
                 mDimmer.resetDimStates();
             }
 
-            if (dimBounds != null) {
+            if (mDimmer.hasDimState()) {
                 if (mDimmer.updateDims(getSyncTransaction())) {
                     scheduleAnimation();
                 }
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 5b2fecd..c7d57fe 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -17,7 +17,6 @@
 package com.android.server.wm;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR;
 import static android.view.Display.TYPE_INTERNAL;
 import static android.view.InsetsFrameProvider.SOURCE_ARBITRARY_RECTANGLE;
 import static android.view.InsetsFrameProvider.SOURCE_CONTAINER_BOUNDS;
@@ -1248,17 +1247,7 @@
                 throw new IllegalArgumentException("IME insets must be provided by a window.");
             }
 
-            if (!ENABLE_HIDE_IME_CAPTION_BAR && mNavigationBar != null && mHasBottomNavigationBar) {
-                // In gesture navigation, nav bar frame is larger than frame to calculate insets.
-                // IME should not provide frame which is smaller than the nav bar frame. Otherwise,
-                // nav bar might be overlapped with the content of the client when IME is shown.
-                sTmpRect.set(inOutFrame);
-                sTmpRect.intersectUnchecked(mNavigationBar.getFrame());
-                inOutFrame.inset(windowState.mGivenContentInsets);
-                inOutFrame.union(sTmpRect);
-            } else {
-                inOutFrame.inset(windowState.mGivenContentInsets);
-            }
+            inOutFrame.inset(windowState.mGivenContentInsets);
             return 0;
         };
     }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 8a624b3..b14dd3f 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3297,22 +3297,25 @@
         mDimmer.resetDimStates();
         super.prepareSurfaces();
 
-        final Rect dimBounds = mDimmer.getDimBounds();
-        if (dimBounds != null) {
-            getDimBounds(dimBounds);
+        Rect dimBounds = null;
+        if (!Flags.useTasksDimOnly()) {
+            dimBounds = mDimmer.getDimBounds();
+            if (dimBounds != null) {
+                getDimBounds(dimBounds);
 
-            // Bounds need to be relative, as the dim layer is a child.
-            if (inFreeformWindowingMode()) {
-                getBounds(mTmpRect);
-                dimBounds.offsetTo(dimBounds.left - mTmpRect.left, dimBounds.top - mTmpRect.top);
-            } else {
-                dimBounds.offsetTo(0, 0);
+                // Bounds need to be relative, as the dim layer is a child.
+                if (inFreeformWindowingMode()) {
+                    getBounds(mTmpRect);
+                    dimBounds.offset(-mTmpRect.left, -mTmpRect.top);
+                } else {
+                    dimBounds.offsetTo(0, 0);
+                }
             }
         }
 
         final SurfaceControl.Transaction t = getSyncTransaction();
 
-        if (dimBounds != null && mDimmer.updateDims(t)) {
+        if (mDimmer.hasDimState() && mDimmer.updateDims(t)) {
             scheduleAnimation();
         }
 
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index d6ba312..606d51d 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -3157,12 +3157,16 @@
 
     /** Bounds to be used for dimming, as well as touch related tests. */
     void getDimBounds(@NonNull Rect out) {
-        if (mIsEmbedded && isDimmingOnParentTask() && getDimmer().getDimBounds() != null) {
-            // Return the task bounds if the dimmer is showing and should cover on the Task (not
-            // just on this embedded TaskFragment).
-            out.set(getTask().getBounds());
+        if (Flags.useTasksDimOnly() && mDimmer.hasDimState()) {
+            out.set(mDimmer.getDimBounds());
         } else {
-            out.set(getBounds());
+            if (mIsEmbedded && isDimmingOnParentTask() && getDimmer().getDimBounds() != null) {
+                // Return the task bounds if the dimmer is showing and should cover on the Task (not
+                // just on this embedded TaskFragment).
+                out.set(getTask().getBounds());
+            } else {
+                out.set(getBounds());
+            }
         }
     }
 
@@ -3193,10 +3197,16 @@
         mDimmer.resetDimStates();
         super.prepareSurfaces();
 
-        final Rect dimBounds = mDimmer.getDimBounds();
-        if (dimBounds != null) {
-            // Bounds need to be relative, as the dim layer is a child.
-            dimBounds.offsetTo(0 /* newLeft */, 0 /* newTop */);
+        if (!Flags.useTasksDimOnly()) {
+            final Rect dimBounds = mDimmer.getDimBounds();
+            if (dimBounds != null) {
+                // Bounds need to be relative, as the dim layer is a child.
+                dimBounds.offsetTo(0 /* newLeft */, 0 /* newTop */);
+                if (mDimmer.updateDims(getSyncTransaction())) {
+                    scheduleAnimation();
+                }
+            }
+        } else {
             if (mDimmer.updateDims(getSyncTransaction())) {
                 scheduleAnimation();
             }
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 585537b..fc46600 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -557,6 +557,14 @@
         });
     }
 
+    /** Set a transition to be a back gesture animation. */
+    void setBackGestureAnimation(@NonNull WindowContainer wc, boolean isTop) {
+        final ChangeInfo info = mChanges.get(wc);
+        if (info == null) return;
+        info.mFlags = info.mFlags | (isTop ? ChangeInfo.FLAG_BACK_GESTURE_ANIMATION
+                : ChangeInfo.FLAG_BELOW_BACK_GESTURE_ANIMATION);
+    }
+
     /** Set a transition to be a seamless-rotation. */
     void setSeamlessRotation(@NonNull WindowContainer wc) {
         final ChangeInfo info = mChanges.get(wc);
@@ -3405,6 +3413,18 @@
         /** Whether this change contains config-at-end members. */
         private static final int FLAG_CHANGE_CONFIG_AT_END = 0x40;
 
+        /**
+         * Whether this change is forced participant transition because it is current top target
+         * of predictive back animation.
+         */
+        private static final int FLAG_BACK_GESTURE_ANIMATION = 0x80;
+
+        /**
+         * Whether this change is forced participant transition because it is previous target of
+         * predictive back animation
+         */
+        private static final int FLAG_BELOW_BACK_GESTURE_ANIMATION = 0x100;
+
         @IntDef(prefix = { "FLAG_" }, value = {
                 FLAG_NONE,
                 FLAG_SEAMLESS_ROTATION,
@@ -3413,7 +3433,9 @@
                 FLAG_CHANGE_NO_ANIMATION,
                 FLAG_CHANGE_YES_ANIMATION,
                 FLAG_CHANGE_MOVED_TO_TOP,
-                FLAG_CHANGE_CONFIG_AT_END
+                FLAG_CHANGE_CONFIG_AT_END,
+                FLAG_BACK_GESTURE_ANIMATION,
+                FLAG_BELOW_BACK_GESTURE_ANIMATION
         })
         @Retention(RetentionPolicy.SOURCE)
         @interface Flag {}
@@ -3487,7 +3509,9 @@
             final boolean currVisible = mContainer.isVisibleRequested();
             // the task including transient launch must promote to root task
             if (currVisible && ((mFlags & ChangeInfo.FLAG_TRANSIENT_LAUNCH) != 0
-                    || (mFlags & ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH) != 0)) {
+                    || (mFlags & ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH) != 0)
+                    || (mFlags & ChangeInfo.FLAG_BACK_GESTURE_ANIMATION) != 0
+                    || (mFlags & ChangeInfo.FLAG_BELOW_BACK_GESTURE_ANIMATION) != 0) {
                 return true;
             }
             // If it's invisible and hasn't changed visibility, always return false since even if
@@ -3509,6 +3533,9 @@
             if ((mFlags & ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH) != 0) {
                 return mExistenceChanged ? TRANSIT_CLOSE : TRANSIT_TO_BACK;
             }
+            if ((mFlags & ChangeInfo.FLAG_BELOW_BACK_GESTURE_ANIMATION) != 0) {
+                return TRANSIT_TO_FRONT;
+            }
             final boolean nowVisible = wc.isVisibleRequested();
             if (nowVisible == mVisible) {
                 return TRANSIT_CHANGE;
@@ -3540,7 +3567,8 @@
                             && topActivity.mStartingData.hasImeSurface()) {
                         flags |= FLAG_WILL_IME_SHOWN;
                     }
-                    if (topActivity.mLaunchTaskBehind) {
+                    if (topActivity.mLaunchTaskBehind
+                            && !topActivity.isAnimating(PARENTS, ANIMATION_TYPE_PREDICT_BACK)) {
                         Slog.e(TAG, "Unexpected launch-task-behind operation in shell transition");
                         flags |= FLAG_TASK_LAUNCHING_BEHIND;
                     }
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 7eb0c42..3805c02 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -191,6 +191,7 @@
 import com.android.server.media.MediaSessionService;
 import com.android.server.media.metrics.MediaMetricsManagerService;
 import com.android.server.media.projection.MediaProjectionManagerService;
+import com.android.server.media.quality.MediaQualityService;
 import com.android.server.midi.MidiService;
 import com.android.server.musicrecognition.MusicRecognitionManagerService;
 import com.android.server.net.NetworkManagementService;
@@ -2587,6 +2588,10 @@
                 t.traceEnd();
             }
 
+            t.traceBegin("StartMediaQuality");
+            mSystemServiceManager.startService(MediaQualityService.class);
+            t.traceEnd();
+
             if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
                 t.traceBegin("StartMediaResourceMonitor");
                 mSystemServiceManager.startService(MediaResourceMonitorService.class);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java
index 59302ee..2988c77 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java
@@ -22,6 +22,7 @@
 import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
 import static android.app.ActivityManager.PROCESS_STATE_HOME;
 import static android.app.ActivityManager.PROCESS_STATE_SERVICE;
+import static android.content.Context.BIND_ALLOW_OOM_MANAGEMENT;
 import static android.content.Context.BIND_AUTO_CREATE;
 import static android.content.Context.BIND_INCLUDE_CAPABILITIES;
 import static android.content.Context.BIND_WAIVE_PRIORITY;
@@ -29,6 +30,7 @@
 import static android.os.UserHandle.USER_SYSTEM;
 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
 
+import static com.android.server.am.ProcessCachedOptimizerRecord.SHOULD_NOT_FREEZE_REASON_NONE;
 import static com.android.server.am.ProcessList.CACHED_APP_MIN_ADJ;
 import static com.android.server.am.ProcessList.HOME_APP_ADJ;
 import static com.android.server.am.ProcessList.PERCEPTIBLE_APP_ADJ;
@@ -63,6 +65,10 @@
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -114,6 +120,8 @@
     public final ServiceThreadRule mServiceThreadRule = new ServiceThreadRule();
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     private Context mContext;
     private HandlerThread mHandlerThread;
@@ -317,6 +325,256 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(com.android.server.am.Flags.FLAG_UNFREEZE_BIND_POLICY_FIX)
+    public void testServiceDistinctBindingOomAdjShouldNotFreeze() throws Exception {
+        // Enable the flags.
+        mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY);
+
+        // Verify that there should be at least 1 oom adj update
+        // because the shouldNotFreeze state needs to be propagated.
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
+                PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                (app) -> {
+                    this.setHasForegroundServices(app);
+                    this.setAllowListed(app);
+                },
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME,
+                HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                this::setHomeProcess,
+                BIND_AUTO_CREATE,
+                atLeastOnce(), atLeastOnce());
+
+        // Verify that there should be at least 1 oom adj update
+        // because the shouldNotFreeze state needs to be propagated.
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_HOME, HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                (app) -> {
+                    this.setHomeProcess(app);
+                    this.setAllowListed(app);
+                },
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE,
+                PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                this::setHasForegroundServices,
+                BIND_AUTO_CREATE,
+                atLeastOnce(), atLeastOnce());
+
+        // Verify that there should be at least 1 oom adj update
+        // because the client is more important (regardless of shouldNotFreeze state).
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
+                PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                (app) -> {
+                    this.setHasForegroundServices(app);
+                    this.setAllowListed(app);
+                },
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME,
+                HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                (app) -> {
+                    this.setHomeProcess(app);
+                    this.setAllowListed(app);
+                },
+                BIND_AUTO_CREATE,
+                atLeastOnce(), atLeastOnce());
+
+        // Verify that there should be 0 oom adj update for binding
+        // because setShouldNotFreeze is already set
+        // but for the unbinding must update in case the binding could be the source of the
+        // shouldNotFreeze.
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_HOME, HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                (app) -> {
+                    this.setHomeProcess(app);
+                    this.setAllowListed(app);
+                },
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE,
+                PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                (app) -> {
+                    this.setHasForegroundServices(app);
+                    this.setAllowListed(app);
+                },
+                BIND_AUTO_CREATE,
+                never(), atLeastOnce());
+
+
+        // Disable the flags.
+        mSetFlagsRule.disableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY);
+
+        // Verify that there should be at least 1 oom adj update
+        // because the client is more important.
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
+                PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                (app) -> {
+                    this.setHasForegroundServices(app);
+                    this.setAllowListed(app);
+                },
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME,
+                HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                this::setHomeProcess,
+                BIND_AUTO_CREATE,
+                atLeastOnce(), atLeastOnce());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(com.android.server.am.Flags.FLAG_UNFREEZE_BIND_POLICY_FIX)
+    public void testServiceDistinctBindingOomAdjAllowOomManagement() throws Exception {
+        // Enable the flags.
+        mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY);
+
+        // Verify that there should be at least 1 oom adj update
+        // because BIND_ALLOW_OOM_MANAGEMENT sets the shouldNotFreeze state which needs to be
+        // propagated.
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
+                PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                this::setHasForegroundServices,
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME,
+                HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                this::setHomeProcess,
+                BIND_AUTO_CREATE | BIND_ALLOW_OOM_MANAGEMENT,
+                atLeastOnce(), atLeastOnce());
+
+        // Verify that there should be at least 1 oom adj update
+        // because BIND_ALLOW_OOM_MANAGEMENT sets the shouldNotFreeze state which needs to be
+        // propagated.
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_HOME, HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                this::setHomeProcess,
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE,
+                PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                this::setHasForegroundServices,
+                BIND_AUTO_CREATE | BIND_ALLOW_OOM_MANAGEMENT,
+                atLeastOnce(), atLeastOnce());
+
+        // Verify that there should be at least 1 oom adj update
+        // because the client is more important (regardless of BIND_ALLOW_OOM_MANAGEMENT).
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
+                PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                this::setHasForegroundServices,
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME,
+                HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                (app) -> {
+                    this.setHomeProcess(app);
+                    this.setAllowListed(app);
+                },
+                BIND_AUTO_CREATE | BIND_ALLOW_OOM_MANAGEMENT,
+                atLeastOnce(), atLeastOnce());
+
+        // Verify that there should be 0 oom adj update for binding
+        // because setShouldNotFreeze is already set
+        // but for the unbinding must update in case the BIND_ALLOW_OOM_MANAGEMENT maintaining the
+        // shouldNotFreeze.
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_HOME, HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                this::setHomeProcess,
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE,
+                PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                (app) -> {
+                    this.setHasForegroundServices(app);
+                    this.setAllowListed(app);
+                },
+                BIND_AUTO_CREATE | BIND_ALLOW_OOM_MANAGEMENT,
+                never(), atLeastOnce());
+
+
+        // Disable the flags.
+        mSetFlagsRule.disableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY);
+
+        // Verify that there should be at least 1 oom adj update
+        // because the client is more important.
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
+                PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                this::setHasForegroundServices,
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME,
+                HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                this::setHomeProcess,
+                BIND_AUTO_CREATE | BIND_ALLOW_OOM_MANAGEMENT,
+                atLeastOnce(), atLeastOnce());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(com.android.server.am.Flags.FLAG_UNFREEZE_BIND_POLICY_FIX)
+    public void testServiceDistinctBindingOomAdjWaivePriority_propagateUnfreeze() throws Exception {
+        // Enable the flags.
+        mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY);
+
+        // Verify that there should be at least 1 oom adj update
+        // because BIND_WAIVE_PRIORITY sets the shouldNotFreeze state which needs to be propagated.
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
+                PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                this::setHasForegroundServices,
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME,
+                HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                this::setHomeProcess,
+                BIND_AUTO_CREATE | BIND_WAIVE_PRIORITY,
+                atLeastOnce(), atLeastOnce());
+
+        // Verify that there should be at least 1 oom adj update
+        // because BIND_WAIVE_PRIORITY sets the shouldNotFreeze state which needs to be propagated.
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_HOME, HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                this::setHomeProcess,
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE,
+                PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                this::setHasForegroundServices,
+                BIND_AUTO_CREATE | BIND_WAIVE_PRIORITY,
+                atLeastOnce(), atLeastOnce());
+
+        // Verify that there should be 0 oom adj update for binding
+        // because setShouldNotFreeze is already set
+        // but for the unbinding, because client is better than service, we can't skip it safely.
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
+                PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                this::setHasForegroundServices,
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME,
+                HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                (app) -> {
+                    this.setHomeProcess(app);
+                    this.setAllowListed(app);
+                },
+                BIND_AUTO_CREATE | BIND_WAIVE_PRIORITY,
+                never(), atLeastOnce());
+
+        // Verify that there should be 0 oom adj update for binding
+        // because setShouldNotFreeze is already set
+        // but for the unbinding must update in case the BIND_WAIVE_PRIORITY maintaining the
+        // shouldNotFreeze.
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_HOME, HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                this::setHomeProcess,
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_FOREGROUND_SERVICE,
+                PERCEPTIBLE_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                (app) -> {
+                    this.setHasForegroundServices(app);
+                    this.setAllowListed(app);
+                },
+                BIND_AUTO_CREATE | BIND_WAIVE_PRIORITY,
+                never(), atLeastOnce());
+
+
+        // Disable the flags.
+        mSetFlagsRule.disableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY);
+
+        // Verify that there should be at least 1 oom adj update
+        // because the client is more important.
+        performTestServiceDistinctBindingOomAdj(TEST_APP1_PID, TEST_APP1_UID,
+                PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
+                PROCESS_CAPABILITY_NONE, TEST_APP1_NAME,
+                this::setHasForegroundServices,
+                TEST_APP2_PID, TEST_APP2_UID, PROCESS_STATE_HOME,
+                HOME_APP_ADJ, PROCESS_CAPABILITY_NONE, TEST_APP2_NAME, TEST_SERVICE2_NAME,
+                this::setHomeProcess,
+                BIND_AUTO_CREATE | BIND_WAIVE_PRIORITY,
+                atLeastOnce(), atLeastOnce());
+    }
+
+    @Test
+    @RequiresFlagsDisabled(com.android.server.am.Flags.FLAG_UNFREEZE_BIND_POLICY_FIX)
     public void testServiceDistinctBindingOomAdjWaivePriority() throws Exception {
         // Enable the flags.
         mSetFlagsRule.enableFlags(Flags.FLAG_SERVICE_BINDING_OOM_ADJ_POLICY);
@@ -527,6 +785,15 @@
     }
 
     @SuppressWarnings("GuardedBy")
+    private void setAllowListed(ProcessRecord app) {
+        final UidRecord uidRec = mock(UidRecord.class);
+        app.setUidRecord(uidRec);
+        doReturn(true).when(uidRec).isCurAllowListed();
+
+        app.mOptRecord.setShouldNotFreeze(true, SHOULD_NOT_FREEZE_REASON_NONE, 1234);
+    }
+
+    @SuppressWarnings("GuardedBy")
     private ProcessRecord addProcessRecord(int pid, int uid, int procState, int adj, int cap,
                 String packageName) {
         final ApplicationThreadDeferred appThread = mock(ApplicationThreadDeferred.class);
@@ -574,8 +841,10 @@
             String processName, String packageName, ActivityManagerService ams) {
         final ProcessRecord app = ApplicationExitInfoTest.makeProcessRecord(pid, uid, packageUid,
                 definingUid, connectionGroup, procState, pss, rss, processName, packageName, ams);
+        app.mState.setCurRawProcState(procState);
         app.mState.setCurProcState(procState);
         app.mState.setSetProcState(procState);
+        app.mState.setCurRawAdj(adj);
         app.mState.setCurAdj(adj);
         app.mState.setSetAdj(adj);
         app.mState.setCurCapability(cap);
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index c099517..1ea3674 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -40,7 +40,6 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -55,6 +54,7 @@
 import android.app.Flags;
 import android.app.WallpaperColors;
 import android.app.WallpaperManager;
+import android.app.wallpaper.WallpaperDescription;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -65,6 +65,7 @@
 import android.graphics.Color;
 import android.hardware.display.DisplayManager;
 import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.platform.test.annotations.DisableFlags;
@@ -426,7 +427,8 @@
 
     @Test
     @EnableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT)
-    public void testSaveLoadSettings() {
+    public void testSaveLoadSettings_withoutWallpaperDescription()
+            throws IOException, XmlPullParserException {
         WallpaperData expectedData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0);
         expectedData.setComponent(sDefaultWallpaperComponent);
         expectedData.primaryColors = new WallpaperColors(Color.valueOf(Color.RED),
@@ -436,27 +438,19 @@
         expectedData.mUidToDimAmount.put(1, 0.4f);
 
         ByteArrayOutputStream ostream = new ByteArrayOutputStream();
-        try {
-            TypedXmlSerializer serializer = Xml.newBinarySerializer();
-            serializer.setOutput(ostream, StandardCharsets.UTF_8.name());
-            mService.mWallpaperDataParser.saveSettingsToSerializer(serializer, expectedData, null);
-            ostream.close();
-        } catch (IOException e) {
-            fail("exception occurred while writing system wallpaper attributes");
-        }
+        TypedXmlSerializer serializer = Xml.newBinarySerializer();
+        serializer.setOutput(ostream, StandardCharsets.UTF_8.name());
+        mService.mWallpaperDataParser.saveSettingsToSerializer(serializer, expectedData, null);
+        ostream.close();
 
         WallpaperData actualData = new WallpaperData(0, FLAG_SYSTEM);
-        try {
-            ByteArrayInputStream istream = new ByteArrayInputStream(ostream.toByteArray());
-            TypedXmlPullParser parser = Xml.newBinaryPullParser();
-            parser.setInput(istream, StandardCharsets.UTF_8.name());
-            mService.mWallpaperDataParser.loadSettingsFromSerializer(parser,
-                    actualData, /* userId= */0, /* loadSystem= */ true, /* loadLock= */
-                    false, /* keepDimensionHints= */ true,
-                    new WallpaperDisplayHelper.DisplayData(0));
-        } catch (IOException | XmlPullParserException e) {
-            fail("exception occurred while parsing wallpaper");
-        }
+        ByteArrayInputStream istream = new ByteArrayInputStream(ostream.toByteArray());
+        TypedXmlPullParser parser = Xml.newBinaryPullParser();
+        parser.setInput(istream, StandardCharsets.UTF_8.name());
+        mService.mWallpaperDataParser.loadSettingsFromSerializer(parser,
+                actualData, /* userId= */0, /* loadSystem= */ true, /* loadLock= */
+                false, /* keepDimensionHints= */ true,
+                new WallpaperDisplayHelper.DisplayData(0));
 
         assertThat(actualData.getComponent()).isEqualTo(expectedData.getComponent());
         assertThat(actualData.primaryColors).isEqualTo(expectedData.primaryColors);
@@ -472,33 +466,58 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT)
+    public void testSaveLoadSettings_withWallpaperDescription()
+            throws IOException, XmlPullParserException {
+        WallpaperData expectedData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0);
+        expectedData.setComponent(sDefaultWallpaperComponent);
+        PersistableBundle content = new PersistableBundle();
+        content.putString("ckey", "cvalue");
+        WallpaperDescription description = new WallpaperDescription.Builder()
+                .setComponent(sDefaultWallpaperComponent).setId("testId").setTitle("fake one")
+                .setContent(content).build();
+        expectedData.setDescription(description);
+
+        ByteArrayOutputStream ostream = new ByteArrayOutputStream();
+        TypedXmlSerializer serializer = Xml.newBinarySerializer();
+        serializer.setOutput(ostream, StandardCharsets.UTF_8.name());
+        mService.mWallpaperDataParser.saveSettingsToSerializer(serializer, expectedData, null);
+        ostream.close();
+
+        WallpaperData actualData = new WallpaperData(0, FLAG_SYSTEM);
+        ByteArrayInputStream istream = new ByteArrayInputStream(ostream.toByteArray());
+        TypedXmlPullParser parser = Xml.newBinaryPullParser();
+        parser.setInput(istream, StandardCharsets.UTF_8.name());
+        mService.mWallpaperDataParser.loadSettingsFromSerializer(parser,
+                actualData, /* userId= */0, /* loadSystem= */ true, /* loadLock= */
+                false, /* keepDimensionHints= */ true,
+                new WallpaperDisplayHelper.DisplayData(0));
+
+        assertThat(actualData.getComponent()).isEqualTo(expectedData.getComponent());
+        assertThat(actualData.getDescription()).isEqualTo(expectedData.getDescription());
+    }
+
+    @Test
     @DisableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT)
-    public void testSaveLoadSettings_legacyNextComponent() {
+    public void testSaveLoadSettings_legacyNextComponent()
+            throws IOException, XmlPullParserException {
         WallpaperData systemWallpaperData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0);
         systemWallpaperData.setComponent(sDefaultWallpaperComponent);
         ByteArrayOutputStream ostream = new ByteArrayOutputStream();
-        try {
-            TypedXmlSerializer serializer = Xml.newBinarySerializer();
-            serializer.setOutput(ostream, StandardCharsets.UTF_8.name());
-            mService.mWallpaperDataParser.saveSettingsToSerializer(serializer, systemWallpaperData,
-                    null);
-            ostream.close();
-        } catch (IOException e) {
-            fail("exception occurred while writing system wallpaper attributes");
-        }
+        TypedXmlSerializer serializer = Xml.newBinarySerializer();
+        serializer.setOutput(ostream, StandardCharsets.UTF_8.name());
+        mService.mWallpaperDataParser.saveSettingsToSerializer(serializer, systemWallpaperData,
+                null);
+        ostream.close();
 
         WallpaperData shouldMatchSystem = new WallpaperData(0, FLAG_SYSTEM);
-        try {
-            ByteArrayInputStream istream = new ByteArrayInputStream(ostream.toByteArray());
-            TypedXmlPullParser parser = Xml.newBinaryPullParser();
-            parser.setInput(istream, StandardCharsets.UTF_8.name());
-            mService.mWallpaperDataParser.loadSettingsFromSerializer(parser,
-                    shouldMatchSystem, /* userId= */0, /* loadSystem= */ true, /* loadLock= */
-                    false, /* keepDimensionHints= */ true,
-                    new WallpaperDisplayHelper.DisplayData(0));
-        } catch (IOException | XmlPullParserException e) {
-            fail("exception occurred while parsing wallpaper");
-        }
+        ByteArrayInputStream istream = new ByteArrayInputStream(ostream.toByteArray());
+        TypedXmlPullParser parser = Xml.newBinaryPullParser();
+        parser.setInput(istream, StandardCharsets.UTF_8.name());
+        mService.mWallpaperDataParser.loadSettingsFromSerializer(parser,
+                shouldMatchSystem, /* userId= */0, /* loadSystem= */ true, /* loadLock= */
+                false, /* keepDimensionHints= */ true,
+                new WallpaperDisplayHelper.DisplayData(0));
 
         assertThat(shouldMatchSystem.nextWallpaperComponent).isEqualTo(
                 systemWallpaperData.getComponent());
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
index 8f23ab9..d7bfea8 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.biometrics.log;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.anyFloat;
@@ -45,10 +47,14 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
 @Presubmit
 @SmallTest
 public class BiometricLoggerTest {
@@ -136,12 +142,53 @@
         final int targetUserId = 4;
         final long latency = 44;
         final boolean enrollSuccessful = true;
+        final int templateId = 4;
 
-        mLogger.logOnEnrolled(targetUserId, latency, enrollSuccessful, -1);
+        mLogger.logOnEnrolled(targetUserId, latency, enrollSuccessful, -1, templateId);
 
         verify(mSink).enroll(
                 eq(DEFAULT_MODALITY), eq(DEFAULT_ACTION), eq(DEFAULT_CLIENT),
-                eq(targetUserId), eq(latency), eq(enrollSuccessful), anyFloat(), anyInt());
+                eq(targetUserId), eq(latency), eq(enrollSuccessful), anyFloat(), anyInt(),
+                eq(templateId));
+    }
+
+    @Test
+    public void testUnEnroll() {
+        mLogger = createLogger();
+
+        final int targetUserId = 4;
+        final int reason = BiometricsProtoEnums.UNENROLL_REASON_DANGLING_FRAMEWORK;
+        final int templateId = 4;
+
+        mLogger.logOnUnEnrolled(targetUserId, reason, templateId);
+
+        verify(mSink).unenrolled(
+                eq(DEFAULT_MODALITY), eq(targetUserId), eq(reason), eq(templateId));
+    }
+
+    @Test
+    public void testEnumeration() {
+        mLogger = createLogger();
+
+        final int targetUserId = 4;
+        final int result = BiometricsProtoEnums.ENUMERATION_RESULT_OK;
+        final int[] templateIdsHal = {1, 2};
+        final int[] templateIdsFw = {2, 3};
+        mLogger.logOnEnumerated(targetUserId, result, templateIdsHal, templateIdsFw);
+
+        final ArgumentCaptor<int[]> captorHalIds = ArgumentCaptor.forClass(int[].class);
+        final ArgumentCaptor<int[]> captorFwIds = ArgumentCaptor.forClass(int[].class);
+        verify(mSink).enumerated(
+                eq(DEFAULT_MODALITY), eq(targetUserId), eq(result), captorHalIds.capture(),
+                captorFwIds.capture());
+        assertThat(captorHalIds.getAllValues().stream()
+                .flatMap(x -> Arrays.stream(x).boxed())
+                .collect(Collectors.toList()))
+                .containsExactly(1, 2);
+        assertThat(captorFwIds.getAllValues().stream()
+                .flatMap(x -> Arrays.stream(x).boxed())
+                .collect(Collectors.toList()))
+                .containsExactly(2, 3);
     }
 
     @Test
@@ -193,7 +240,8 @@
         mLogger.logOnEnrolled(2 /* targetUserId */,
                 10 /* latency */,
                 true /* enrollSuccessful */,
-                30 /* source */);
+                30 /* source */,
+                1 /* templateId */);
         mLogger.logOnError(mContext, mOpContext,
                 4 /* error */,
                 0 /* vendorCode */,
@@ -207,7 +255,7 @@
                 anyLong(), anyInt(), anyBoolean(), anyInt(), anyFloat());
         verify(mSink, never()).enroll(
                 anyInt(), anyInt(), anyInt(), anyInt(), anyLong(), anyBoolean(), anyFloat(),
-                anyInt());
+                anyInt(), anyInt());
         verify(mSink, never()).error(eq(mOpContext),
                 anyInt(), anyInt(), anyInt(), anyBoolean(),
                 anyLong(), anyInt(), anyInt(), anyInt());
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
index 90c07d4..fd8c792 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
@@ -37,6 +37,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.mockito.kotlin.StubberKt.doReturn;
 
 import android.content.Context;
 import android.hardware.biometrics.AuthenticateOptions;
@@ -810,14 +811,16 @@
         final ClientMonitorCallback callback0 = mock(ClientMonitorCallback.class);
         final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
         final ClientMonitorCallback callback2 = mock(ClientMonitorCallback.class);
+        final BiometricLogger logger = mock(BiometricLogger.class);
+        doReturn(logger).when(logger).swapAction(any(), anyInt());
 
         final TestInternalCleanupClient client1 = new
                 TestInternalCleanupClient(mContext, daemon, userId,
-                owner, TEST_SENSOR_ID, mock(BiometricLogger.class),
+                owner, TEST_SENSOR_ID, logger,
                 mBiometricContext, utils, authenticatorIds);
         final TestInternalCleanupClient client2 = new
                 TestInternalCleanupClient(mContext, daemon, userId,
-                owner, TEST_SENSOR_ID, mock(BiometricLogger.class),
+                owner, TEST_SENSOR_ID, logger,
                 mBiometricContext, utils, authenticatorIds);
 
         //add initial start client to scheduler, so later clients will be on pending operation queue
@@ -1248,9 +1251,9 @@
                 @Nullable ClientMonitorCallbackConverter listener, int[] biometricIds, int userId,
                 @NonNull String owner, @NonNull BiometricUtils<Fingerprint> utils, int sensorId,
                 @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
-                @NonNull Map<Integer, Long> authenticatorIds) {
+                @NonNull Map<Integer, Long> authenticatorIds, int reason) {
             super(context, lazyDaemon, token, listener, userId, owner, utils, sensorId,
-                    logger, biometricContext, authenticatorIds);
+                    logger, biometricContext, authenticatorIds, reason);
         }
 
         @Override
@@ -1289,10 +1292,10 @@
                 Supplier<Object> lazyDaemon, IBinder token, int biometricId, int userId,
                 String owner, BiometricUtils<Fingerprint> utils, int sensorId,
                 @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
-                Map<Integer, Long> authenticatorIds) {
+                Map<Integer, Long> authenticatorIds, int reason) {
             return new TestRemovalClient(context, lazyDaemon, token, null,
                     new int[]{biometricId}, userId, owner, utils, sensorId, logger,
-                    biometricContext, authenticatorIds);
+                    biometricContext, authenticatorIds, reason);
         }
 
         @Override
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java
index 6ac95c8..276da39 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java
@@ -197,7 +197,7 @@
         client.onEnrollResult(new Face("face", 1 /* faceId */, 20 /* deviceId */), 0);
 
         verify(mBiometricLogger).logOnEnrolled(anyInt(), anyLong(), anyBoolean(),
-                eq(BiometricsProtoEnums.ENROLLMENT_SOURCE_SUW));
+                eq(BiometricsProtoEnums.ENROLLMENT_SOURCE_SUW), eq(1));
     }
 
     private FaceEnrollClient createClient() throws RemoteException {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java
index d8bdd50..734ee16 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -28,6 +29,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.face.ISession;
 import android.hardware.face.Face;
 import android.os.IBinder;
@@ -44,12 +46,15 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Collectors;
 
 @Presubmit
 @SmallTest
@@ -104,6 +109,8 @@
             mClient.onEnumerationResult(mFace, 0);
             return null;
         }).when(mSession).enumerateEnrollments();
+        final ArgumentCaptor<int[]> captorHalIds = ArgumentCaptor.forClass(int[].class);
+        final ArgumentCaptor<int[]> captorFwIds = ArgumentCaptor.forClass(int[].class);
 
         mClient.start(mCallback);
 
@@ -111,6 +118,17 @@
         assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
         assertThat(mNotificationSent).isFalse();
         verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
+        verify(mBiometricLogger).logOnEnumerated(eq(USER_ID),
+                eq(BiometricsProtoEnums.ENUMERATION_RESULT_OK), captorHalIds.capture(),
+                captorFwIds.capture());
+        assertThat(captorHalIds.getAllValues().stream()
+                .flatMap(x -> Arrays.stream(x).boxed())
+                .collect(Collectors.toList()))
+                .containsExactly(mBiometricId);
+        assertThat(captorFwIds.getAllValues().stream()
+                .flatMap(x -> Arrays.stream(x).boxed())
+                .collect(Collectors.toList()))
+                .containsExactly(mBiometricId);
         verify(mCallback).onClientFinished(mClient, true);
     }
 
@@ -171,6 +189,8 @@
             mClient.onEnumerationResult(identifier, 0);
             return null;
         }).when(mSession).enumerateEnrollments();
+        final ArgumentCaptor<int[]> captorHalIds = ArgumentCaptor.forClass(int[].class);
+        final ArgumentCaptor<int[]> captorFwIds = ArgumentCaptor.forClass(int[].class);
 
         mClient.start(mCallback);
 
@@ -178,6 +198,17 @@
         assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(1);
         assertThat(mNotificationSent).isTrue();
         verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, mBiometricId);
+        verify(mBiometricLogger).logOnEnumerated(eq(USER_ID),
+                eq(BiometricsProtoEnums.ENUMERATION_RESULT_DANGLING_BOTH), captorHalIds.capture(),
+                captorFwIds.capture());
+        assertThat(captorHalIds.getAllValues().stream()
+                .flatMap(x -> Arrays.stream(x).boxed())
+                .collect(Collectors.toList()))
+                .containsExactly(2);
+        assertThat(captorFwIds.getAllValues().stream()
+                .flatMap(x -> Arrays.stream(x).boxed())
+                .collect(Collectors.toList()))
+                .containsExactly(mBiometricId);
         verify(mCallback).onClientFinished(mClient, true);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceRemovalClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceRemovalClientTest.java
index 1d9e933..c224af2 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceRemovalClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceRemovalClientTest.java
@@ -25,6 +25,7 @@
 
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
+import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.face.ISession;
 import android.hardware.face.Face;
 import android.os.IBinder;
@@ -119,6 +120,6 @@
         return new FaceRemovalClient(mContext, () -> aidl, mToken,
                 mClientMonitorCallbackConverter, biometricIds, USER_ID,
                 "own-it", mUtils /* utils */, 5 /* sensorId */, mBiometricLogger, mBiometricContext,
-                mAuthenticatorIds);
+                mAuthenticatorIds, BiometricsProtoEnums.UNENROLL_REASON_UNKNOWN);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
index 5c6513d..ea96d19 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
@@ -309,7 +309,7 @@
         client.onEnrollResult(new Fingerprint("fingerprint", 1 /* faceId */, 20 /* deviceId */), 0);
 
         verify(mBiometricLogger).logOnEnrolled(anyInt(), anyLong(), anyBoolean(),
-                eq(BiometricsProtoEnums.ENROLLMENT_SOURCE_SUW));
+                eq(BiometricsProtoEnums.ENROLLMENT_SOURCE_SUW), eq(1));
     }
 
     private void showHideOverlay(
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java
index 7dcf841..a6604d1 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
@@ -99,6 +100,7 @@
         mAddedIds = new ArrayList<>();
 
         when(mAidlSession.getSession()).thenReturn(mSession);
+        doReturn(mLogger).when(mLogger).swapAction(any(), anyInt());
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java
index fab1200..a809c3b 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -28,6 +29,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.fingerprint.Fingerprint;
 import android.os.IBinder;
@@ -44,11 +46,13 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -108,6 +112,8 @@
             mClient.onEnumerationResult(new Fingerprint("three", 3, 1), 0);
             return null;
         }).when(mSession).enumerateEnrollments();
+        final ArgumentCaptor<int[]> captorHalIds = ArgumentCaptor.forClass(int[].class);
+        final ArgumentCaptor<int[]> captorFwIds = ArgumentCaptor.forClass(int[].class);
         mClient.start(mCallback);
 
         verify(mSession).enumerateEnrollments();
@@ -116,6 +122,17 @@
                 .collect(Collectors.toList())).containsExactly(2, 3);
         assertThat(mNotificationSent).isTrue();
         verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, 1);
+        verify(mBiometricLogger).logOnEnumerated(eq(USER_ID),
+                eq(BiometricsProtoEnums.ENUMERATION_RESULT_DANGLING_BOTH), captorHalIds.capture(),
+                captorFwIds.capture());
+        assertThat(captorHalIds.getAllValues().stream()
+                .flatMap(x -> Arrays.stream(x).boxed())
+                .collect(Collectors.toList()))
+                .containsExactly(2, 3);
+        assertThat(captorFwIds.getAllValues().stream()
+                .flatMap(x -> Arrays.stream(x).boxed())
+                .collect(Collectors.toList()))
+                .containsExactly(1);
         verify(mCallback).onClientFinished(mClient, true);
     }
 
@@ -125,12 +142,25 @@
             mClient.onEnumerationResult(new Fingerprint("one", 1, 1), 0);
             return null;
         }).when(mSession).enumerateEnrollments();
+        final ArgumentCaptor<int[]> captorHalIds = ArgumentCaptor.forClass(int[].class);
+        final ArgumentCaptor<int[]> captorFwIds = ArgumentCaptor.forClass(int[].class);
         mClient.start(mCallback);
 
         verify(mSession).enumerateEnrollments();
         assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
         assertThat(mNotificationSent).isFalse();
         verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
+        verify(mBiometricLogger).logOnEnumerated(eq(USER_ID),
+                eq(BiometricsProtoEnums.ENUMERATION_RESULT_OK), captorHalIds.capture(),
+                captorFwIds.capture());
+        assertThat(captorHalIds.getAllValues().stream()
+                .flatMap(x -> Arrays.stream(x).boxed())
+                .collect(Collectors.toList()))
+                .containsExactly(1);
+        assertThat(captorFwIds.getAllValues().stream()
+                .flatMap(x -> Arrays.stream(x).boxed())
+                .collect(Collectors.toList()))
+                .containsExactly(1);
         verify(mCallback).onClientFinished(mClient, true);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintRemovalClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintRemovalClientTest.java
index 64f07e2..ced916e 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintRemovalClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintRemovalClientTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.fingerprint.Fingerprint;
 import android.os.IBinder;
@@ -91,7 +92,8 @@
 
         mClient = new FingerprintRemovalClient(mContext, () -> mAidlSession, mToken, mListener,
                 mBiometricIds, USER_ID, TAG, mBiometricUtils, SENSOR_ID,
-                mBiometricLogger, mBiometricContext, mAuthenticatorIds);
+                mBiometricLogger, mBiometricContext, mAuthenticatorIds,
+                BiometricsProtoEnums.UNENROLL_REASON_UNKNOWN);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
index 1f3aa35..a30591e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
@@ -23,6 +23,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.server.wm.utils.LastCallVerifier.lastCall;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -33,6 +34,7 @@
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 
@@ -56,6 +58,7 @@
         final SurfaceSession mSession = new SurfaceSession();
         final SurfaceControl mHostControl = mock(SurfaceControl.class);
         final SurfaceControl.Transaction mHostTransaction = spy(StubTransaction.class);
+        Rect mBounds = new Rect(10, 20, 30, 40);
 
         MockSurfaceBuildingContainer(WindowManagerService wm) {
             super(wm);
@@ -94,6 +97,11 @@
         public SurfaceControl.Transaction getPendingTransaction() {
             return mHostTransaction;
         }
+
+        @Override
+        public Rect getBounds() {
+            return mBounds;
+        }
     }
 
     static class MockAnimationAdapterFactory extends DimmerAnimationHelper.AnimationAdapterFactory {
@@ -104,34 +112,63 @@
         }
     }
 
+    static class TestActivityEmbeddingMock {
+        Task mTask = mock(Task.class);
+        TaskFragment mLeft = mock(TaskFragment.class);
+        TaskFragment mRight = mock(TaskFragment.class);
+        Rect mTaskBounds = new Rect(10, 0, 50, 40);
+        Rect mLeftBounds = new Rect(10, 0, 30, 40);
+        Rect mRightBounds = new Rect(30, 0, 50, 40);
+
+        TestActivityEmbeddingMock() {
+            when(mTask.getBounds()).thenReturn(mTaskBounds);
+            when(mLeft.getBounds()).thenReturn(mLeftBounds);
+            when(mRight.getBounds()).thenReturn(mRightBounds);
+            when(mLeft.isEmbedded()).thenReturn(true);
+            when(mRight.isEmbedded()).thenReturn(true);
+        }
+
+        void pretendParentToTask(WindowState child) {
+            when(child.getTaskFragment()).thenReturn(mTask);
+            when(child.getTask()).thenReturn(mTask);
+        }
+
+        void pretendParentToRight(WindowState child) {
+            when(child.getTaskFragment()).thenReturn(mRight);
+            when(child.getTask()).thenReturn(mTask);
+        }
+    }
+
+    WindowState getMockDimmingContainer() {
+        WindowState window = mock(WindowState.class);
+        SurfaceControl surface = mock(SurfaceControl.class);
+        when(window.getSurfaceControl()).thenReturn(surface);
+        return window;
+    }
+
     private Dimmer mDimmer;
     private SurfaceControl.Transaction mTransaction;
+    MockSurfaceBuildingContainer mHost;
     private WindowState mChild1;
     private WindowState mChild2;
     private static AnimationAdapter sTestAnimation;
 
     @Before
     public void setUp() throws Exception {
-        MockSurfaceBuildingContainer host = new MockSurfaceBuildingContainer(mWm);
-        mTransaction = host.getSyncTransaction();
-
-        final SurfaceControl mControl1 = mock(SurfaceControl.class);
-        final SurfaceControl mControl2 = mock(SurfaceControl.class);
+        mHost = new MockSurfaceBuildingContainer(mWm);
+        mTransaction = mHost.getSyncTransaction();
 
         SurfaceAnimator animator = mock(SurfaceAnimator.class);
         when(animator.getAnimation()).thenReturn(null);
 
-        mChild1 = mock(WindowState.class);
-        when(mChild1.getSurfaceControl()).thenReturn(mControl1);
+        mChild1 = getMockDimmingContainer();
+        mChild2 = getMockDimmingContainer();
 
-        mChild2 = mock(WindowState.class);
-        when(mChild2.getSurfaceControl()).thenReturn(mControl2);
-
-        host.addChild(mChild1, 0);
-        host.addChild(mChild2, 1);
+        mHost.addChild(mChild1, 0);
+        mHost.addChild(mChild2, 1);
 
         sTestAnimation = spy(new MockAnimationAdapter());
-        mDimmer = new Dimmer(host, new MockAnimationAdapterFactory());
+        mDimmer = new Dimmer(mHost, new MockAnimationAdapterFactory());
     }
 
     @Test
@@ -150,6 +187,63 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_USE_TASKS_DIM_ONLY)
+    public void testBoundsInActivityEmbeddingForWholeTask() {
+        final WindowState dimmingWindow = getMockDimmingContainer();
+        TestActivityEmbeddingMock embedding = new TestActivityEmbeddingMock();
+        embedding.pretendParentToRight(dimmingWindow);
+        when(embedding.mRight.isDimmingOnParentTask()).thenReturn(true);
+
+        mDimmer.adjustAppearance(dimmingWindow, 1, 1);
+        mDimmer.adjustPosition(dimmingWindow, dimmingWindow);
+        mDimmer.updateDims(mTransaction);
+        verify(mTransaction).setWindowCrop(mDimmer.getDimLayer(),
+                embedding.mTaskBounds.width(), embedding.mTaskBounds.height());
+        verify(mTransaction).setPosition(mDimmer.getDimLayer(), 0, 0);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_USE_TASKS_DIM_ONLY)
+    public void testBoundsInActivityEmbeddingForTaskFragmentOnly() {
+        final WindowState dimmingWindow = getMockDimmingContainer();
+        TestActivityEmbeddingMock embedding = new TestActivityEmbeddingMock();
+        embedding.pretendParentToRight(dimmingWindow);
+        when(embedding.mRight.isDimmingOnParentTask()).thenReturn(false);
+
+        mDimmer.adjustAppearance(dimmingWindow, 1, 1);
+        mDimmer.adjustPosition(dimmingWindow, dimmingWindow);
+        mDimmer.updateDims(mTransaction);
+        Rect expectedAbsoluteBounds = embedding.mRightBounds;
+        Rect expectedRelativeBounds = new Rect(expectedAbsoluteBounds);
+        expectedRelativeBounds.offset(-embedding.mTaskBounds.left, -embedding.mTaskBounds.top);
+        verify(mTransaction).setWindowCrop(mDimmer.getDimLayer(),
+                embedding.mRightBounds.width(), embedding.mRightBounds.height());
+        verify(mTransaction).setPosition(mDimmer.getDimLayer(),
+                expectedRelativeBounds.left, expectedRelativeBounds.top);
+        assertEquals(expectedAbsoluteBounds, mDimmer.getDimBounds());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_USE_TASKS_DIM_ONLY)
+    public void testDimBoundsAdaptToResizing() {
+        // First call with some generic bounds
+        mDimmer.adjustAppearance(mChild1, 0.5f, 1);
+        mDimmer.adjustPosition(mChild1, mChild1);
+        mDimmer.updateDims(mTransaction);
+        verify(mTransaction).setWindowCrop(mDimmer.getDimLayer(),
+                mHost.getBounds().width(), mHost.getBounds().height());
+
+        // Change bounds
+        mHost.getBounds().left += 5;
+        mHost.getBounds().top += 6;
+        mDimmer.adjustAppearance(mChild1, 1, 1);
+        mDimmer.adjustPosition(mChild1, mChild1);
+        mDimmer.updateDims(mTransaction);
+        verify(mTransaction).setWindowCrop(mDimmer.getDimLayer(),
+                mHost.getBounds().width(), mHost.getBounds().height());
+    }
+
+    @Test
     public void testDimBelowWithChildSurfaceCreatesSurfaceBelowChild() {
         final float alpha = 0.7f;
         final int blur = 50;
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index eb8bc91..27d46fc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -16,10 +16,8 @@
 
 package com.android.server.wm;
 
-import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR;
 import static android.view.DisplayCutout.NO_CUTOUT;
 import static android.view.InsetsSource.ID_IME;
-import static android.view.RoundedCorners.NO_ROUNDED_CORNERS;
 import static android.view.Surface.ROTATION_0;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
@@ -57,14 +55,11 @@
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 import android.view.DisplayInfo;
-import android.view.DisplayShape;
 import android.view.InsetsFrameProvider;
 import android.view.InsetsSource;
 import android.view.InsetsState;
-import android.view.PrivacyIndicatorBounds;
 import android.view.Surface;
 import android.view.WindowInsets;
-import android.view.WindowInsets.Side;
 import android.view.WindowManager;
 
 import androidx.test.filters.SmallTest;
@@ -495,44 +490,6 @@
                 di.logicalHeight).mNonDecorInsets.bottom);
     }
 
-    @SetupWindows(addWindows = { W_NAVIGATION_BAR, W_INPUT_METHOD })
-    @Test
-    public void testImeMinimalSourceFrame() {
-        Assume.assumeFalse("Behavior no longer needed with ENABLE_HIDE_IME_CAPTION_BAR",
-                ENABLE_HIDE_IME_CAPTION_BAR);
-
-        final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy();
-        final DisplayInfo displayInfo = mDisplayContent.getDisplayInfo();
-
-        WindowManager.LayoutParams attrs = mNavBarWindow.mAttrs;
-        displayPolicy.addWindowLw(mNavBarWindow, attrs);
-        mNavBarWindow.setRequestedSize(attrs.width, attrs.height);
-        mNavBarWindow.getControllableInsetProvider().setServerVisible(true);
-        final InsetsState state = mDisplayContent.getInsetsStateController().getRawInsetsState();
-        mImeWindow.mAboveInsetsState.set(state);
-        mDisplayContent.mDisplayFrames = new DisplayFrames(
-                state, displayInfo, NO_CUTOUT, NO_ROUNDED_CORNERS, new PrivacyIndicatorBounds(),
-                DisplayShape.NONE);
-
-        mDisplayContent.setInputMethodWindowLocked(mImeWindow);
-        mImeWindow.mAttrs.setFitInsetsSides(Side.all() & ~Side.BOTTOM);
-        mImeWindow.mGivenContentInsets.set(0, displayInfo.logicalHeight, 0, 0);
-        mImeWindow.getControllableInsetProvider().setServerVisible(true);
-
-        displayPolicy.layoutWindowLw(mNavBarWindow, null, mDisplayContent.mDisplayFrames);
-        displayPolicy.layoutWindowLw(mImeWindow, null, mDisplayContent.mDisplayFrames);
-
-        final InsetsSource imeSource = state.peekSource(ID_IME);
-        final InsetsSource navBarSource = state.peekSource(
-                mNavBarWindow.getControllableInsetProvider().getSource().getId());
-
-        assertNotNull(imeSource);
-        assertNotNull(navBarSource);
-        assertFalse(imeSource.getFrame().isEmpty());
-        assertFalse(navBarSource.getFrame().isEmpty());
-        assertTrue(imeSource.getFrame().contains(navBarSource.getFrame()));
-    }
-
     @SetupWindows(addWindows = W_INPUT_METHOD)
     @Test
     public void testImeInsetsGivenContentFrame() {
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index f0c3504..c78dbe7 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -710,6 +710,16 @@
     public static final String ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED =
             "android.telephony.action.ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED";
 
+
+    /**
+     * This intent will be broadcasted to start a non-emergency session.
+     * This intent will be sent only to the app with component defined in
+     * config_satellite_carrier_roaming_non_emergency_session_class and package defined in
+     * config_satellite_gateway_service_package
+     * @hide
+     */
+    public static final String ACTION_SATELLITE_START_NON_EMERGENCY_SESSION =
+            "android.telephony.action.ACTION_SATELLITE_START_NON_EMERGENCY_SESSION";
     /**
      * Meta-data represents whether the application supports P2P SMS over carrier roaming satellite
      * which needs manual trigger to connect to satellite. The messaging applications that supports
diff --git a/tests/Input/res/xml/keyboard_glyph_maps.xml b/tests/Input/res/xml/keyboard_glyph_maps.xml
index d0616ff..42561c1 100644
--- a/tests/Input/res/xml/keyboard_glyph_maps.xml
+++ b/tests/Input/res/xml/keyboard_glyph_maps.xml
@@ -19,4 +19,8 @@
         androidprv:glyphMap="@xml/test_glyph_map"
         androidprv:vendorId="0x1234"
         androidprv:productId="0x3456" />
+    <keyboard-glyph-map
+        androidprv:glyphMap="@xml/test_glyph_map2"
+        androidprv:vendorId="0x1235"
+        androidprv:productId="0x3457" />
 </keyboard-glyph-maps>
\ No newline at end of file
diff --git a/tests/Input/res/xml/test_glyph_map2.xml b/tests/Input/res/xml/test_glyph_map2.xml
new file mode 100644
index 0000000..7a7c1ac
--- /dev/null
+++ b/tests/Input/res/xml/test_glyph_map2.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<keyboard-glyph-map xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <key-glyph
+        androidprv:keycode="KEYCODE_BACK"
+        androidprv:glyphDrawable="@drawable/test_key_drawable" />
+    <modifier-glyph
+        androidprv:modifier="META"
+        androidprv:glyphDrawable="@drawable/test_modifier_drawable" />
+    <function-row-key androidprv:keycode="KEYCODE_EMOJI_PICKER" />
+    <hardware-defined-shortcut
+        androidprv:keycode="KEYCODE_1"
+        androidprv:modifierState="FUNCTION"
+        androidprv:outKeycode="KEYCODE_BACK" />
+    <hardware-defined-shortcut
+        androidprv:keycode="KEYCODE_2"
+        androidprv:modifierState="FUNCTION|META"
+        androidprv:outKeycode="KEYCODE_HOME" />
+</keyboard-glyph-map>
\ No newline at end of file
diff --git a/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt
index ff8a9ba..5da0beb 100644
--- a/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt
@@ -59,6 +59,9 @@
         const val DEVICE_ID = 1
         const val VENDOR_ID = 0x1234
         const val PRODUCT_ID = 0x3456
+        const val DEVICE_ID2 = 2
+        const val VENDOR_ID2 = 0x1235
+        const val PRODUCT_ID2 = 0x3457
         const val PACKAGE_NAME = "KeyboardLayoutManagerTests"
         const val RECEIVER_NAME = "DummyReceiver"
     }
@@ -96,8 +99,11 @@
             .thenReturn(inputManager)
 
         keyboardDevice = createKeyboard(DEVICE_ID, VENDOR_ID, PRODUCT_ID, 0, "", "")
-        Mockito.`when`(inputManagerRule.mock.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID))
+        Mockito.`when`(inputManagerRule.mock.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID, DEVICE_ID2))
         Mockito.`when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice)
+
+        val keyboardDevice2 = createKeyboard(DEVICE_ID2, VENDOR_ID2, PRODUCT_ID2, 0, "", "")
+        Mockito.`when`(inputManagerRule.mock.getInputDevice(DEVICE_ID2)).thenReturn(keyboardDevice2)
     }
 
     private fun setupBroadcastReceiver() {
@@ -143,6 +149,10 @@
             "Glyph map for test keyboard(deviceId=$DEVICE_ID) must exist",
             keyboardGlyphManager.getKeyGlyphMap(DEVICE_ID)
         )
+        assertNotNull(
+            "Glyph map for test keyboard(deviceId=$DEVICE_ID2) must exist",
+            keyboardGlyphManager.getKeyGlyphMap(DEVICE_ID2)
+        )
         assertNull(
             "Glyph map for non-existing keyboard must be null",
             keyboardGlyphManager.getKeyGlyphMap(-2)
@@ -158,6 +168,7 @@
 
         assertNotNull(glyphMap.getDrawableForModifier(context, KeyEvent.KEYCODE_META_LEFT))
         assertNotNull(glyphMap.getDrawableForModifier(context, KeyEvent.KEYCODE_META_RIGHT))
+        assertNotNull(glyphMap.getDrawableForModifierState(context, KeyEvent.META_META_ON))
 
         val functionRowKeys = glyphMap.functionRowKeys
         assertEquals(1, functionRowKeys.size)
diff --git a/tests/TouchLatency/app/src/main/res/values/styles.xml b/tests/TouchLatency/app/src/main/res/values/styles.xml
index 5058331..fa352cf 100644
--- a/tests/TouchLatency/app/src/main/res/values/styles.xml
+++ b/tests/TouchLatency/app/src/main/res/values/styles.xml
@@ -18,7 +18,7 @@
     <!-- Base application theme. -->
     <style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
         <!-- Customize your theme here. -->
-        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
+        <item name="android:windowLayoutInDisplayCutoutMode">default</item>
     </style>
 
 </resources>
diff --git a/tests/vcn/OWNERS b/tests/vcn/OWNERS
index 2441e77..937699a 100644
--- a/tests/vcn/OWNERS
+++ b/tests/vcn/OWNERS
@@ -1,7 +1,6 @@
 set noparent
 
-benedictwong@google.com
-ckesting@google.com
 evitayan@google.com
-junyin@google.com
 nharold@google.com
+benedictwong@google.com #{LAST_RESORT_SUGGESTION}
+yangji@google.com #{LAST_RESORT_SUGGESTION}