Merge "Propagate displayId in ImeTargetChangeListener" into main
diff --git a/core/java/Android.bp b/core/java/Android.bp
index 128fb62..92bca3c 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -22,11 +22,9 @@
         ":framework-nfc-non-updatable-sources",
         ":messagequeue-gen",
     ],
-    // Exactly one of the below will be added to srcs by messagequeue-gen
+    // Exactly one MessageQueue.java will be added to srcs by messagequeue-gen
     exclude_srcs: [
-        "android/os/LegacyMessageQueue/MessageQueue.java",
-        "android/os/ConcurrentMessageQueue/MessageQueue.java",
-        "android/os/SemiConcurrentMessageQueue/MessageQueue.java",
+        "android/os/*MessageQueue/**/*.java",
     ],
     visibility: ["//frameworks/base"],
 }
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 2f80b30..d455853 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -232,6 +232,7 @@
 import com.android.internal.content.ReferrerIntent;
 import com.android.internal.os.BinderCallsStats;
 import com.android.internal.os.BinderInternal;
+import com.android.internal.os.DebugStore;
 import com.android.internal.os.RuntimeInit;
 import com.android.internal.os.SafeZipPathValidatorCallback;
 import com.android.internal.os.SomeArgs;
@@ -358,6 +359,15 @@
     private static final long BINDER_CALLBACK_THROTTLE = 10_100L;
     private long mBinderCallbackLast = -1;
 
+    private static final boolean DEBUG_STORE_ENABLED =
+            com.android.internal.os.Flags.debugStoreEnabled();
+
+    /**
+    * Threshold for identifying long-running looper messages (in milliseconds).
+    * Calculated as 2 seconds multiplied by the hardware timeout multiplier.
+    */
+    private static final long LONG_MESSAGE_THRESHOLD_MS = 2000 * Build.HW_TIMEOUT_MULTIPLIER;
+
     /**
      * Denotes the sequence number of the process state change for which the main thread needs
      * to block until the network rules are updated for it.
@@ -2395,6 +2405,12 @@
         }
         public void handleMessage(Message msg) {
             if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
+            long debugStoreId = -1;
+            // By default, log all long messages when the debug store is enabled,
+            // unless this is overridden for certain message types, for which we have
+            // more granular debug store logging.
+            boolean shouldLogLongMessage = DEBUG_STORE_ENABLED;
+            final long messageStartUptimeMs = SystemClock.uptimeMillis();
             switch (msg.what) {
                 case BIND_APPLICATION:
                     Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
@@ -2419,24 +2435,61 @@
                                     "broadcastReceiveComp");
                         }
                     }
-                    handleReceiver((ReceiverData)msg.obj);
-                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                    ReceiverData receiverData = (ReceiverData) msg.obj;
+                    if (DEBUG_STORE_ENABLED) {
+                        debugStoreId =
+                                DebugStore.recordBroadcastHandleReceiver(receiverData.intent);
+                    }
+
+                    try {
+                        handleReceiver(receiverData);
+                    } finally {
+                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                        if (DEBUG_STORE_ENABLED) {
+                            DebugStore.recordEventEnd(debugStoreId);
+                            shouldLogLongMessage = false;
+                        }
+                    }
                     break;
                 case CREATE_SERVICE:
                     if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                                 ("serviceCreate: " + String.valueOf(msg.obj)));
                     }
-                    handleCreateService((CreateServiceData)msg.obj);
-                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                    CreateServiceData createServiceData = (CreateServiceData) msg.obj;
+                    if (DEBUG_STORE_ENABLED) {
+                        debugStoreId = DebugStore.recordServiceCreate(createServiceData.info);
+                    }
+
+                    try {
+                        handleCreateService(createServiceData);
+                    } finally {
+                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                        if (DEBUG_STORE_ENABLED) {
+                            DebugStore.recordEventEnd(debugStoreId);
+                            shouldLogLongMessage = false;
+                        }
+                    }
                     break;
                 case BIND_SERVICE:
                     if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceBind: "
                                 + String.valueOf(msg.obj));
                     }
-                    handleBindService((BindServiceData)msg.obj);
-                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                    BindServiceData bindData = (BindServiceData) msg.obj;
+                    if (DEBUG_STORE_ENABLED) {
+                        debugStoreId =
+                                DebugStore.recordServiceBind(bindData.rebind, bindData.intent);
+                    }
+                    try {
+                        handleBindService(bindData);
+                    } finally {
+                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                        if (DEBUG_STORE_ENABLED) {
+                            DebugStore.recordEventEnd(debugStoreId);
+                            shouldLogLongMessage = false;
+                        }
+                    }
                     break;
                 case UNBIND_SERVICE:
                     if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
@@ -2452,8 +2505,21 @@
                         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                                 ("serviceStart: " + String.valueOf(msg.obj)));
                     }
-                    handleServiceArgs((ServiceArgsData)msg.obj);
-                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                    ServiceArgsData serviceData = (ServiceArgsData) msg.obj;
+                    if (DEBUG_STORE_ENABLED) {
+                        debugStoreId = DebugStore.recordServiceOnStart(serviceData.startId,
+                                serviceData.flags, serviceData.args);
+                    }
+
+                    try {
+                        handleServiceArgs(serviceData);
+                    } finally {
+                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+                        if (DEBUG_STORE_ENABLED) {
+                            DebugStore.recordEventEnd(debugStoreId);
+                            shouldLogLongMessage = false;
+                        }
+                    }
                     break;
                 case STOP_SERVICE:
                     if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
@@ -2649,11 +2715,17 @@
                     handleFinishInstrumentationWithoutRestart();
                     break;
             }
+            long messageElapsedTimeMs = SystemClock.uptimeMillis() - messageStartUptimeMs;
             Object obj = msg.obj;
             if (obj instanceof SomeArgs) {
                 ((SomeArgs) obj).recycle();
             }
             if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
+            if (shouldLogLongMessage
+                    && messageElapsedTimeMs > LONG_MESSAGE_THRESHOLD_MS) {
+                DebugStore.recordLongLooperMessage(msg.what, msg.getTarget().getClass().getName(),
+                        messageElapsedTimeMs);
+            }
         }
     }
 
diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java
index 1f19f81..933c336 100644
--- a/core/java/android/appwidget/AppWidgetHostView.java
+++ b/core/java/android/appwidget/AppWidgetHostView.java
@@ -132,7 +132,7 @@
      * Create a host view. Uses specified animations when pushing
      * {@link #updateAppWidget(RemoteViews)}.
      *
-     * @param animationIn Resource ID of in animation to use
+     * @param animationIn  Resource ID of in animation to use
      * @param animationOut Resource ID of out animation to use
      */
     @SuppressWarnings({"UnusedDeclaration"})
@@ -148,7 +148,7 @@
      * Pass the given handler to RemoteViews when updating this widget. Unless this
      * is done immediatly after construction, a call to {@link #updateAppWidget(RemoteViews)}
      * should be made.
-     * @param handler
+     *
      * @hide
      */
     public void setInteractionHandler(InteractionHandler handler) {
@@ -206,10 +206,10 @@
      * order for the AppWidgetHost to account for the automatic padding when computing the number
      * of cells to allocate to a particular widget.
      *
-     * @param context the current context
+     * @param context   the current context
      * @param component the component name of the widget
-     * @param padding Rect in which to place the output, if null, a new Rect will be allocated and
-     *                returned
+     * @param padding   Rect in which to place the output, if null, a new Rect will be allocated and
+     *                  returned
      * @return default padding for this widget, in pixels
      */
     public static Rect getDefaultPaddingForWidget(Context context, ComponentName component,
@@ -291,7 +291,7 @@
         }
         mDelayedRestoredInflationId = -1;
         mDelayedRestoredState = null;
-        try  {
+        try {
             super.dispatchRestoreInstanceState(state);
         } catch (Exception e) {
             Log.e(TAG, "failed to restoreInstanceState for widget id: " + mAppWidgetId + ", "
@@ -354,14 +354,14 @@
      * the framework will be accounted for automatically. This information gets embedded into the
      * AppWidget options and causes a callback to the AppWidgetProvider. In addition, the list of
      * sizes is explicitly set to an empty list.
-     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
      *
      * @param newOptions The bundle of options, in addition to the size information,
-     *          can be null.
-     * @param minWidth The minimum width in dips that the widget will be displayed at.
-     * @param minHeight The maximum height in dips that the widget will be displayed at.
-     * @param maxWidth The maximum width in dips that the widget will be displayed at.
-     * @param maxHeight The maximum height in dips that the widget will be displayed at.
+     *                   can be null.
+     * @param minWidth   The minimum width in dips that the widget will be displayed at.
+     * @param minHeight  The maximum height in dips that the widget will be displayed at.
+     * @param maxWidth   The maximum width in dips that the widget will be displayed at.
+     * @param maxHeight  The maximum height in dips that the widget will be displayed at.
+     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
      * @deprecated use {@link AppWidgetHostView#updateAppWidgetSize(Bundle, List)} instead.
      */
     @Deprecated
@@ -378,12 +378,14 @@
      * This method will update the option bundle with the list of sizes and the min/max bounds for
      * width and height.
      *
-     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
-     *
      * @param newOptions The bundle of options, in addition to the size information.
-     * @param sizes Sizes, in dips, the widget may be displayed at without calling the provider
-     *              again. Typically, this will be size of the widget in landscape and portrait.
-     *              On some foldables, this might include the size on the outer and inner screens.
+     * @param sizes      Sizes, in dips, the widget may be displayed at without calling the
+     *                   provider
+     *                   again. Typically, this will be size of the widget in landscape and
+     *                   portrait.
+     *                   On some foldables, this might include the size on the outer and inner
+     *                   screens.
+     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
      */
     public void updateAppWidgetSize(@NonNull Bundle newOptions, @NonNull List<SizeF> sizes) {
         AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext);
@@ -470,9 +472,9 @@
     /**
      * Specify some extra information for the widget provider. Causes a callback to the
      * AppWidgetProvider.
-     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
      *
      * @param options The bundle of options information.
+     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
      */
     public void updateAppWidgetOptions(Bundle options) {
         AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(mAppWidgetId, options);
@@ -507,6 +509,7 @@
     /**
      * Sets whether the widget is being displayed on a light/white background and use an
      * alternate UI if available.
+     *
      * @see RemoteViews#setLightBackgroundLayoutId(int)
      */
     public void setOnLightBackground(boolean onLightBackground) {
@@ -620,7 +623,7 @@
         if (content == null) {
             if (mViewMode == VIEW_MODE_ERROR) {
                 // We've already done this -- nothing to do.
-                return ;
+                return;
             }
             if (exception != null) {
                 Log.w(TAG, "Error inflating RemoteViews", exception);
@@ -733,7 +736,7 @@
             if (adapter instanceof BaseAdapter) {
                 BaseAdapter baseAdapter = (BaseAdapter) adapter;
                 baseAdapter.notifyDataSetChanged();
-            }  else if (adapter == null && adapterView instanceof RemoteAdapterConnectionCallback) {
+            } else if (adapter == null && adapterView instanceof RemoteAdapterConnectionCallback) {
                 // If the adapter is null, it may mean that the RemoteViewsAapter has not yet
                 // connected to its associated service, and hence the adapter hasn't been set.
                 // In this case, we need to defer the notify call until it has been set.
@@ -745,6 +748,7 @@
     /**
      * Build a {@link Context} cloned into another package name, usually for the
      * purposes of reading remote resources.
+     *
      * @hide
      */
     protected Context getRemoteContextEnsuringCorrectCachedApkPath() {
@@ -760,7 +764,7 @@
             }
             return newContext;
         } catch (NameNotFoundException e) {
-            Log.e(TAG, "Package name " +  mInfo.providerInfo.packageName + " not found");
+            Log.e(TAG, "Package name " + mInfo.providerInfo.packageName + " not found");
             return mContext;
         } catch (NullPointerException e) {
             Log.e(TAG, "Error trying to create the remote context.", e);
@@ -774,7 +778,7 @@
      */
     protected void prepareView(View view) {
         // Take requested dimensions from child, but apply default gravity.
-        FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams();
+        FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams) view.getLayoutParams();
         if (requested == null) {
             requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                     LayoutParams.MATCH_PARENT);
@@ -839,7 +843,18 @@
         return defaultView;
     }
 
-    private void onDefaultViewClicked(View view) {
+    /**
+     * Handles interactions on the default view of the widget. By default does not use the
+     * {@link InteractionHandler} used by other interactions. However, this can be overridden
+     * in order to customize the click behavior.
+     *
+     * @hide
+     */
+    protected void onDefaultViewClicked(@NonNull View view) {
+        final AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
+        if (manager != null) {
+            manager.noteAppWidgetTapped(mAppWidgetId);
+        }
         if (mInfo != null) {
             LauncherApps launcherApps = getContext().getSystemService(LauncherApps.class);
             List<LauncherActivityInfo> activities = launcherApps.getActivityList(
diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java
index d7195a7..964a8be 100644
--- a/core/java/android/content/BroadcastReceiver.java
+++ b/core/java/android/content/BroadcastReceiver.java
@@ -34,6 +34,8 @@
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.os.DebugStore;
+
 /**
  * Base class for code that receives and handles broadcast intents sent by
  * {@link android.content.Context#sendBroadcast(Intent)}.
@@ -55,6 +57,9 @@
     private PendingResult mPendingResult;
     private boolean mDebugUnregister;
 
+    private static final boolean DEBUG_STORE_ENABLED =
+            com.android.internal.os.Flags.debugStoreEnabled();
+
     /**
      * State for a result that is pending for a broadcast receiver.  Returned
      * by {@link BroadcastReceiver#goAsync() goAsync()}
@@ -255,6 +260,9 @@
                         "PendingResult#finish#ClassName:" + mReceiverClassName,
                         1);
             }
+            if (DEBUG_STORE_ENABLED) {
+                DebugStore.recordFinish(mReceiverClassName);
+            }
 
             if (mType == TYPE_COMPONENT) {
                 final IActivityManager mgr = ActivityManager.getService();
@@ -433,7 +441,9 @@
     public final PendingResult goAsync() {
         PendingResult res = mPendingResult;
         mPendingResult = null;
-
+        if (DEBUG_STORE_ENABLED) {
+            DebugStore.recordGoAsync(getClass().getName());
+        }
         if (res != null && Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
             res.mReceiverClassName = getClass().getName();
             Trace.traceCounter(Trace.TRACE_TAG_ACTIVITY_MANAGER,
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index d9b0e6d..7c2edd7 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -237,6 +237,8 @@
     bug: "307327678"
 }
 
+# This flag is enabled since V but not a MUST requirement in CDD yet, so it needs to stay around
+# for now and any code working with it should keep checking the flag.
 flag {
     name: "restrict_nonpreloads_system_shareduids"
     namespace: "package_manager_service"
diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
index 72b5cf7..90678df 100644
--- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
@@ -214,7 +214,7 @@
     private volatile long mNextInsertSeqValue = 0;
     /*
      * The exception to the FIFO order rule is sendMessageAtFrontOfQueue().
-     * Those messages must be in LIFO order - SIGH.
+     * Those messages must be in LIFO order.
      * Decrements on each front of queue insert.
      */
     private static final VarHandle sNextFrontInsertSeq;
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
index e6b1c07..14005b3 100644
--- a/core/java/android/os/FileUtils.java
+++ b/core/java/android/os/FileUtils.java
@@ -54,7 +54,6 @@
 import android.provider.MediaStore;
 import android.system.ErrnoException;
 import android.system.Os;
-import android.system.OsConstants;
 import android.system.StructStat;
 import android.text.TextUtils;
 import android.util.DataUnit;
@@ -1535,7 +1534,6 @@
     }
 
     /** {@hide} */
-    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = OsConstants.class)
     public static int translateModeStringToPosix(String mode) {
         // Quick check for invalid chars
         for (int i = 0; i < mode.length(); i++) {
@@ -1570,7 +1568,6 @@
     }
 
     /** {@hide} */
-    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = OsConstants.class)
     public static String translateModePosixToString(int mode) {
         String res = "";
         if ((mode & O_ACCMODE) == O_RDWR) {
@@ -1592,7 +1589,6 @@
     }
 
     /** {@hide} */
-    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = OsConstants.class)
     public static int translateModePosixToPfd(int mode) {
         int res = 0;
         if ((mode & O_ACCMODE) == O_RDWR) {
@@ -1617,7 +1613,6 @@
     }
 
     /** {@hide} */
-    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = OsConstants.class)
     public static int translateModePfdToPosix(int mode) {
         int res = 0;
         if ((mode & MODE_READ_WRITE) == MODE_READ_WRITE) {
@@ -1642,7 +1637,6 @@
     }
 
     /** {@hide} */
-    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = OsConstants.class)
     public static int translateModeAccessToPosix(int mode) {
         if (mode == F_OK) {
             // There's not an exact mapping, so we attempt a read-only open to
diff --git a/core/java/android/os/LockedMessageQueue/MessageQueue.java b/core/java/android/os/LockedMessageQueue/MessageQueue.java
new file mode 100644
index 0000000..b24e14b
--- /dev/null
+++ b/core/java/android/os/LockedMessageQueue/MessageQueue.java
@@ -0,0 +1,1351 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Handler;
+import android.os.Trace;
+import android.util.Log;
+import android.util.Printer;
+import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Low-level class holding the list of messages to be dispatched by a
+ * {@link Looper}.  Messages are not added directly to a MessageQueue,
+ * but rather through {@link Handler} objects associated with the Looper.
+ *
+ * <p>You can retrieve the MessageQueue for the current thread with
+ * {@link Looper#myQueue() Looper.myQueue()}.
+ */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
+@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
+        "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host")
+public final class MessageQueue {
+    private static final String TAG = "LockedMessageQueue";
+    private static final boolean DEBUG = false;
+    private static final boolean TRACE = false;
+
+    static final class MessageHeap {
+        static final int MESSAGE_HEAP_INITIAL_SIZE = 16;
+
+        Message[] mHeap = new Message[MESSAGE_HEAP_INITIAL_SIZE];
+        int mNumElements = 0;
+
+        static int parentNodeIdx(int i) {
+            return (i - 1) >>> 1;
+        }
+
+        Message getParentNode(int i) {
+            return mHeap[(i - 1) >>> 1];
+        }
+
+        static int rightNodeIdx(int i) {
+            return 2 * i + 2;
+        }
+
+        Message getRightNode(int i) {
+            return mHeap[2 * i + 2];
+        }
+
+        static int leftNodeIdx(int i) {
+            return 2 * i + 1;
+        }
+
+        Message getLeftNode(int i) {
+            return mHeap[2 * i + 1];
+        }
+
+        int size() {
+            return mHeap.length;
+        }
+
+        int numElements() {
+            return mNumElements;
+        }
+
+        boolean isEmpty() {
+            return mNumElements == 0;
+        }
+
+        Message getMessageAt(int index) {
+            return mHeap[index];
+        }
+
+        /*
+        * Returns:
+        *    0 if x==y.
+        *    A value less than 0 if x<y.
+        *    A value greater than 0 if x>y.
+        */
+        int compareMessage(Message x, Message y) {
+            int compared = Long.compare(x.when, y.when);
+            if (compared == 0) {
+                compared = Long.compare(x.mInsertSeq, y.mInsertSeq);
+            }
+            return compared;
+        }
+
+        int compareMessageByIdx(int x, int y) {
+            return compareMessage(mHeap[x], mHeap[y]);
+        }
+
+        void swap(int x, int y) {
+            Message tmp = mHeap[x];
+            mHeap[x] = mHeap[y];
+            mHeap[y] = tmp;
+        }
+
+        void siftDown(int i) {
+            int smallest = i;
+            int r, l;
+
+            while (true) {
+                r = rightNodeIdx(i);
+                l = leftNodeIdx(i);
+
+                if (r < mNumElements && compareMessageByIdx(r, smallest) < 0) {
+                    smallest = r;
+                }
+
+                if (l < mNumElements && compareMessageByIdx(l, smallest) < 0) {
+                    smallest = l;
+                }
+
+                if (smallest != i) {
+                    swap(i, smallest);
+                    i = smallest;
+                    continue;
+                }
+                break;
+            }
+        }
+
+        boolean siftUp(int i) {
+            boolean swapped = false;
+            while (i != 0 && compareMessage(mHeap[i], getParentNode(i)) < 0) {
+                int p = parentNodeIdx(i);
+
+                swap(i, p);
+                swapped = true;
+                i = p;
+            }
+
+            return swapped;
+        }
+
+        void maybeGrowHeap() {
+            if (mNumElements == mHeap.length) {
+                /* Grow by 1.5x */
+                int newSize = mHeap.length + (mHeap.length >>> 1);
+                Message[] newHeap;
+                if (DEBUG) {
+                    Log.v(TAG, "maybeGrowHeap mNumElements " + mNumElements + " mHeap.length "
+                            + mHeap.length + " newSize " + newSize);
+                }
+
+                newHeap = Arrays.copyOf(mHeap, newSize);
+                mHeap = newHeap;
+            }
+        }
+
+        void add(Message m) {
+            int i;
+
+            maybeGrowHeap();
+
+            i = mNumElements;
+            mNumElements++;
+            mHeap[i] = m;
+
+            siftUp(i);
+        }
+
+        void maybeShrinkHeap() {
+            /* Shrink by 2x */
+            int newSize = mHeap.length >>> 1;
+
+            if (newSize >= MESSAGE_HEAP_INITIAL_SIZE
+                    && mNumElements <= newSize) {
+                Message[] newHeap;
+
+                if (DEBUG) {
+                    Log.v(TAG, "maybeShrinkHeap mNumElements " + mNumElements + " mHeap.length "
+                            + mHeap.length + " newSize " + newSize);
+                }
+
+                newHeap = Arrays.copyOf(mHeap, newSize);
+                mHeap = newHeap;
+            }
+        }
+
+        Message poll() {
+            if (mNumElements > 0) {
+                Message ret = mHeap[0];
+                mNumElements--;
+                mHeap[0] = mHeap[mNumElements];
+                mHeap[mNumElements] = null;
+
+                siftDown(0);
+
+                maybeShrinkHeap();
+                return ret;
+            }
+            return null;
+        }
+
+        Message peek() {
+            if (mNumElements > 0) {
+                return mHeap[0];
+            }
+            return null;
+        }
+
+        private void remove(int i) throws IllegalArgumentException {
+            if (i > mNumElements || mNumElements == 0) {
+                throw new IllegalArgumentException("Index " + i + " out of bounds: "
+                        + mNumElements);
+            } else if (i == (mNumElements - 1)) {
+                mHeap[i] = null;
+                mNumElements--;
+            } else {
+                mNumElements--;
+                mHeap[i] = mHeap[mNumElements];
+                mHeap[mNumElements] = null;
+                if (!siftUp(i)) {
+                    siftDown(i);
+                }
+            }
+            /* Don't shink here, let the caller do this once it has removed all matching items. */
+        }
+
+        void removeAll() {
+            Message m;
+            for (int i = 0; i < mNumElements; i++) {
+                m = mHeap[i];
+                mHeap[i] = null;
+                m.recycleUnchecked();
+            }
+            mNumElements = 0;
+            maybeShrinkHeap();
+        }
+
+        abstract static class MessageHeapCompare {
+            public abstract boolean compareMessage(Message m, Handler h, int what, Object object,
+                    Runnable r, long when);
+        }
+
+        boolean findOrRemoveMessages(Handler h, int what, Object object, Runnable r, long when,
+                MessageHeapCompare compare, boolean removeMatches) {
+            boolean found = false;
+            /*
+             * Walk the heap backwards so we don't have to re-visit an array element due to
+             * sifting
+             */
+            for (int i = mNumElements - 1; i >= 0; i--) {
+                if (compare.compareMessage(mHeap[i], h, what, object, r, when)) {
+                    found = true;
+                    if (removeMatches) {
+                        Message m = mHeap[i];
+                        try {
+                            remove(i);
+                        } catch (IllegalArgumentException e) {
+                            Log.wtf(TAG, "Index out of bounds during remove " + e);
+                        }
+                        m.recycleUnchecked();
+                        continue;
+                    }
+                    break;
+                }
+            }
+            if (found && removeMatches) {
+                maybeShrinkHeap();
+            }
+            return found;
+        }
+
+        /*
+        * Keep this for manual debugging. It's easier to pepper the code with this function
+        * than MessageQueue.dump()
+        */
+        void print() {
+            Log.v(TAG, "heap num elem: " + mNumElements + " mHeap.length " + mHeap.length);
+            for (int i = 0; i < mNumElements; i++) {
+                Log.v(TAG, "[" + i + "]\t" + mHeap[i] + " seq: " + mHeap[i].mInsertSeq + " async: "
+                        + mHeap[i].isAsynchronous());
+            }
+        }
+
+        boolean verify(int root) {
+            int r = rightNodeIdx(root);
+            int l = leftNodeIdx(root);
+
+            if (l >= mNumElements && r >= mNumElements) {
+                return true;
+            }
+
+            if (l < mNumElements && compareMessageByIdx(l, root) < 0) {
+                Log.wtf(TAG, "Verify failure: root idx/when: " + root + "/" + mHeap[root].when
+                        + " left node idx/when: " + l + "/" + mHeap[l].when);
+                return false;
+            }
+
+            if (r < mNumElements && compareMessageByIdx(r, root) < 0) {
+                Log.wtf(TAG, "Verify failure: root idx/when: " + root + "/" + mHeap[root].when
+                        + " right node idx/when: " + r + "/" + mHeap[r].when);
+                return false;
+            }
+
+            if (!verify(r) || !verify(l)) {
+                return false;
+            }
+            return true;
+        }
+
+        boolean checkDanglingReferences(String where) {
+            /* First, let's make sure we didn't leave any dangling references */
+            for (int i = mNumElements; i < mHeap.length; i++) {
+                if (mHeap[i] != null) {
+                    Log.wtf(TAG, "[" + where
+                            + "] Verify failure: dangling reference found at index "
+                            + i + ": " + mHeap[i] + " Async " + mHeap[i].isAsynchronous()
+                            + " mNumElements " + mNumElements + " mHeap.length " + mHeap.length);
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        boolean verify() {
+            if (!checkDanglingReferences(TAG)) {
+                return false;
+            }
+            return verify(0);
+        }
+    }
+
+    // True if the message queue can be quit.
+    @UnsupportedAppUsage
+    private final boolean mQuitAllowed;
+
+    @UnsupportedAppUsage
+    @SuppressWarnings("unused")
+    private long mPtr; // used by native code
+
+    private final MessageHeap mPriorityQueue = new MessageHeap();
+    private final MessageHeap mAsyncPriorityQueue = new MessageHeap();
+
+    /*
+     * This helps us ensure that messages with the same timestamp are inserted in FIFO order.
+     * Increments on each insert, starting at 0. MessaeHeap.compareMessage() will compare sequences
+     * when delivery timestamps are identical.
+     */
+    private long mNextInsertSeq;
+
+    /*
+     * The exception to the FIFO order rule is sendMessageAtFrontOfQueue().
+     * Those messages must be in LIFO order.
+     * Decrements on each front of queue insert.
+     */
+    private long mNextFrontInsertSeq = -1;
+
+    @UnsupportedAppUsage
+    private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
+    private SparseArray<FileDescriptorRecord> mFileDescriptorRecords;
+    private IdleHandler[] mPendingIdleHandlers;
+    private boolean mQuitting;
+
+    // Indicates whether next() is blocked waiting in pollOnce() with a non-zero timeout.
+    private boolean mBlocked;
+
+    // The next barrier token.
+    // Barriers are indicated by messages with a null target whose arg1 field carries the token.
+    @UnsupportedAppUsage
+    private int mNextBarrierToken;
+
+    private native static long nativeInit();
+    private native static void nativeDestroy(long ptr);
+    @UnsupportedAppUsage
+    private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+    private native static void nativeWake(long ptr);
+    private native static boolean nativeIsPolling(long ptr);
+    private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
+
+    MessageQueue(boolean quitAllowed) {
+        mQuitAllowed = quitAllowed;
+        mPtr = nativeInit();
+    }
+
+    @GuardedBy("this")
+    private void removeRootFromPriorityQueue(Message msg) {
+        Message tmp;
+        if (msg.isAsynchronous()) {
+            tmp = mAsyncPriorityQueue.poll();
+        } else {
+            tmp = mPriorityQueue.poll();
+        }
+        if (DEBUG && tmp != msg) {
+            Log.wtf(TAG, "Unexpected message at head of heap. Wanted: " + msg + " msg.isAsync "
+                    + msg.isAsynchronous() + " Found: " + tmp);
+
+            mPriorityQueue.print();
+            mAsyncPriorityQueue.print();
+        }
+    }
+
+    @GuardedBy("this")
+    private Message pickEarliestMessage(Message x, Message y) {
+        if (x != null && y != null) {
+            if (mPriorityQueue.compareMessage(x, y) < 0) {
+                return x;
+            }
+            return y;
+        }
+
+        return x != null ? x : y;
+    }
+
+    @GuardedBy("this")
+    private Message peekEarliestMessage() {
+        Message x = mPriorityQueue.peek();
+        Message y = mAsyncPriorityQueue.peek();
+
+        return pickEarliestMessage(x, y);
+    }
+
+    @GuardedBy("this")
+    private boolean priorityQueuesAreEmpty() {
+        return mPriorityQueue.isEmpty() && mAsyncPriorityQueue.isEmpty();
+    }
+
+    @GuardedBy("this")
+    private boolean priorityQueueHasBarrier() {
+        Message m = mPriorityQueue.peek();
+
+        if (m != null && m.target == null) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            dispose();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    // Disposes of the underlying message queue.
+    // Must only be called on the looper thread or the finalizer.
+    private void dispose() {
+        if (mPtr != 0) {
+            nativeDestroy(mPtr);
+            mPtr = 0;
+        }
+    }
+
+    /**
+     * Returns true if the looper has no pending messages which are due to be processed.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @return True if the looper is idle.
+     */
+    public boolean isIdle() {
+        synchronized (this) {
+            Message m = peekEarliestMessage();
+            final long now = SystemClock.uptimeMillis();
+
+            return (priorityQueuesAreEmpty() || now < m.when);
+        }
+    }
+
+    /**
+     * Add a new {@link IdleHandler} to this message queue.  This may be
+     * removed automatically for you by returning false from
+     * {@link IdleHandler#queueIdle IdleHandler.queueIdle()} when it is
+     * invoked, or explicitly removing it with {@link #removeIdleHandler}.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @param handler The IdleHandler to be added.
+     */
+    public void addIdleHandler(@NonNull IdleHandler handler) {
+        if (handler == null) {
+            throw new NullPointerException("Can't add a null IdleHandler");
+        }
+        synchronized (this) {
+            mIdleHandlers.add(handler);
+        }
+    }
+
+    /**
+     * Remove an {@link IdleHandler} from the queue that was previously added
+     * with {@link #addIdleHandler}.  If the given object is not currently
+     * in the idle list, nothing is done.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @param handler The IdleHandler to be removed.
+     */
+    public void removeIdleHandler(@NonNull IdleHandler handler) {
+        synchronized (this) {
+            mIdleHandlers.remove(handler);
+        }
+    }
+
+    /**
+     * Returns whether this looper's thread is currently polling for more work to do.
+     * This is a good signal that the loop is still alive rather than being stuck
+     * handling a callback.  Note that this method is intrinsically racy, since the
+     * state of the loop can change before you get the result back.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @return True if the looper is currently polling for events.
+     * @hide
+     */
+    public boolean isPolling() {
+        synchronized (this) {
+            return isPollingLocked();
+        }
+    }
+
+    private boolean isPollingLocked() {
+        // If the loop is quitting then it must not be idling.
+        // We can assume mPtr != 0 when mQuitting is false.
+        return !mQuitting && nativeIsPolling(mPtr);
+    }
+
+    /**
+     * Adds a file descriptor listener to receive notification when file descriptor
+     * related events occur.
+     * <p>
+     * If the file descriptor has already been registered, the specified events
+     * and listener will replace any that were previously associated with it.
+     * It is not possible to set more than one listener per file descriptor.
+     * </p><p>
+     * It is important to always unregister the listener when the file descriptor
+     * is no longer of use.
+     * </p>
+     *
+     * @param fd The file descriptor for which a listener will be registered.
+     * @param events The set of events to receive: a combination of the
+     * {@link OnFileDescriptorEventListener#EVENT_INPUT},
+     * {@link OnFileDescriptorEventListener#EVENT_OUTPUT}, and
+     * {@link OnFileDescriptorEventListener#EVENT_ERROR} event masks.  If the requested
+     * set of events is zero, then the listener is unregistered.
+     * @param listener The listener to invoke when file descriptor events occur.
+     *
+     * @see OnFileDescriptorEventListener
+     * @see #removeOnFileDescriptorEventListener
+     */
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    public void addOnFileDescriptorEventListener(@NonNull FileDescriptor fd,
+            @OnFileDescriptorEventListener.Events int events,
+            @NonNull OnFileDescriptorEventListener listener) {
+        if (fd == null) {
+            throw new IllegalArgumentException("fd must not be null");
+        }
+        if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null");
+        }
+
+        synchronized (this) {
+            updateOnFileDescriptorEventListenerLocked(fd, events, listener);
+        }
+    }
+
+    /**
+     * Removes a file descriptor listener.
+     * <p>
+     * This method does nothing if no listener has been registered for the
+     * specified file descriptor.
+     * </p>
+     *
+     * @param fd The file descriptor whose listener will be unregistered.
+     *
+     * @see OnFileDescriptorEventListener
+     * @see #addOnFileDescriptorEventListener
+     */
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    public void removeOnFileDescriptorEventListener(@NonNull FileDescriptor fd) {
+        if (fd == null) {
+            throw new IllegalArgumentException("fd must not be null");
+        }
+
+        synchronized (this) {
+            updateOnFileDescriptorEventListenerLocked(fd, 0, null);
+        }
+    }
+
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    private void updateOnFileDescriptorEventListenerLocked(FileDescriptor fd, int events,
+            OnFileDescriptorEventListener listener) {
+        final int fdNum = fd.getInt$();
+
+        int index = -1;
+        FileDescriptorRecord record = null;
+        if (mFileDescriptorRecords != null) {
+            index = mFileDescriptorRecords.indexOfKey(fdNum);
+            if (index >= 0) {
+                record = mFileDescriptorRecords.valueAt(index);
+                if (record != null && record.mEvents == events) {
+                    return;
+                }
+            }
+        }
+
+        if (events != 0) {
+            events |= OnFileDescriptorEventListener.EVENT_ERROR;
+            if (record == null) {
+                if (mFileDescriptorRecords == null) {
+                    mFileDescriptorRecords = new SparseArray<FileDescriptorRecord>();
+                }
+                record = new FileDescriptorRecord(fd, events, listener);
+                mFileDescriptorRecords.put(fdNum, record);
+            } else {
+                record.mListener = listener;
+                record.mEvents = events;
+                record.mSeq += 1;
+            }
+            nativeSetFileDescriptorEvents(mPtr, fdNum, events);
+        } else if (record != null) {
+            record.mEvents = 0;
+            mFileDescriptorRecords.removeAt(index);
+            nativeSetFileDescriptorEvents(mPtr, fdNum, 0);
+        }
+    }
+
+    // Called from native code.
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private int dispatchEvents(int fd, int events) {
+        // Get the file descriptor record and any state that might change.
+        final FileDescriptorRecord record;
+        final int oldWatchedEvents;
+        final OnFileDescriptorEventListener listener;
+        final int seq;
+        synchronized (this) {
+            record = mFileDescriptorRecords.get(fd);
+            if (record == null) {
+                return 0; // spurious, no listener registered
+            }
+
+            oldWatchedEvents = record.mEvents;
+            events &= oldWatchedEvents; // filter events based on current watched set
+            if (events == 0) {
+                return oldWatchedEvents; // spurious, watched events changed
+            }
+
+            listener = record.mListener;
+            seq = record.mSeq;
+        }
+
+        // Invoke the listener outside of the lock.
+        int newWatchedEvents = listener.onFileDescriptorEvents(
+                record.mDescriptor, events);
+        if (newWatchedEvents != 0) {
+            newWatchedEvents |= OnFileDescriptorEventListener.EVENT_ERROR;
+        }
+
+        // Update the file descriptor record if the listener changed the set of
+        // events to watch and the listener itself hasn't been updated since.
+        if (newWatchedEvents != oldWatchedEvents) {
+            synchronized (this) {
+                int index = mFileDescriptorRecords.indexOfKey(fd);
+                if (index >= 0 && mFileDescriptorRecords.valueAt(index) == record
+                        && record.mSeq == seq) {
+                    record.mEvents = newWatchedEvents;
+                    if (newWatchedEvents == 0) {
+                        mFileDescriptorRecords.removeAt(index);
+                    }
+                }
+            }
+        }
+
+        // Return the new set of events to watch for native code to take care of.
+        return newWatchedEvents;
+    }
+
+    private static final AtomicLong mMessagesDelivered = new AtomicLong();
+
+    @UnsupportedAppUsage
+    Message next() {
+        // Return here if the message loop has already quit and been disposed.
+        // This can happen if the application tries to restart a looper after quit
+        // which is not supported.
+        final long ptr = mPtr;
+        if (ptr == 0) {
+            return null;
+        }
+
+        int pendingIdleHandlerCount = -1; // -1 only during first iteration
+        int nextPollTimeoutMillis = 0;
+        for (;;) {
+            if (nextPollTimeoutMillis != 0) {
+                Binder.flushPendingCommands();
+            }
+
+            nativePollOnce(ptr, nextPollTimeoutMillis);
+
+            synchronized (this) {
+                // Try to retrieve the next message.  Return if found.
+                final long now = SystemClock.uptimeMillis();
+                Message prevMsg = null;
+                Message msg = peekEarliestMessage();
+
+                if (DEBUG && msg != null) {
+                    Log.v(TAG, "Next found message " + msg + " isAsynchronous: "
+                            + msg.isAsynchronous() + " target " + msg.target);
+                }
+
+                if (msg != null && !msg.isAsynchronous() && msg.target == null) {
+                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
+                    msg = mAsyncPriorityQueue.peek();
+                    if (DEBUG) {
+                        Log.v(TAG, "Next message was barrier async msg: " + msg);
+                    }
+                }
+
+                if (msg != null) {
+                    if (now < msg.when) {
+                        // Next message is not ready.  Set a timeout to wake up when it is ready.
+                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
+                    } else {
+                        mBlocked = false;
+                        removeRootFromPriorityQueue(msg);
+                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
+                        msg.markInUse();
+                        if (TRACE) {
+                            Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet());
+                        }
+                        return msg;
+                    }
+                } else {
+                    // No more messages.
+                    nextPollTimeoutMillis = -1;
+                }
+
+                // Process the quit message now that all pending messages have been handled.
+                if (mQuitting) {
+                    dispose();
+                    return null;
+                }
+
+                // If first time idle, then get the number of idlers to run.
+                // Idle handles only run if the queue is empty or if the first message
+                // in the queue (possibly a barrier) is due to be handled in the future.
+                Message next = peekEarliestMessage();
+                if (pendingIdleHandlerCount < 0
+                        && (next == null || now < next.when)) {
+                    pendingIdleHandlerCount = mIdleHandlers.size();
+                }
+                if (pendingIdleHandlerCount <= 0) {
+                    // No idle handlers to run.  Loop and wait some more.
+                    mBlocked = true;
+                    continue;
+                }
+
+                if (mPendingIdleHandlers == null) {
+                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
+                }
+                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
+            }
+
+            // Run the idle handlers.
+            // We only ever reach this code block during the first iteration.
+            for (int i = 0; i < pendingIdleHandlerCount; i++) {
+                final IdleHandler idler = mPendingIdleHandlers[i];
+                mPendingIdleHandlers[i] = null; // release the reference to the handler
+
+                boolean keep = false;
+                try {
+                    keep = idler.queueIdle();
+                } catch (Throwable t) {
+                    Log.wtf(TAG, "IdleHandler threw exception", t);
+                }
+
+                if (!keep) {
+                    synchronized (this) {
+                        mIdleHandlers.remove(idler);
+                    }
+                }
+            }
+
+            // Reset the idle handler count to 0 so we do not run them again.
+            pendingIdleHandlerCount = 0;
+
+            // While calling an idle handler, a new message could have been delivered
+            // so go back and look again for a pending message without waiting.
+            nextPollTimeoutMillis = 0;
+        }
+    }
+
+    void quit(boolean safe) {
+        if (!mQuitAllowed) {
+            throw new IllegalStateException("Main thread not allowed to quit.");
+        }
+
+        synchronized (this) {
+            if (mQuitting) {
+                return;
+            }
+            mQuitting = true;
+
+            if (safe) {
+                removeAllFutureMessagesLocked();
+            } else {
+                removeAllMessagesLocked();
+            }
+
+            // We can assume mPtr != 0 because mQuitting was previously false.
+            nativeWake(mPtr);
+        }
+    }
+
+    /**
+     * Posts a synchronization barrier to the Looper's message queue.
+     *
+     * Message processing occurs as usual until the message queue encounters the
+     * synchronization barrier that has been posted.  When the barrier is encountered,
+     * later synchronous messages in the queue are stalled (prevented from being executed)
+     * until the barrier is released by calling {@link #removeSyncBarrier} and specifying
+     * the token that identifies the synchronization barrier.
+     *
+     * This method is used to immediately postpone execution of all subsequently posted
+     * synchronous messages until a condition is met that releases the barrier.
+     * Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier
+     * and continue to be processed as usual.
+     *
+     * This call must be always matched by a call to {@link #removeSyncBarrier} with
+     * the same token to ensure that the message queue resumes normal operation.
+     * Otherwise the application will probably hang!
+     *
+     * @return A token that uniquely identifies the barrier.  This token must be
+     * passed to {@link #removeSyncBarrier} to release the barrier.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    @TestApi
+    public int postSyncBarrier() {
+        return postSyncBarrier(SystemClock.uptimeMillis());
+    }
+
+    private int postSyncBarrier(long when) {
+        // Enqueue a new sync barrier token.
+        // We don't need to wake the queue because the purpose of a barrier is to stall it.
+        synchronized (this) {
+            final int token = mNextBarrierToken++;
+            final Message msg = Message.obtain();
+            msg.arg1 = token;
+
+            enqueueMessageUnchecked(msg, when);
+            return token;
+        }
+    }
+
+    private class MatchBarrierToken extends MessageHeap.MessageHeapCompare {
+        int mBarrierToken;
+
+        MatchBarrierToken(int token) {
+            super();
+            mBarrierToken = token;
+        }
+
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == null && m.arg1 == mBarrierToken) {
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Removes a synchronization barrier.
+     *
+     * @param token The synchronization barrier token that was returned by
+     * {@link #postSyncBarrier}.
+     *
+     * @throws IllegalStateException if the barrier was not found.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    @TestApi
+    public void removeSyncBarrier(int token) {
+        final MatchBarrierToken matchBarrierToken = new MatchBarrierToken(token);
+
+        // Remove a sync barrier token from the queue.
+        // If the queue is no longer stalled by a barrier then wake it.
+        synchronized (this) {
+            boolean removed;
+            Message first = mPriorityQueue.peek();
+
+            removed = mPriorityQueue.findOrRemoveMessages(null, 0, null, null, 0,
+                    matchBarrierToken, true);
+            if (removed && first != null) {
+                // If the loop is quitting then it is already awake.
+                // We can assume mPtr != 0 when mQuitting is false.
+                if (first.target == null && first.arg1 == token && !mQuitting) {
+                    nativeWake(mPtr);
+                }
+            } else if (!removed) {
+                throw new IllegalStateException("The specified message queue synchronization "
+                        + " barrier token has not been posted or has already been removed.");
+            }
+        }
+    }
+
+    boolean enqueueMessage(Message msg, long when) {
+        if (msg.target == null) {
+            throw new IllegalArgumentException("Message must have a target.");
+        }
+
+        return enqueueMessageUnchecked(msg, when);
+    }
+
+    boolean enqueueMessageUnchecked(Message msg, long when) {
+        synchronized (this) {
+            if (mQuitting) {
+                IllegalStateException e = new IllegalStateException(
+                        msg.target + " sending message to a Handler on a dead thread");
+                Log.w(TAG, e.getMessage(), e);
+                msg.recycle();
+                return false;
+            }
+
+            if (msg.isInUse()) {
+                throw new IllegalStateException(msg + " This message is already in use.");
+            }
+
+            msg.markInUse();
+            msg.when = when;
+            msg.mInsertSeq = when != 0 ? mNextInsertSeq++ : mNextFrontInsertSeq--;
+            if (DEBUG) Log.v(TAG, "Enqueue message: " + msg);
+            boolean needWake;
+            boolean isBarrier = msg.target == null;
+            Message first = peekEarliestMessage();
+
+            if (priorityQueuesAreEmpty() || when == 0 || when < first.when) {
+                needWake = mBlocked && !isBarrier;
+            } else {
+                Message firstNonAsyncMessage =
+                        first.isAsynchronous() ? mPriorityQueue.peek() : first;
+
+                needWake = mBlocked && firstNonAsyncMessage != null
+                        && firstNonAsyncMessage.target == null && msg.isAsynchronous();
+            }
+
+            if (msg.isAsynchronous()) {
+                mAsyncPriorityQueue.add(msg);
+            } else {
+                mPriorityQueue.add(msg);
+            }
+
+            // We can assume mPtr != 0 because mQuitting is false.
+            if (needWake) {
+                nativeWake(mPtr);
+            }
+        }
+        return true;
+    }
+
+    @GuardedBy("this")
+    boolean findOrRemoveMessages(Handler h, int what, Object object, Runnable r, long when,
+                MessageHeap.MessageHeapCompare compare, boolean removeMatches) {
+        boolean found = mPriorityQueue.findOrRemoveMessages(h, what, object, r, when, compare,
+                removeMatches);
+        boolean foundAsync = mAsyncPriorityQueue.findOrRemoveMessages(h, what, object, r, when,
+                compare, removeMatches);
+        return found || foundAsync;
+    }
+
+    private static class MatchHandlerWhatAndObject extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.what == what && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandlerWhatAndObject sMatchHandlerWhatAndObject =
+            new MatchHandlerWhatAndObject();
+
+    boolean hasMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return false;
+        }
+
+        synchronized (this) {
+            return findOrRemoveMessages(h, what, object, null, 0, sMatchHandlerWhatAndObject,
+                    false);
+        }
+    }
+
+    private static class MatchHandlerWhatAndObjectEquals extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.what == what && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandlerWhatAndObjectEquals sMatchHandlerWhatAndObjectEquals =
+            new MatchHandlerWhatAndObjectEquals();
+    boolean hasEqualMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return false;
+        }
+
+        synchronized (this) {
+            return findOrRemoveMessages(h, what, object, null, 0,
+                    sMatchHandlerWhatAndObjectEquals, false);
+        }
+    }
+
+    private static class MatchHandlerRunnableAndObject extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.callback == r && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandlerRunnableAndObject sMatchHandlerRunnableAndObject =
+            new MatchHandlerRunnableAndObject();
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    boolean hasMessages(Handler h, Runnable r, Object object) {
+        if (h == null) {
+            return false;
+        }
+
+        synchronized (this) {
+            return findOrRemoveMessages(h, -1, object, r, 0, sMatchHandlerRunnableAndObject,
+                    false);
+        }
+    }
+
+    private static class MatchHandler extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandler sMatchHandler = new MatchHandler();
+    boolean hasMessages(Handler h) {
+        if (h == null) {
+            return false;
+        }
+
+        synchronized (this) {
+            return findOrRemoveMessages(h, -1, null, null, 0, sMatchHandler, false);
+        }
+    }
+
+    void removeMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return;
+        }
+
+        synchronized (this) {
+            findOrRemoveMessages(h, what, object, null, 0, sMatchHandlerWhatAndObject, true);
+        }
+    }
+
+    void removeEqualMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return;
+        }
+
+        synchronized (this) {
+            findOrRemoveMessages(h, what, object, null, 0, sMatchHandlerWhatAndObjectEquals, true);
+        }
+    }
+
+    void removeMessages(Handler h, Runnable r, Object object) {
+        if (h == null || r == null) {
+            return;
+        }
+
+        synchronized (this) {
+            findOrRemoveMessages(h, -1, object, r, 0, sMatchHandlerRunnableAndObject, true);
+        }
+    }
+
+    private static class MatchHandlerRunnableAndObjectEquals
+            extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.callback == r && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandlerRunnableAndObjectEquals sMatchHandlerRunnableAndObjectEquals =
+            new MatchHandlerRunnableAndObjectEquals();
+    void removeEqualMessages(Handler h, Runnable r, Object object) {
+        if (h == null || r == null) {
+            return;
+        }
+
+        synchronized (this) {
+            findOrRemoveMessages(h, -1, object, r, 0, sMatchHandlerRunnableAndObjectEquals, true);
+        }
+    }
+
+    private static class MatchHandlerAndObject extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandlerAndObject sMatchHandlerAndObject = new MatchHandlerAndObject();
+    void removeCallbacksAndMessages(Handler h, Object object) {
+        if (h == null) {
+            return;
+        }
+
+        synchronized (this) {
+            findOrRemoveMessages(h, -1, object, null, 0, sMatchHandlerAndObject, true);
+        }
+    }
+
+    private static class MatchHandlerAndObjectEquals extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchHandlerAndObjectEquals sMatchHandlerAndObjectEquals =
+            new MatchHandlerAndObjectEquals();
+    void removeCallbacksAndEqualMessages(Handler h, Object object) {
+        if (h == null) {
+            return;
+        }
+
+        synchronized (this) {
+            findOrRemoveMessages(h, -1, object, null, 0, sMatchHandlerAndObjectEquals, true);
+        }
+    }
+
+    @GuardedBy("this")
+    private void removeAllMessagesLocked() {
+        mPriorityQueue.removeAll();
+        mAsyncPriorityQueue.removeAll();
+    }
+
+    private static class MatchAllFutureMessages extends MessageHeap.MessageHeapCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.when > when) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private static final MatchAllFutureMessages sMatchAllFutureMessages =
+            new MatchAllFutureMessages();
+    @GuardedBy("this")
+    private void removeAllFutureMessagesLocked() {
+        findOrRemoveMessages(null, -1, null, null, SystemClock.uptimeMillis(),
+                sMatchAllFutureMessages, true);
+    }
+
+    int dumpPriorityQueue(Printer pw, String prefix, Handler h, MessageHeap priorityQueue) {
+        int n = 0;
+        long now = SystemClock.uptimeMillis();
+        for (int i = 0; i < priorityQueue.numElements(); i++) {
+            Message m = priorityQueue.getMessageAt(i);
+            if (h == null && h == m.target) {
+                pw.println(prefix + "Message " + n + ": " + m.toString(now));
+                n++;
+            }
+        }
+        return n;
+    }
+
+    void dumpPriorityQueue(ProtoOutputStream proto, MessageHeap priorityQueue) {
+        for (int i = 0; i < priorityQueue.numElements(); i++) {
+            Message m = priorityQueue.getMessageAt(i);
+            m.dumpDebug(proto, MessageQueueProto.MESSAGES);
+        }
+    }
+
+    void dump(Printer pw, String prefix, Handler h) {
+        synchronized (this) {
+            pw.println(prefix + "(MessageQueue is using Locked implementation)");
+            long now = SystemClock.uptimeMillis();
+            int n = dumpPriorityQueue(pw, prefix, h, mPriorityQueue);
+            n += dumpPriorityQueue(pw, prefix, h, mAsyncPriorityQueue);
+            pw.println(prefix + "(Total messages: " + n + ", polling=" + isPollingLocked()
+                    + ", quitting=" + mQuitting + ")");
+        }
+    }
+
+    void dumpDebug(ProtoOutputStream proto, long fieldId) {
+        final long messageQueueToken = proto.start(fieldId);
+        synchronized (this) {
+            dumpPriorityQueue(proto, mPriorityQueue);
+            dumpPriorityQueue(proto, mAsyncPriorityQueue);
+            proto.write(MessageQueueProto.IS_POLLING_LOCKED, isPollingLocked());
+            proto.write(MessageQueueProto.IS_QUITTING, mQuitting);
+        }
+        proto.end(messageQueueToken);
+    }
+
+    /**
+     * Callback interface for discovering when a thread is going to block
+     * waiting for more messages.
+     */
+    public static interface IdleHandler {
+        /**
+         * Called when the message queue has run out of messages and will now
+         * wait for more.  Return true to keep your idle handler active, false
+         * to have it removed.  This may be called if there are still messages
+         * pending in the queue, but they are all scheduled to be dispatched
+         * after the current time.
+         */
+        boolean queueIdle();
+    }
+
+    /**
+     * A listener which is invoked when file descriptor related events occur.
+     */
+    public interface OnFileDescriptorEventListener {
+        /**
+         * File descriptor event: Indicates that the file descriptor is ready for input
+         * operations, such as reading.
+         * <p>
+         * The listener should read all available data from the file descriptor
+         * then return <code>true</code> to keep the listener active or <code>false</code>
+         * to remove the listener.
+         * </p><p>
+         * In the case of a socket, this event may be generated to indicate
+         * that there is at least one incoming connection that the listener
+         * should accept.
+         * </p><p>
+         * This event will only be generated if the {@link #EVENT_INPUT} event mask was
+         * specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_INPUT = 1 << 0;
+
+        /**
+         * File descriptor event: Indicates that the file descriptor is ready for output
+         * operations, such as writing.
+         * <p>
+         * The listener should write as much data as it needs.  If it could not
+         * write everything at once, then it should return <code>true</code> to
+         * keep the listener active.  Otherwise, it should return <code>false</code>
+         * to remove the listener then re-register it later when it needs to write
+         * something else.
+         * </p><p>
+         * This event will only be generated if the {@link #EVENT_OUTPUT} event mask was
+         * specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_OUTPUT = 1 << 1;
+
+        /**
+         * File descriptor event: Indicates that the file descriptor encountered a
+         * fatal error.
+         * <p>
+         * File descriptor errors can occur for various reasons.  One common error
+         * is when the remote peer of a socket or pipe closes its end of the connection.
+         * </p><p>
+         * This event may be generated at any time regardless of whether the
+         * {@link #EVENT_ERROR} event mask was specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_ERROR = 1 << 2;
+
+        /** @hide */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(flag = true, prefix = { "EVENT_" }, value = {
+                EVENT_INPUT,
+                EVENT_OUTPUT,
+                EVENT_ERROR
+        })
+        public @interface Events {}
+
+        /**
+         * Called when a file descriptor receives events.
+         *
+         * @param fd The file descriptor.
+         * @param events The set of events that occurred: a combination of the
+         * {@link #EVENT_INPUT}, {@link #EVENT_OUTPUT}, and {@link #EVENT_ERROR} event masks.
+         * @return The new set of events to watch, or 0 to unregister the listener.
+         *
+         * @see #EVENT_INPUT
+         * @see #EVENT_OUTPUT
+         * @see #EVENT_ERROR
+         */
+        @Events int onFileDescriptorEvents(@NonNull FileDescriptor fd, @Events int events);
+    }
+
+    private static final class FileDescriptorRecord {
+        public final FileDescriptor mDescriptor;
+        public int mEvents;
+        public OnFileDescriptorEventListener mListener;
+        public int mSeq;
+
+        public FileDescriptorRecord(FileDescriptor descriptor,
+                int events, OnFileDescriptorEventListener listener) {
+            mDescriptor = descriptor;
+            mEvents = events;
+            mListener = listener;
+        }
+    }
+}
diff --git a/core/java/android/os/Message.java b/core/java/android/os/Message.java
index 161951e..a1db9be 100644
--- a/core/java/android/os/Message.java
+++ b/core/java/android/os/Message.java
@@ -126,6 +126,10 @@
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public long when;
 
+    /** @hide */
+    @SuppressWarnings("unused")
+    public long mInsertSeq;
+
     /*package*/ Bundle data;
 
     @UnsupportedAppUsage
diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java
index 464df23..4cc057a 100644
--- a/core/java/android/os/ParcelFileDescriptor.java
+++ b/core/java/android/os/ParcelFileDescriptor.java
@@ -340,7 +340,6 @@
         return pfd;
     }
 
-    @RavenwoodReplace
     private static FileDescriptor openInternal(File file, int mode) throws FileNotFoundException {
         if ((mode & MODE_WRITE_ONLY) != 0 && (mode & MODE_APPEND) == 0
                 && (mode & MODE_TRUNCATE) == 0 && ((mode & MODE_READ_ONLY) == 0)
@@ -364,26 +363,16 @@
         }
     }
 
-    private static FileDescriptor openInternal$ravenwood(File file, int mode)
-            throws FileNotFoundException {
-        try {
-            return native_open$ravenwood(file, mode);
-        } catch (FileNotFoundException e) {
-            throw e;
-        } catch (IOException e) {
-            throw new FileNotFoundException(e.getMessage());
-        }
-    }
-
     @RavenwoodReplace
     private static void closeInternal(FileDescriptor fd) {
         IoUtils.closeQuietly(fd);
     }
 
     private static void closeInternal$ravenwood(FileDescriptor fd) {
-        // Desktop JVM doesn't have FileDescriptor.close(), so we'll need to go to the ravenwood
-        // side to close it.
-        native_close$ravenwood(fd);
+        try {
+            Os.close(fd);
+        } catch (ErrnoException ignored) {
+        }
     }
 
     /**
@@ -743,7 +732,6 @@
      * Return the total size of the file representing this fd, as determined by
      * {@code stat()}. Returns -1 if the fd is not a file.
      */
-    @RavenwoodThrow(reason = "Os.readlink() and Os.stat()")
     public long getStatSize() {
         if (mWrapped != null) {
             return mWrapped.getStatSize();
@@ -1277,32 +1265,19 @@
         }
     }
 
-    // These native methods are currently only implemented by Ravenwood, as it's the only
-    // mechanism we have to jump to our RavenwoodNativeSubstitutionClass
-    private static native void native_setFdInt$ravenwood(FileDescriptor fd, int fdInt);
-    private static native int native_getFdInt$ravenwood(FileDescriptor fd);
-    private static native FileDescriptor native_open$ravenwood(File file, int pfdMode)
-            throws IOException;
-    private static native void native_close$ravenwood(FileDescriptor fd);
+    private static native void setFdInt$ravenwood(FileDescriptor fd, int fdInt);
+    private static native int getFdInt$ravenwood(FileDescriptor fd);
 
     @RavenwoodReplace
     private static void setFdInt(FileDescriptor fd, int fdInt) {
         fd.setInt$(fdInt);
     }
 
-    private static void setFdInt$ravenwood(FileDescriptor fd, int fdInt) {
-        native_setFdInt$ravenwood(fd, fdInt);
-    }
-
     @RavenwoodReplace
     private static int getFdInt(FileDescriptor fd) {
         return fd.getInt$();
     }
 
-    private static int getFdInt$ravenwood(FileDescriptor fd) {
-        return native_getFdInt$ravenwood(fd);
-    }
-
     @RavenwoodReplace
     private void setFdOwner(FileDescriptor fd) {
         IoUtils.setFdOwner(fd, this);
@@ -1320,7 +1295,6 @@
     private int acquireRawFd$ravenwood(FileDescriptor fd) {
         // FD owners currently unsupported under Ravenwood; return FD directly
         return getFdInt(fd);
-
     }
 
     @RavenwoodReplace
diff --git a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
index 967332f..79f229a 100644
--- a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
@@ -209,7 +209,7 @@
     private volatile long mNextInsertSeqValue = 0;
     /*
      * The exception to the FIFO order rule is sendMessageAtFrontOfQueue().
-     * Those messages must be in LIFO order - SIGH.
+     * Those messages must be in LIFO order.
      * Decrements on each front of queue insert.
      */
     private static final VarHandle sNextFrontInsertSeq;
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 5ef597d..3fe063d 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -91,6 +91,8 @@
     bug: "283989236"
 }
 
+# This flag is enabled since V but not a MUST requirement in CDD yet, so it needs to stay around
+# for now and any code working with it should keep checking the flag.
 flag {
     name: "signature_permission_allowlist_enabled"
     is_fixed_read_only: true
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index e16a6a1..7ca248d 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -211,6 +211,9 @@
             SUPPRESSED_EFFECT_SCREEN_OFF | SUPPRESSED_EFFECT_FULL_SCREEN_INTENT
                     | SUPPRESSED_EFFECT_LIGHTS | SUPPRESSED_EFFECT_PEEK | SUPPRESSED_EFFECT_AMBIENT;
 
+    private static final int LEGACY_SUPPRESSED_EFFECTS =
+            Policy.SUPPRESSED_EFFECT_SCREEN_ON | Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
+
     // ZenModeConfig XML versions distinguishing key changes.
     public static final int XML_VERSION_ZEN_UPGRADE = 8;
     public static final int XML_VERSION_MODES_API = 11;
@@ -284,6 +287,7 @@
     private static final String RULE_ATT_TRIGGER_DESC = "triggerDesc";
     private static final String RULE_ATT_DELETION_INSTANT = "deletionInstant";
     private static final String RULE_ATT_DISABLED_ORIGIN = "disabledOrigin";
+    private static final String RULE_ATT_LEGACY_SUPPRESSED_EFFECTS = "legacySuppressedEffects";
 
     private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale";
     private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY =
@@ -1171,6 +1175,8 @@
             if (Flags.modesUi()) {
                 rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN,
                         UPDATE_ORIGIN_UNKNOWN);
+                rt.legacySuppressedEffects = safeInt(parser,
+                        RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, 0);
             }
         }
         return rt;
@@ -1228,6 +1234,8 @@
             }
             if (Flags.modesUi()) {
                 out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin);
+                out.attributeInt(null, RULE_ATT_LEGACY_SUPPRESSED_EFFECTS,
+                        rule.legacySuppressedEffects);
             }
         }
     }
@@ -1903,6 +1911,13 @@
                             ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST))) {
                 suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST;
             }
+
+            // Restore legacy suppressed effects (obsolete fields which are not in ZenPolicy).
+            // These are deprecated and have no effect on behavior, however apps should get them
+            // back if provided to setNotificationPolicy() earlier.
+            suppressedVisualEffects &= ~LEGACY_SUPPRESSED_EFFECTS;
+            suppressedVisualEffects |=
+                    (LEGACY_SUPPRESSED_EFFECTS & manualRule.legacySuppressedEffects);
         } else {
             if (isAllowConversations()) {
                 priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS;
@@ -1996,6 +2011,8 @@
         if (policy == null) return;
         if (Flags.modesUi()) {
             manualRule.zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy);
+            manualRule.legacySuppressedEffects =
+                    LEGACY_SUPPRESSED_EFFECTS & policy.suppressedVisualEffects;
         } else {
             setAllowAlarms((policy.priorityCategories & Policy.PRIORITY_CATEGORY_ALARMS) != 0);
             allowMedia = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_MEDIA) != 0;
@@ -2521,6 +2538,10 @@
         @Nullable public Instant deletionInstant; // Only set on deleted rules.
         @FlaggedApi(Flags.FLAG_MODES_UI)
         @ConfigChangeOrigin public int disabledOrigin = UPDATE_ORIGIN_UNKNOWN;
+        // The obsolete suppressed effects in NM.Policy (SCREEN_ON, SCREEN_OFF) cannot be put in a
+        // ZenPolicy, so we store them here, only for the manual rule.
+        @FlaggedApi(Flags.FLAG_MODES_UI)
+        int legacySuppressedEffects;
 
         public ZenRule() { }
 
@@ -2561,6 +2582,7 @@
                 }
                 if (Flags.modesUi()) {
                     disabledOrigin = source.readInt();
+                    legacySuppressedEffects = source.readInt();
                 }
             }
         }
@@ -2638,6 +2660,7 @@
                 }
                 if (Flags.modesUi()) {
                     dest.writeInt(disabledOrigin);
+                    dest.writeInt(legacySuppressedEffects);
                 }
             }
         }
@@ -2686,6 +2709,7 @@
                 }
                 if (Flags.modesUi()) {
                     sb.append(",disabledOrigin=").append(disabledOrigin);
+                    sb.append(",legacySuppressedEffects=").append(legacySuppressedEffects);
                 }
             }
 
@@ -2754,7 +2778,8 @@
 
                 if (Flags.modesUi()) {
                     finalEquals = finalEquals
-                            && other.disabledOrigin == disabledOrigin;
+                            && other.disabledOrigin == disabledOrigin
+                            && other.legacySuppressedEffects == legacySuppressedEffects;
                 }
             }
 
@@ -2769,15 +2794,15 @@
                             component, configurationActivity, pkg, id, enabler, zenPolicy,
                             zenDeviceEffects, modified, allowManualInvocation, iconResName,
                             triggerDescription, type, userModifiedFields,
-                            zenPolicyUserModifiedFields,
-                            zenDeviceEffectsUserModifiedFields, deletionInstant, disabledOrigin);
+                            zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
+                            deletionInstant, disabledOrigin, legacySuppressedEffects);
                 } else {
                     return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
                             component, configurationActivity, pkg, id, enabler, zenPolicy,
                             zenDeviceEffects, modified, allowManualInvocation, iconResName,
                             triggerDescription, type, userModifiedFields,
-                            zenPolicyUserModifiedFields,
-                            zenDeviceEffectsUserModifiedFields, deletionInstant);
+                            zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
+                            deletionInstant);
                 }
             }
             return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java
index 91ef11c..a37e227 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -472,6 +472,7 @@
         public static final String FIELD_ICON_RES = "iconResName";
         public static final String FIELD_TRIGGER_DESCRIPTION = "triggerDescription";
         public static final String FIELD_TYPE = "type";
+        public static final String FIELD_LEGACY_SUPPRESSED_EFFECTS = "legacySuppressedEffects";
         // NOTE: new field strings must match the variable names in ZenModeConfig.ZenRule
 
         // Special field to track whether this rule became active or inactive
@@ -567,6 +568,13 @@
                 if (!Objects.equals(from.iconResName, to.iconResName)) {
                     addField(FIELD_ICON_RES, new FieldDiff<>(from.iconResName, to.iconResName));
                 }
+                if (android.app.Flags.modesUi()) {
+                    if (from.legacySuppressedEffects != to.legacySuppressedEffects) {
+                        addField(FIELD_LEGACY_SUPPRESSED_EFFECTS,
+                                new FieldDiff<>(from.legacySuppressedEffects,
+                                        to.legacySuppressedEffects));
+                    }
+                }
             }
         }
 
diff --git a/core/java/android/view/SurfaceControlRegistry.java b/core/java/android/view/SurfaceControlRegistry.java
index a806bd2..121c01b 100644
--- a/core/java/android/view/SurfaceControlRegistry.java
+++ b/core/java/android/view/SurfaceControlRegistry.java
@@ -73,7 +73,7 @@
             }
             // Sort entries by time registered when dumping
             // TODO: Or should it sort by name?
-            entries.sort((o1, o2) -> (int) (o1.getValue() - o2.getValue()));
+            entries.sort((o1, o2) -> Long.compare(o1.getValue(), o2.getValue()));
             final int size = Math.min(entries.size(), limit);
 
             pw.println("SurfaceControlRegistry");
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 2ac5873..4ab6758 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -973,8 +973,12 @@
 
         @GuardedBy("mH")
         private void setCurrentRootViewLocked(ViewRootImpl rootView) {
+            final boolean wasEmpty = mCurRootView == null;
             mImeDispatcher.switchRootView(mCurRootView, rootView);
             mCurRootView = rootView;
+            if (wasEmpty && mCurRootView != null) {
+                mImeDispatcher.updateReceivingDispatcher(mCurRootView.getOnBackInvokedDispatcher());
+            }
         }
     }
 
diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java
index ce1f986..771dc7a 100644
--- a/core/java/android/window/ImeOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java
@@ -27,10 +27,12 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.util.Log;
+import android.util.Pair;
 import android.view.ViewRootImpl;
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.function.Consumer;
 
@@ -58,7 +60,7 @@
     // The handler to run callbacks on. This should be on the same thread
     // the ViewRootImpl holding IME's WindowOnBackInvokedDispatcher is created on.
     private Handler mHandler;
-
+    private final ArrayDeque<Pair<Integer, Bundle>> mQueuedReceive = new ArrayDeque<>();
     public ImeOnBackInvokedDispatcher(Handler handler) {
         mResultReceiver = new ResultReceiver(handler) {
             @Override
@@ -66,11 +68,22 @@
                 WindowOnBackInvokedDispatcher dispatcher = getReceivingDispatcher();
                 if (dispatcher != null) {
                     receive(resultCode, resultData, dispatcher);
+                } else {
+                    mQueuedReceive.add(new Pair<>(resultCode, resultData));
                 }
             }
         };
     }
 
+    /** Set receiving dispatcher to consume queued receiving events. */
+    public void updateReceivingDispatcher(@NonNull WindowOnBackInvokedDispatcher dispatcher) {
+        while (!mQueuedReceive.isEmpty()) {
+            final Pair<Integer, Bundle> queuedMessage = mQueuedReceive.poll();
+            receive(queuedMessage.first, queuedMessage.second, dispatcher);
+        }
+    }
+
+
     void setHandler(@NonNull Handler handler) {
         mHandler = handler;
     }
@@ -198,6 +211,7 @@
             }
         }
         mImeCallbacks.clear();
+        mQueuedReceive.clear();
     }
 
     @VisibleForTesting(visibility = PACKAGE)
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 2d5b02a..e5ef95c 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -204,3 +204,10 @@
     description: "Enables the tracking of the status for compat ui elements."
     bug: "350953004"
 }
+
+flag {
+    name: "enable_desktop_windowing_app_to_web_education"
+    namespace: "lse_desktop_experience"
+    description: "Enables desktop windowing app-to-web education"
+    bug: "348205896"
+}
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 80a0102..d5746e5 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -94,14 +94,6 @@
 }
 
 flag {
-    name: "activity_snapshot_by_default"
-    namespace: "systemui"
-    description: "Enable record activity snapshot by default"
-    bug: "259497289"
-    is_fixed_read_only: true
-}
-
-flag {
     name: "supports_multi_instance_system_ui"
     is_exported: true
     namespace: "multitasking"
diff --git a/core/java/com/android/internal/os/DebugStore.java b/core/java/com/android/internal/os/DebugStore.java
new file mode 100644
index 0000000..4c45fee
--- /dev/null
+++ b/core/java/com/android/internal/os/DebugStore.java
@@ -0,0 +1,247 @@
+/*
+ * 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.internal.os;
+
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Intent;
+import android.content.pm.ServiceInfo;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+
+/**
+ * The DebugStore class provides methods for recording various debug events related to service
+ * lifecycle, broadcast receivers and others.
+ * The DebugStore class facilitates debugging ANR issues by recording time-stamped events
+ * related to service lifecycles, broadcast receivers, and other framework operations. It logs
+ * the start and end times of operations within the ANR timer scope called  by framework,
+ * enabling pinpointing of methods and events contributing to ANRs.
+ *
+ * Usage currently includes recording service starts, binds, and asynchronous operations initiated
+ * by broadcast receivers, providing a granular view of system behavior that facilitates
+ * identifying performance bottlenecks and optimizing issue resolution.
+ *
+ * @hide
+ */
+public class DebugStore {
+    private static DebugStoreNative sDebugStoreNative = new DebugStoreNativeImpl();
+
+    @UnsupportedAppUsage
+    @VisibleForTesting
+    public static void setDebugStoreNative(DebugStoreNative nativeImpl) {
+        sDebugStoreNative = nativeImpl;
+    }
+    /**
+     * Records the start of a service.
+     *
+     * @param startId The start ID of the service.
+     * @param flags Additional flags for the service start.
+     * @param intent The Intent associated with the service start.
+     * @return A unique ID for the recorded event.
+     */
+    @UnsupportedAppUsage
+    public static long recordServiceOnStart(int startId, int flags, @Nullable Intent intent) {
+        return sDebugStoreNative.beginEvent(
+                "SvcStart",
+                List.of(
+                        "stId",
+                        String.valueOf(startId),
+                        "flg",
+                        Integer.toHexString(flags),
+                        "act",
+                        Objects.toString(intent != null ? intent.getAction() : null),
+                        "comp",
+                        Objects.toString(intent != null ? intent.getComponent() : null),
+                        "pkg",
+                        Objects.toString(intent != null ? intent.getPackage() : null)));
+    }
+
+    /**
+     * Records the creation of a service.
+     *
+     * @param serviceInfo Information about the service being created.
+     * @return A unique ID for the recorded event.
+     */
+    @UnsupportedAppUsage
+    public static long recordServiceCreate(@Nullable ServiceInfo serviceInfo) {
+        return sDebugStoreNative.beginEvent(
+                "SvcCreate",
+                List.of(
+                        "name",
+                        Objects.toString(serviceInfo != null ? serviceInfo.name : null),
+                        "pkg",
+                        Objects.toString(serviceInfo != null ? serviceInfo.packageName : null)));
+    }
+
+    /**
+     * Records the binding of a service.
+     *
+     * @param isRebind Indicates whether the service is being rebound.
+     * @param intent The Intent associated with the service binding.
+     * @return A unique identifier for the recorded event.
+     */
+    @UnsupportedAppUsage
+    public static long recordServiceBind(boolean isRebind, @Nullable Intent intent) {
+        return sDebugStoreNative.beginEvent(
+                "SvcBind",
+                List.of(
+                        "rebind",
+                        String.valueOf(isRebind),
+                        "act",
+                        Objects.toString(intent != null ? intent.getAction() : null),
+                        "cmp",
+                        Objects.toString(intent != null ? intent.getComponent() : null),
+                        "pkg",
+                        Objects.toString(intent != null ? intent.getPackage() : null)));
+    }
+
+    /**
+     * Records an asynchronous operation initiated by a broadcast receiver through calling GoAsync.
+     *
+     * @param receiverClassName The class name of the broadcast receiver.
+     */
+    @UnsupportedAppUsage
+    public static void recordGoAsync(String receiverClassName) {
+        sDebugStoreNative.recordEvent(
+                "GoAsync",
+                List.of(
+                        "tname",
+                        Thread.currentThread().getName(),
+                        "tid",
+                        String.valueOf(Thread.currentThread().getId()),
+                        "rcv",
+                        Objects.toString(receiverClassName)));
+    }
+
+    /**
+     * Records the completion of a broadcast operation through calling Finish.
+     *
+     * @param receiverClassName The class of the broadcast receiver that completed the operation.
+     */
+    @UnsupportedAppUsage
+    public static void recordFinish(String receiverClassName) {
+        sDebugStoreNative.recordEvent(
+                "Finish",
+                List.of(
+                        "tname",
+                        Thread.currentThread().getName(),
+                        "tid",
+                        String.valueOf(Thread.currentThread().getId()),
+                        "rcv",
+                        Objects.toString(receiverClassName)));
+    }
+    /**
+     * Records the completion of a long-running looper message.
+     *
+     * @param messageCode The code representing the type of the message.
+     * @param targetClass The FQN of the class that handled the message.
+     * @param elapsedTimeMs The time that was taken to process the message, in milliseconds.
+     */
+    @UnsupportedAppUsage
+    public static void recordLongLooperMessage(int messageCode, String targetClass,
+            long elapsedTimeMs) {
+        sDebugStoreNative.recordEvent(
+                "LooperMsg",
+                List.of(
+                        "code",
+                        String.valueOf(messageCode),
+                        "trgt",
+                        Objects.toString(targetClass),
+                        "elapsed",
+                        String.valueOf(elapsedTimeMs)));
+    }
+
+
+    /**
+     * Records the reception of a broadcast.
+     *
+     * @param intent The Intent associated with the broadcast.
+     * @return A unique ID for the recorded event.
+     */
+    @UnsupportedAppUsage
+    public static long recordBroadcastHandleReceiver(@Nullable Intent intent) {
+        return sDebugStoreNative.beginEvent(
+                "HandleReceiver",
+                List.of(
+                        "tname", Thread.currentThread().getName(),
+                        "tid", String.valueOf(Thread.currentThread().getId()),
+                        "act", Objects.toString(intent != null ? intent.getAction() : null),
+                        "cmp", Objects.toString(intent != null ? intent.getComponent() : null),
+                        "pkg", Objects.toString(intent != null ? intent.getPackage() : null)));
+    }
+
+    /**
+     * Ends a previously recorded event.
+     *
+     * @param id The unique ID of the event to be ended.
+     */
+    @UnsupportedAppUsage
+    public static void recordEventEnd(long id) {
+        sDebugStoreNative.endEvent(id, Collections.emptyList());
+    }
+
+    /**
+     * An interface for a class that acts as a wrapper for the static native methods
+     * of the Debug Store.
+     *
+     * It allows us to mock static native methods in our tests and should be removed
+     * once mocking static methods becomes easier.
+     */
+    @VisibleForTesting
+    public interface DebugStoreNative {
+        /**
+         * Begins an event with the given name and attributes.
+         */
+        long beginEvent(String eventName, List<String> attributes);
+        /**
+         * Ends an event with the given ID and attributes.
+         */
+        void endEvent(long id, List<String> attributes);
+        /**
+         * Records an event with the given name and attributes.
+         */
+        void recordEvent(String eventName, List<String> attributes);
+    }
+
+    private static class DebugStoreNativeImpl implements DebugStoreNative {
+        @Override
+        public long beginEvent(String eventName, List<String> attributes) {
+            return DebugStore.beginEventNative(eventName, attributes);
+        }
+
+        @Override
+        public void endEvent(long id, List<String> attributes) {
+            DebugStore.endEventNative(id, attributes);
+        }
+
+        @Override
+        public void recordEvent(String eventName, List<String> attributes) {
+            DebugStore.recordEventNative(eventName, attributes);
+        }
+    }
+
+    private static native long beginEventNative(String eventName, List<String> attributes);
+
+    private static native void endEventNative(long id, List<String> attributes);
+
+    private static native void recordEventNative(String eventName, List<String> attributes);
+}
diff --git a/core/java/com/android/internal/os/flags.aconfig b/core/java/com/android/internal/os/flags.aconfig
index 2ad6651..c7117e9 100644
--- a/core/java/com/android/internal/os/flags.aconfig
+++ b/core/java/com/android/internal/os/flags.aconfig
@@ -19,4 +19,12 @@
     metadata {
         purpose: PURPOSE_BUGFIX
     }
+}
+
+flag {
+    name: "debug_store_enabled"
+    namespace: "stability"
+    description: "If the debug store is enabled."
+    bug: "314735374"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/core/java/com/android/internal/widget/OWNERS b/core/java/com/android/internal/widget/OWNERS
index cf2f202..2d1c2f0 100644
--- a/core/java/com/android/internal/widget/OWNERS
+++ b/core/java/com/android/internal/widget/OWNERS
@@ -3,7 +3,9 @@
 per-file ViewPager.java = mount@google.com
 
 # LockSettings related
-per-file *LockPattern* = file:/services/core/java/com/android/server/locksettings/OWNERS
+per-file LockPatternChecker.java = file:/services/core/java/com/android/server/locksettings/OWNERS
+per-file LockPatternUtils.java = file:/services/core/java/com/android/server/locksettings/OWNERS
+per-file LockPatternView.java = file:/packages/SystemUI/OWNERS
 per-file *LockScreen* = file:/services/core/java/com/android/server/locksettings/OWNERS
 per-file *Lockscreen* = file:/services/core/java/com/android/server/locksettings/OWNERS
 per-file *LockSettings* = file:/services/core/java/com/android/server/locksettings/OWNERS
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
index 5c2a167..effbbe2 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
@@ -18,6 +18,11 @@
 import com.android.internal.widget.remotecompose.core.operations.NamedVariable;
 import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
 import com.android.internal.widget.remotecompose.core.operations.Theme;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentEnd;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStartOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.RootLayoutComponent;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -30,6 +35,9 @@
 public class CoreDocument {
 
     ArrayList<Operation> mOperations;
+
+    RootLayoutComponent mRootLayoutComponent = null;
+
     RemoteComposeState mRemoteComposeState = new RemoteComposeState();
     TimeVariables mTimeVariables = new TimeVariables();
     // Semantic version of the document
@@ -81,7 +89,6 @@
     public void setHeight(int height) {
         this.mHeight = height;
         mRemoteComposeState.setWindowHeight(height);
-
     }
 
     public RemoteComposeBuffer getBuffer() {
@@ -259,10 +266,43 @@
         translateOutput[1] = translateY;
     }
 
+    /**
+     * Returns the list of click areas
+     * @return list of click areas in document coordinates
+     */
     public Set<ClickAreaRepresentation> getClickAreas() {
         return mClickAreas;
     }
 
+    /**
+     * Returns the root layout component
+     * @return returns the root component if it exists, null otherwise
+     */
+    public RootLayoutComponent getRootLayoutComponent() {
+        return mRootLayoutComponent;
+    }
+
+    /**
+     * Invalidate the document for layout measures. This will trigger a layout remeasure pass.
+     */
+    public void invalidateMeasure() {
+        if (mRootLayoutComponent != null) {
+            mRootLayoutComponent.invalidateMeasure();
+        }
+    }
+
+    /**
+     * Returns the component with the given id
+     * @param id component id
+     * @return the component if it exists, null otherwise
+     */
+    public Component getComponent(int id) {
+        if (mRootLayoutComponent != null) {
+            return mRootLayoutComponent.getComponent(id);
+        }
+        return null;
+    }
+
     public interface ClickCallbacks {
         void click(int id, String metadata);
     }
@@ -354,7 +394,54 @@
     public void initFromBuffer(RemoteComposeBuffer buffer) {
         mOperations = new ArrayList<Operation>();
         buffer.inflateFromBuffer(mOperations);
+        mOperations = inflateComponents(mOperations);
         mBuffer = buffer;
+        for (Operation op : mOperations) {
+            if (op instanceof RootLayoutComponent) {
+                mRootLayoutComponent = (RootLayoutComponent) op;
+                break;
+            }
+        }
+        if (mRootLayoutComponent != null) {
+            mRootLayoutComponent.assignIds();
+        }
+    }
+
+    /**
+     * Inflate a component tree
+     * @param operations flat list of operations
+     * @return nested list of operations / components
+     */
+    private ArrayList<Operation> inflateComponents(ArrayList<Operation> operations) {
+        Component currentComponent = null;
+        ArrayList<Component> components = new ArrayList<>();
+        ArrayList<Operation> finalOperationsList = new ArrayList<>();
+        ArrayList<Operation> ops = finalOperationsList;
+
+        for (Operation o : operations) {
+            if (o instanceof ComponentStartOperation) {
+                Component component = (Component) o;
+                component.setParent(currentComponent);
+                components.add(component);
+                currentComponent = component;
+                ops.add(currentComponent);
+                ops = currentComponent.getList();
+            } else if (o instanceof ComponentEnd) {
+                if (currentComponent instanceof LayoutComponent) {
+                    ((LayoutComponent) currentComponent).inflate();
+                }
+                components.remove(components.size() - 1);
+                if (!components.isEmpty()) {
+                    currentComponent = components.get(components.size() - 1);
+                    ops = currentComponent.getList();
+                } else {
+                    ops = finalOperationsList;
+                }
+            } else {
+                ops.add(o);
+            }
+        }
+        return ops;
     }
 
     /**
@@ -559,6 +646,18 @@
         context.loadFloat(RemoteContext.ID_WINDOW_WIDTH, getWidth());
         context.loadFloat(RemoteContext.ID_WINDOW_HEIGHT, getHeight());
         mRepaintNext = context.updateOps();
+        if (mRootLayoutComponent != null) {
+            if (context.mWidth != mRootLayoutComponent.getWidth()
+                    || context.mHeight != mRootLayoutComponent.getHeight()) {
+                mRootLayoutComponent.invalidateMeasure();
+            }
+            if (mRootLayoutComponent.needsMeasure()) {
+                mRootLayoutComponent.layout(context);
+            }
+            if (mRootLayoutComponent.doesNeedsRepaint()) {
+                mRepaintNext = 1;
+            }
+        }
         for (Operation op : mOperations) {
             // operations will only be executed if no theme is set (ie UNSPECIFIED)
             // or the theme is equal as the one passed in argument to paint.
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operation.java b/core/java/com/android/internal/widget/remotecompose/core/Operation.java
index 7cb9a42..4a8b3d7 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Operation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operation.java
@@ -37,4 +37,3 @@
      */
     String deepToString(String indent);
 }
-
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
index 4b8dbf6..9cb024b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Operations.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
@@ -54,6 +54,21 @@
 import com.android.internal.widget.remotecompose.core.operations.TextFromFloat;
 import com.android.internal.widget.remotecompose.core.operations.TextMerge;
 import com.android.internal.widget.remotecompose.core.operations.Theme;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentEnd;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStart;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponentContent;
+import com.android.internal.widget.remotecompose.core.operations.layout.RootLayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.animation.AnimationSpec;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.BoxLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.ColumnLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.RowLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.BackgroundModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.BorderModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ClipRectModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.PaddingModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.RoundedClipRectModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.WidthModifierOperation;
 import com.android.internal.widget.remotecompose.core.operations.utilities.IntMap;
 import com.android.internal.widget.remotecompose.core.types.BooleanConstant;
 import com.android.internal.widget.remotecompose.core.types.IntegerConstant;
@@ -117,6 +132,27 @@
     public static final int INTEGER_EXPRESSION = 144;
 
     /////////////////////////////////////////======================
+
+    ////////////////////////////////////////
+    // Layout commands
+    ////////////////////////////////////////
+
+    public static final int LAYOUT_ROOT = 200;
+    public static final int LAYOUT_CONTENT = 201;
+    public static final int LAYOUT_BOX = 202;
+    public static final int LAYOUT_ROW = 203;
+    public static final int LAYOUT_COLUMN = 204;
+    public static final int COMPONENT_START = 2;
+    public static final int COMPONENT_END = 3;
+    public static final int MODIFIER_WIDTH = 16;
+    public static final int MODIFIER_HEIGHT = 67;
+    public static final int MODIFIER_BACKGROUND = 55;
+    public static final int MODIFIER_BORDER = 107;
+    public static final int MODIFIER_PADDING = 58;
+    public static final int MODIFIER_CLIP_RECT = 108;
+    public static final int MODIFIER_ROUNDED_CLIP_RECT = 54;
+    public static final int ANIMATION_SPEC = 14;
+
     public static IntMap<CompanionOperation> map = new IntMap<>();
 
     static {
@@ -162,6 +198,26 @@
         map.put(DATA_INT, IntegerConstant.COMPANION);
         map.put(INTEGER_EXPRESSION, IntegerExpression.COMPANION);
         map.put(DATA_BOOLEAN, BooleanConstant.COMPANION);
+
+        // Layout
+
+        map.put(COMPONENT_START, ComponentStart.COMPANION);
+        map.put(COMPONENT_END, ComponentEnd.COMPANION);
+        map.put(ANIMATION_SPEC, AnimationSpec.COMPANION);
+
+        map.put(MODIFIER_WIDTH, WidthModifierOperation.COMPANION);
+        map.put(MODIFIER_HEIGHT, HeightModifierOperation.COMPANION);
+        map.put(MODIFIER_PADDING, PaddingModifierOperation.COMPANION);
+        map.put(MODIFIER_BACKGROUND, BackgroundModifierOperation.COMPANION);
+        map.put(MODIFIER_BORDER, BorderModifierOperation.COMPANION);
+        map.put(MODIFIER_ROUNDED_CLIP_RECT, RoundedClipRectModifierOperation.COMPANION);
+        map.put(MODIFIER_CLIP_RECT, ClipRectModifierOperation.COMPANION);
+
+        map.put(LAYOUT_ROOT, RootLayoutComponent.COMPANION);
+        map.put(LAYOUT_CONTENT, LayoutComponentContent.COMPANION);
+        map.put(LAYOUT_BOX, BoxLayout.COMPANION);
+        map.put(LAYOUT_COLUMN, ColumnLayout.COMPANION);
+        map.put(LAYOUT_ROW, RowLayout.COMPANION);
     }
 
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
index 6d8a442..665fcb7 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
@@ -23,6 +23,10 @@
 public abstract class PaintContext {
     protected RemoteContext mContext;
 
+    public RemoteContext getContext() {
+        return mContext;
+    }
+
     public PaintContext(RemoteContext context) {
         this.mContext = context;
     }
@@ -31,6 +35,28 @@
         this.mContext = context;
     }
 
+    /**
+     * convenience function to call matrixSave()
+     */
+    public void save() {
+        matrixSave();
+    }
+
+    /**
+     * convenience function to call matrixRestore()
+     */
+    public void restore() {
+        matrixRestore();
+    }
+
+    /**
+     * convenience function to call matrixSave()
+     */
+    public void saveLayer(float x, float y, float width, float height) {
+        // TODO
+        matrixSave();
+    }
+
     public abstract void drawBitmap(int imageId,
                                     int srcLeft, int srcTop, int srcRight, int srcBottom,
                                     int dstLeft, int dstTop, int dstRight, int dstBottom,
@@ -197,8 +223,49 @@
     public abstract void clipPath(int pathId, int regionOp);
 
     /**
+     * Clip based ona  round rect
+     * @param width
+     * @param height
+     * @param topStart
+     * @param topEnd
+     * @param bottomStart
+     * @param bottomEnd
+     */
+    public abstract void roundedClipRect(float width, float height,
+                                         float topStart, float topEnd,
+                                         float bottomStart, float bottomEnd);
+
+    /**
      * Reset the paint
      */
     public abstract void reset();
+
+    /**
+     * Returns true if the context is in debug mode
+     *
+     * @return true if in debug mode, false otherwise
+     */
+    public boolean isDebug() {
+        return mContext.isDebug();
+    }
+
+    /**
+     * Returns true if layout animations are enabled
+     *
+     * @return true if animations are enabled, false otherwise
+     */
+    public boolean isAnimationEnabled() {
+        return mContext.isAnimationEnabled();
+    }
+
+    /**
+     * Utility function to log comments
+     *
+     * @param content the content to log
+     */
+    public void log(String content) {
+        System.out.println("[LOG] " + content);
+    }
+
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java b/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java
index 2f3fe57..4a1ccc9 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/PaintOperation.java
@@ -23,9 +23,11 @@
 
     @Override
     public void apply(RemoteContext context) {
-        if (context.getMode() == RemoteContext.ContextMode.PAINT
-                && context.getPaintContext() != null) {
-            paint((PaintContext) context.getPaintContext());
+        if (context.getMode() == RemoteContext.ContextMode.PAINT) {
+            PaintContext paintContext = context.getPaintContext();
+            if (paintContext != null) {
+                paint(paintContext);
+            }
         }
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
index f5f155e..333951b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
@@ -54,6 +54,18 @@
 import com.android.internal.widget.remotecompose.core.operations.TextMerge;
 import com.android.internal.widget.remotecompose.core.operations.Theme;
 import com.android.internal.widget.remotecompose.core.operations.Utils;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentEnd;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStart;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponentContent;
+import com.android.internal.widget.remotecompose.core.operations.layout.RootLayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.BoxLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.ColumnLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.RowLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.BackgroundModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.BorderModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ClipRectModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.PaddingModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.RoundedClipRectModifierOperation;
 import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
 import com.android.internal.widget.remotecompose.core.operations.utilities.easing.FloatAnimation;
 import com.android.internal.widget.remotecompose.core.types.IntegerConstant;
@@ -132,8 +144,9 @@
      * @param contentDescription content description of the document
      * @param capabilities       bitmask indicating needed capabilities (unused for now)
      */
-    public void header(int width, int height, String contentDescription, long capabilities) {
-        Header.COMPANION.apply(mBuffer, width, height, capabilities);
+    public void header(int width, int height, String contentDescription,
+                       float density, long capabilities) {
+        Header.COMPANION.apply(mBuffer, width, height, density, capabilities);
         int contentDescriptionId = 0;
         if (contentDescription != null) {
             contentDescriptionId = addText(contentDescription);
@@ -149,7 +162,7 @@
      * @param contentDescription content description of the document
      */
     public void header(int width, int height, String contentDescription) {
-        header(width, height, contentDescription, 0);
+        header(width, height, contentDescription, 1f, 0);
     }
 
     /**
@@ -857,7 +870,7 @@
     }
 
     /**
-     * Sets the clip based on clip rec
+     * Sets the clip based on clip rect
      * @param left
      * @param top
      * @param right
@@ -1074,5 +1087,128 @@
                 NamedVariable.COLOR_TYPE, name);
     }
 
+    /**
+     * Add a component start tag
+     * @param type type of component
+     * @param id component id
+     */
+    public void addComponentStart(int type, int id) {
+        switch (type) {
+            case ComponentStart.ROOT_LAYOUT: {
+                RootLayoutComponent.COMPANION.apply(mBuffer);
+            } break;
+            case ComponentStart.LAYOUT_CONTENT: {
+                LayoutComponentContent.COMPANION.apply(mBuffer);
+            } break;
+            case ComponentStart.LAYOUT_BOX: {
+                BoxLayout.COMPANION.apply(mBuffer, id, -1,
+                        BoxLayout.CENTER, BoxLayout.CENTER);
+            } break;
+            case ComponentStart.LAYOUT_ROW: {
+                RowLayout.COMPANION.apply(mBuffer, id, -1,
+                        RowLayout.START, RowLayout.TOP, 0f);
+            } break;
+            case ComponentStart.LAYOUT_COLUMN: {
+                ColumnLayout.COMPANION.apply(mBuffer, id, -1,
+                        ColumnLayout.START, ColumnLayout.TOP, 0f);
+            } break;
+            default:
+                ComponentStart.Companion.apply(mBuffer,
+                        type, id, 0f, 0f);
+        }
+    }
+
+    /**
+     * Add a component start tag
+     * @param type type of component
+     */
+    public void addComponentStart(int type) {
+        addComponentStart(type, -1);
+    }
+
+    /**
+     * Add a component end tag
+     */
+    public void addComponentEnd() {
+        ComponentEnd.Companion.apply(mBuffer);
+    }
+
+    /**
+     * Add a background modifier of provided color
+     * @param color the color of the background
+     * @param shape the background shape -- SHAPE_RECTANGLE, SHAPE_CIRCLE
+     */
+    public void addModifierBackground(int color, int shape) {
+        float r = ((color >> 16) & 0xff) / 255.0f;
+        float g = ((color >> 8) & 0xff) / 255.0f;
+        float b = ((color) & 0xff) / 255.0f;
+        float a = ((color >> 24) & 0xff) / 255.0f;
+        BackgroundModifierOperation.COMPANION.apply(mBuffer, 0f, 0f, 0f, 0f,
+                r, g, b, a, shape);
+    }
+
+    /**
+     * Add a border modifier
+     * @param borderWidth the border width
+     * @param borderRoundedCorner the rounded corner radius if the shape is ROUNDED_RECT
+     * @param color the color of the border
+     * @param shape the shape of the border
+     */
+    public void addModifierBorder(float borderWidth, float borderRoundedCorner,
+                                  int color, int shape) {
+        float r = ((color >> 16) & 0xff) / 255.0f;
+        float g = ((color >>  8) & 0xff) / 255.0f;
+        float b = ((color) & 0xff) / 255.0f;
+        float a = ((color >> 24) & 0xff) / 255.0f;
+        BorderModifierOperation.COMPANION.apply(mBuffer, 0f, 0f, 0f, 0f,
+                borderWidth, borderRoundedCorner, r, g, b, a, shape);
+    }
+
+    /**
+     * Add a padding modifier
+     * @param left left padding
+     * @param top top padding
+     * @param right right padding
+     * @param bottom bottom padding
+     */
+    public void addModifierPadding(float left, float top, float right, float bottom) {
+        PaddingModifierOperation.COMPANION.apply(mBuffer, left, top, right, bottom);
+    }
+
+
+    /**
+     * Sets the clip based on rounded clip rect
+     * @param topStart
+     * @param topEnd
+     * @param bottomStart
+     * @param bottomEnd
+     */
+    public void addRoundClipRectModifier(float topStart, float topEnd,
+                                         float bottomStart, float bottomEnd) {
+        RoundedClipRectModifierOperation.COMPANION.apply(mBuffer,
+                topStart, topEnd, bottomStart, bottomEnd);
+    }
+
+    public void addClipRectModifier() {
+        ClipRectModifierOperation.COMPANION.apply(mBuffer);
+    }
+
+    public void addBoxStart(int componentId, int animationId,
+                            int horizontal, int vertical) {
+        BoxLayout.COMPANION.apply(mBuffer, componentId, animationId,
+                horizontal, vertical);
+    }
+
+    public void addRowStart(int componentId, int animationId,
+                            int horizontal, int vertical, float spacedBy) {
+        RowLayout.COMPANION.apply(mBuffer, componentId, animationId,
+                horizontal, vertical, spacedBy);
+    }
+
+    public void addColumnStart(int componentId, int animationId,
+                            int horizontal, int vertical, float spacedBy) {
+        ColumnLayout.COMPANION.apply(mBuffer, componentId, animationId,
+                horizontal, vertical, spacedBy);
+    }
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
index 41eeb5b..893dcce 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
@@ -19,6 +19,7 @@
 import com.android.internal.widget.remotecompose.core.operations.ShaderData;
 import com.android.internal.widget.remotecompose.core.operations.Theme;
 import com.android.internal.widget.remotecompose.core.operations.Utils;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
 
 /**
  * Specify an abstract context used to playback RemoteCompose documents
@@ -35,12 +36,26 @@
     ContextMode mMode = ContextMode.UNSET;
 
     boolean mDebug = false;
+
     private int mTheme = Theme.UNSPECIFIED;
 
     public float mWidth = 0f;
     public float mHeight = 0f;
     private float mAnimationTime;
 
+    private boolean mAnimate = true;
+
+    public Component lastComponent;
+    public long currentTime = 0L;
+
+    public boolean isAnimationEnabled() {
+        return mAnimate;
+    }
+
+    public void setAnimationEnabled(boolean value) {
+        mAnimate = value;
+    }
+
     /**
      * Load a path under an id.
      * Paths can be use in clip drawPath and drawTweenPath
@@ -333,9 +348,11 @@
     public static final float FLOAT_COMPONENT_HEIGHT = Utils.asNan(ID_COMPONENT_HEIGHT);
     // ID_OFFSET_TO_UTC is the offset from UTC in sec (typically / 3600f)
     public static final float FLOAT_OFFSET_TO_UTC = Utils.asNan(ID_OFFSET_TO_UTC);
+
     ///////////////////////////////////////////////////////////////////////////////////////////////
     // Click handling
     ///////////////////////////////////////////////////////////////////////////////////////////////
+
     public abstract void addClickArea(
             int id,
             int contentDescription,
diff --git a/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentationBuilder.java b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentationBuilder.java
new file mode 100644
index 0000000..ccbcdf6
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentationBuilder.java
@@ -0,0 +1,22 @@
+/*
+ * 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.internal.widget.remotecompose.core.documentation;
+
+public interface DocumentationBuilder {
+    void add(String value);
+    Operation operation(String category, int id, String name);
+    Operation wipOperation(String category, int id, String name);
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedCompanionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedCompanionOperation.java
new file mode 100644
index 0000000..6a98b78
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/DocumentedCompanionOperation.java
@@ -0,0 +1,22 @@
+/*
+ * 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.internal.widget.remotecompose.core.documentation;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+
+public interface DocumentedCompanionOperation extends CompanionOperation {
+    void documentation(DocumentationBuilder doc);
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/documentation/Operation.java b/core/java/com/android/internal/widget/remotecompose/core/documentation/Operation.java
new file mode 100644
index 0000000..643b925
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/Operation.java
@@ -0,0 +1,151 @@
+/*
+ * 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.internal.widget.remotecompose.core.documentation;
+
+import java.util.ArrayList;
+
+public class Operation {
+    public static final int LAYOUT = 0;
+    public static final int INT = 0;
+    public static final int FLOAT = 1;
+    public static final int BOOLEAN = 2;
+    public static final int BUFFER = 4;
+    public static final int UTF8 = 5;
+    public static final int BYTE = 6;
+    public static final int VALUE = 7;
+    public static final int LONG = 8;
+
+    String mCategory;
+    int mId;
+    String mName;
+    String mDescription;
+
+    boolean mWIP;
+    String mTextExamples;
+
+    ArrayList<StringPair> mExamples = new ArrayList<>();
+    ArrayList<OperationField> mFields = new ArrayList<>();
+
+    int mExamplesWidth = 100;
+    int mExamplesHeight = 100;
+
+
+    public static String getType(int type) {
+        switch (type) {
+            case (INT): return "INT";
+            case (FLOAT): return "FLOAT";
+            case (BOOLEAN): return "BOOLEAN";
+            case (BUFFER): return "BUFFER";
+            case (UTF8): return "UTF8";
+            case (BYTE): return "BYTE";
+            case (VALUE): return "VALUE";
+            case (LONG): return "LONG";
+        }
+        return "UNKNOWN";
+    }
+
+    public Operation(String category, int id, String name, boolean wip) {
+        mCategory = category;
+        mId = id;
+        mName = name;
+        mWIP = wip;
+    }
+
+    public Operation(String category, int id, String name) {
+        this(category, id, name, false);
+    }
+
+    public ArrayList<OperationField> getFields() {
+        return mFields;
+    }
+
+    public String getCategory() {
+        return mCategory;
+    }
+
+    public int getId() {
+        return mId;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public boolean isWIP() {
+        return mWIP;
+    }
+
+    public int getSizeFields() {
+        int size = 0;
+        for (OperationField field : mFields) {
+            size += field.getSize();
+        }
+        return size;
+    }
+
+    public String getDescription() {
+        return mDescription;
+    }
+
+    public String getTextExamples() {
+        return mTextExamples;
+    }
+
+    public ArrayList<StringPair> getExamples() {
+        return mExamples;
+    }
+
+    public int getExamplesWidth() {
+        return mExamplesWidth;
+    }
+
+    public int getExamplesHeight() {
+        return mExamplesHeight;
+    }
+
+    public Operation field(int type, String name, String description) {
+        mFields.add(new OperationField(type, name, description));
+        return this;
+    }
+
+    public Operation possibleValues(String name, int value) {
+        if (!mFields.isEmpty()) {
+            mFields.get(mFields.size() - 1).possibleValue(name, "" + value);
+        }
+        return this;
+    }
+
+    public Operation description(String description) {
+        mDescription = description;
+        return this;
+    }
+
+    public Operation examples(String examples) {
+        mTextExamples = examples;
+        return this;
+    }
+
+    public Operation exampleImage(String name, String imagePath) {
+        mExamples.add(new StringPair(name, imagePath));
+        return this;
+    }
+
+    public Operation examplesDimension(int width, int height) {
+        mExamplesWidth = width;
+        mExamplesHeight = height;
+        return this;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/documentation/OperationField.java b/core/java/com/android/internal/widget/remotecompose/core/documentation/OperationField.java
new file mode 100644
index 0000000..fc73f4ed6
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/OperationField.java
@@ -0,0 +1,58 @@
+/*
+ * 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.internal.widget.remotecompose.core.documentation;
+
+import java.util.ArrayList;
+
+public class OperationField {
+    int mType;
+    String mName;
+    String mDescription;
+    ArrayList<StringPair> mPossibleValues = new ArrayList<>();
+
+    public OperationField(int type, String name, String description) {
+        mType = type;
+        mName = name;
+        mDescription = description;
+    }
+    public int getType() {
+        return mType;
+    }
+    public String getName() {
+        return mName;
+    }
+    public String getDescription() {
+        return mDescription;
+    }
+    public ArrayList<StringPair> getPossibleValues() {
+        return mPossibleValues;
+    }
+    public void possibleValue(String name, String value) {
+        mPossibleValues.add(new StringPair(name, value));
+    }
+    public boolean hasEnumeratedValues() {
+        return !mPossibleValues.isEmpty();
+    }
+    public int getSize() {
+        switch (mType) {
+            case (Operation.BYTE) : return 1;
+            case (Operation.INT) : return 4;
+            case (Operation.FLOAT) : return 4;
+            case (Operation.LONG) : return 8;
+            default : return 0;
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/documentation/StringPair.java b/core/java/com/android/internal/widget/remotecompose/core/documentation/StringPair.java
new file mode 100644
index 0000000..787bb54
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/documentation/StringPair.java
@@ -0,0 +1,32 @@
+/*
+ * 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.internal.widget.remotecompose.core.documentation;
+public class StringPair {
+    String mName;
+    String mValue;
+
+    StringPair(String name, String value) {
+        mName = name;
+        mValue = value;
+    }
+
+    public String getName() {
+        return mName;
+    }
+    public String getValue() {
+        return mValue;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java
index ec35a16..53a3aa9 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java
@@ -41,10 +41,10 @@
                 }
             };
     protected String mName = "DrawRectBase";
-    float mX1;
-    float mY1;
-    float mX2;
-    float mY2;
+    protected float mX1;
+    protected float mY1;
+    protected float mX2;
+    protected float mY2;
     float mX1Value;
     float mY1Value;
     float mX2Value;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
index aabed15..9a1f37b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
@@ -15,12 +15,16 @@
  */
 package com.android.internal.widget.remotecompose.core.operations;
 
-import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.INT;
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.LONG;
+
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
 import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
 import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
 
 import java.util.List;
 
@@ -41,6 +45,8 @@
 
     int mWidth;
     int mHeight;
+
+    float mDensity;
     long mCapabilities;
 
     public static final Companion COMPANION = new Companion();
@@ -54,21 +60,23 @@
      * @param patchVersion the patch version of the RemoteCompose document API
      * @param width        the width of the RemoteCompose document
      * @param height       the height of the RemoteCompose document
+     * @param density      the density at which the document was originally created
      * @param capabilities bitmask field storing needed capabilities (unused for now)
      */
     public Header(int majorVersion, int minorVersion, int patchVersion,
-                  int width, int height, long capabilities) {
+                  int width, int height, float density, long capabilities) {
         this.mMajorVersion = majorVersion;
         this.mMinorVersion = minorVersion;
         this.mPatchVersion = patchVersion;
         this.mWidth = width;
         this.mHeight = height;
+        this.mDensity = density;
         this.mCapabilities = capabilities;
     }
 
     @Override
     public void write(WireBuffer buffer) {
-        COMPANION.apply(buffer, mWidth, mHeight, mCapabilities);
+        COMPANION.apply(buffer, mWidth, mHeight, mDensity, mCapabilities);
     }
 
     @Override
@@ -88,7 +96,7 @@
         return toString();
     }
 
-    public static class Companion implements CompanionOperation {
+    public static class Companion implements DocumentedCompanionOperation {
         private Companion() {
         }
 
@@ -102,13 +110,15 @@
             return Operations.HEADER;
         }
 
-        public void apply(WireBuffer buffer, int width, int height, long capabilities) {
+        public void apply(WireBuffer buffer, int width, int height,
+                          float density, long capabilities) {
             buffer.start(Operations.HEADER);
             buffer.writeInt(MAJOR_VERSION); // major version number of the protocol
             buffer.writeInt(MINOR_VERSION); // minor version number of the protocol
             buffer.writeInt(PATCH_VERSION); // patch version number of the protocol
             buffer.writeInt(width);
             buffer.writeInt(height);
+            // buffer.writeFloat(density);
             buffer.writeLong(capabilities);
         }
 
@@ -119,10 +129,26 @@
             int patchVersion = buffer.readInt();
             int width = buffer.readInt();
             int height = buffer.readInt();
+            // float density = buffer.readFloat();
+            float density = 1f;
             long capabilities = buffer.readLong();
             Header header = new Header(majorVersion, minorVersion, patchVersion,
-                    width, height, capabilities);
+                    width, height, density, capabilities);
             operations.add(header);
         }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Protocol Operations", id(), name())
+                    .description("Document metadata, containing the version,"
+                          + " original size & density, capabilities mask")
+                    .field(INT, "MAJOR_VERSION", "Major version")
+                    .field(INT, "MINOR_VERSION", "Minor version")
+                    .field(INT, "PATCH_VERSION", "Patch version")
+                    .field(INT, "WIDTH", "Major version")
+                    .field(INT, "HEIGHT", "Major version")
+                    // .field(FLOAT, "DENSITY", "Major version")
+                    .field(LONG, "CAPABILITIES", "Major version");
+        }
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java
index cbe9c12..f982997 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Theme.java
@@ -15,12 +15,15 @@
  */
 package com.android.internal.widget.remotecompose.core.operations;
 
-import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.INT;
+
 import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.Operations;
 import com.android.internal.widget.remotecompose.core.RemoteComposeOperation;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
 import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
 
 import java.util.List;
 
@@ -70,12 +73,12 @@
         return indent + toString();
     }
 
-    public static class Companion implements CompanionOperation {
+    public static class Companion implements DocumentedCompanionOperation {
         private Companion() {}
 
         @Override
         public String name() {
-            return "SetTheme";
+            return "Theme";
         }
 
         @Override
@@ -93,5 +96,15 @@
             int theme = buffer.readInt();
             operations.add(new Theme(theme));
         }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Protocol Operations", id(), name())
+                    .description("Set a theme")
+                    .field(INT, "THEME", "theme id")
+                    .possibleValues("UNSPECIFIED", Theme.UNSPECIFIED)
+                    .possibleValues("DARK", Theme.DARK)
+                    .possibleValues("LIGHT", Theme.LIGHT);
+        }
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java
new file mode 100644
index 0000000..ee2e11b
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java
@@ -0,0 +1,473 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.layout.animation.AnimateMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.animation.AnimationSpec;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Measurable;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentModifiers;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.ArrayList;
+
+/**
+ * Generic Component class
+ */
+public class Component extends PaintOperation implements Measurable {
+
+    protected int mComponentId = -1;
+    protected float mX;
+    protected float mY;
+    protected float mWidth;
+    protected float mHeight;
+    protected Component mParent;
+    protected int mAnimationId = -1;
+    public Visibility mVisibility = Visibility.VISIBLE;
+    public ArrayList<Operation> mList = new ArrayList<>();
+    public PaintOperation mPreTranslate;
+    public boolean mNeedsMeasure = true;
+    public boolean mNeedsRepaint = false;
+    public AnimateMeasure mAnimateMeasure;
+    public AnimationSpec mAnimationSpec = new AnimationSpec();
+    public boolean mFirstLayout = true;
+    PaintBundle mPaint = new PaintBundle();
+
+    public ArrayList<Operation> getList() {
+        return mList;
+    }
+    public float getX() {
+        return mX;
+    }
+    public float getY() {
+        return mY;
+    }
+    public float getWidth() {
+        return mWidth;
+    }
+    public float getHeight() {
+        return mHeight;
+    }
+    public int getComponentId() {
+        return mComponentId;
+    }
+
+    public int getAnimationId() {
+        return mAnimationId;
+    }
+
+    public Component getParent() {
+        return mParent;
+    }
+    public void setX(float value) {
+        mX = value;
+    }
+    public void setY(float value) {
+        mY = value;
+    }
+    public void setWidth(float value) {
+        mWidth = value;
+    }
+    public void setHeight(float value) {
+        mHeight = value;
+    }
+
+    public void setComponentId(int id) {
+        mComponentId = id;
+    }
+
+    public void setAnimationId(int id) {
+        mAnimationId = id;
+    }
+
+    public Component(Component parent, int componentId, int animationId,
+                     float x, float y, float width, float height) {
+        this.mComponentId = componentId;
+        this.mX = x;
+        this.mY = y;
+        this.mWidth = width;
+        this.mHeight = height;
+        this.mParent = parent;
+        this.mAnimationId = animationId;
+    }
+
+    public Component(int componentId, float x, float y, float width, float height,
+                     Component parent) {
+        this(parent, componentId, -1, x, y, width, height);
+    }
+
+    public Component(Component component) {
+        this(component.mParent, component.mComponentId, component.mAnimationId,
+                component.mX, component.mY, component.mWidth, component.mHeight
+        );
+        mList.addAll(component.mList);
+        finalizeCreation();
+    }
+
+    public void finalizeCreation() {
+        for (Operation op : mList) {
+            if (op instanceof Component) {
+                ((Component) op).mParent = this;
+            }
+            if (op instanceof AnimationSpec) {
+                mAnimationSpec = (AnimationSpec) op;
+                mAnimationId = mAnimationSpec.getAnimationId();
+            }
+        }
+    }
+
+    @Override
+    public boolean needsMeasure() {
+        return mNeedsMeasure;
+    }
+
+    public void setParent(Component parent) {
+        mParent = parent;
+    }
+
+    public enum Visibility {
+        VISIBLE,
+        INVISIBLE,
+        GONE
+    }
+
+    public boolean isVisible() {
+        if (mVisibility != Visibility.VISIBLE || mParent == null) {
+            return mVisibility == Visibility.VISIBLE;
+        }
+        if (mParent != null) {
+            return mParent.isVisible();
+        }
+        return true;
+    }
+
+    @Override
+    public void measure(PaintContext context, float minWidth, float maxWidth,
+                        float minHeight, float maxHeight, MeasurePass measure) {
+        ComponentMeasure m = measure.get(this);
+        m.setW(mWidth);
+        m.setH(mHeight);
+    }
+
+    @Override
+    public void layout(RemoteContext context, MeasurePass measure) {
+        ComponentMeasure m = measure.get(this);
+        if (!mFirstLayout && context.isAnimationEnabled()) {
+            if (mAnimateMeasure == null) {
+                ComponentMeasure origin = new ComponentMeasure(mComponentId,
+                        mX, mY, mWidth, mHeight, mVisibility);
+                ComponentMeasure target = new ComponentMeasure(mComponentId,
+                        m.getX(), m.getY(), m.getW(), m.getH(), m.getVisibility());
+                mAnimateMeasure = new AnimateMeasure(context.currentTime, this,
+                        origin, target,
+                        mAnimationSpec.getMotionDuration(), mAnimationSpec.getVisibilityDuration(),
+                        mAnimationSpec.getEnterAnimation(), mAnimationSpec.getExitAnimation(),
+                        mAnimationSpec.getMotionEasingType(),
+                        mAnimationSpec.getVisibilityEasingType());
+            } else {
+                mAnimateMeasure.updateTarget(m, context.currentTime);
+            }
+        } else {
+            mVisibility = m.getVisibility();
+        }
+        mWidth = m.getW();
+        mHeight = m.getH();
+        setLayoutPosition(m.getX(), m.getY());
+        mFirstLayout = false;
+    }
+
+    public float[] locationInWindow = new float[2];
+
+    public boolean contains(float x, float y) {
+        locationInWindow[0] = 0f;
+        locationInWindow[1] = 0f;
+        getLocationInWindow(locationInWindow);
+        float lx1 = locationInWindow[0];
+        float lx2 = lx1 + mWidth;
+        float ly1 = locationInWindow[1];
+        float ly2 = ly1 + mHeight;
+        return x >= lx1 && x < lx2 && y >= ly1 && y < ly2;
+    }
+
+    public void onClick(float x, float y) {
+        if (!contains(x, y)) {
+            return;
+        }
+        for (Operation op : mList) {
+            if (op instanceof Component) {
+                ((Component) op).onClick(x, y);
+            }
+            if (op instanceof ComponentModifiers) {
+                ((ComponentModifiers) op).onClick(x, y);
+            }
+        }
+    }
+
+    public void getLocationInWindow(float[] value) {
+        value[0] += mX;
+        value[1] += mY;
+        if (mParent != null && mParent instanceof Component) {
+            if (mParent instanceof LayoutComponent) {
+                value[0] += ((LayoutComponent) mParent).getMarginLeft();
+                value[1] += ((LayoutComponent) mParent).getMarginTop();
+            }
+            mParent.getLocationInWindow(value);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "COMPONENT(<" + mComponentId + "> " + getClass().getSimpleName()
+                + ") [" + mX + "," + mY + " - " + mWidth + " x " + mHeight + "] " + textContent()
+                + " Visibility (" + mVisibility + ") ";
+    }
+
+    protected String getSerializedName() {
+        return "COMPONENT";
+    }
+
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(indent, getSerializedName() + " [" + mComponentId
+                + ":" + mAnimationId + "] = "
+                + "[" + mX + ", " + mY + ", " + mWidth + ", " + mHeight + "] "
+                + mVisibility
+        //        + " [" + mNeedsMeasure + ", " + mNeedsRepaint + "]"
+        );
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        // nothing
+    }
+
+    /**
+     * Returns the top-level RootLayoutComponent
+     */
+    public RootLayoutComponent getRoot() throws Exception {
+        if (this instanceof RootLayoutComponent) {
+            return (RootLayoutComponent) this;
+        }
+        Component p = mParent;
+        while (!(p instanceof RootLayoutComponent)) {
+            if (p == null) {
+                throw new Exception("No RootLayoutComponent found");
+            }
+            p = p.mParent;
+        }
+        return (RootLayoutComponent) p;
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        StringBuilder builder = new StringBuilder();
+        builder.append(indent);
+        builder.append(toString());
+        builder.append("\n");
+        String indent2 = "  " + indent;
+        for (Operation op : mList) {
+            builder.append(op.deepToString(indent2));
+            builder.append("\n");
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Mark itself as needing to be remeasured, and walk back up the tree
+     * to mark each parents as well.
+     */
+    public void invalidateMeasure() {
+        needsRepaint();
+        mNeedsMeasure = true;
+        Component p = mParent;
+        while (p != null) {
+            p.mNeedsMeasure = true;
+            p = p.mParent;
+        }
+    }
+
+    public void needsRepaint() {
+        try {
+            getRoot().mNeedsRepaint = true;
+        } catch (Exception e) {
+            // nothing
+        }
+    }
+
+    public String content() {
+        StringBuilder builder = new StringBuilder();
+        for (Operation op : mList) {
+            builder.append("- ");
+            builder.append(op);
+            builder.append("\n");
+        }
+        return builder.toString();
+    }
+
+    public String textContent() {
+        StringBuilder builder = new StringBuilder();
+        for (Operation op : mList) {
+            String letter = "";
+            // if (op instanceof DrawTextRun) {
+            //   letter = "[" + ((DrawTextRun) op).text + "]";
+            // }
+            builder.append(letter);
+        }
+        return builder.toString();
+    }
+
+    public void debugBox(Component component, PaintContext context) {
+        float width = component.mWidth;
+        float height = component.mHeight;
+
+        context.savePaint();
+        mPaint.reset();
+        mPaint.setColor(0, 0, 255, 255); // Blue color
+        context.applyPaint(mPaint);
+        context.drawLine(0f, 0f, width, 0f);
+        context.drawLine(width, 0f, width, height);
+        context.drawLine(width, height, 0f, height);
+        context.drawLine(0f, height, 0f, 0f);
+        //        context.setColor(255, 0, 0, 255)
+        //        context.drawLine(0f, 0f, width, height)
+        //        context.drawLine(0f, height, width, 0f)
+        context.restorePaint();
+    }
+
+    public void setLayoutPosition(float x, float y) {
+        this.mX = x;
+        this.mY = y;
+    }
+
+    public float getTranslateX() {
+        if (mParent != null) {
+            return mX - mParent.mX;
+        }
+        return 0f;
+    }
+
+    public float getTranslateY() {
+        if (mParent != null) {
+            return mY - mParent.mY;
+        }
+        return 0f;
+    }
+
+    public void paintingComponent(PaintContext context) {
+        if (mPreTranslate != null) {
+            mPreTranslate.paint(context);
+        }
+        context.save();
+        context.translate(mX, mY);
+        if (context.isDebug()) {
+            debugBox(this, context);
+        }
+        for (Operation op : mList) {
+            if (op instanceof PaintOperation) {
+                ((PaintOperation) op).paint(context);
+            }
+        }
+        context.restore();
+    }
+
+    public boolean applyAnimationAsNeeded(PaintContext context) {
+        if (context.isAnimationEnabled() && mAnimateMeasure != null) {
+            mAnimateMeasure.apply(context);
+            needsRepaint();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        if (context.isDebug()) {
+            context.save();
+            context.translate(mX, mY);
+            context.savePaint();
+            mPaint.reset();
+            mPaint.setColor(0, 255, 0, 255); // Green
+            context.applyPaint(mPaint);
+            context.drawLine(0f, 0f, mWidth, 0f);
+            context.drawLine(mWidth, 0f, mWidth, mHeight);
+            context.drawLine(mWidth, mHeight, 0f, mHeight);
+            context.drawLine(0f, mHeight, 0f, 0f);
+            mPaint.setColor(255, 0, 0, 255); // Red
+            context.applyPaint(mPaint);
+            context.drawLine(0f, 0f, mWidth, mHeight);
+            context.drawLine(0f, mHeight, mWidth, 0f);
+            context.restorePaint();
+            context.restore();
+        }
+        if (applyAnimationAsNeeded(context)) {
+            return;
+        }
+        if (mVisibility == Visibility.GONE) {
+            return;
+        }
+        paintingComponent(context);
+    }
+
+    public void getComponents(ArrayList<Component> components) {
+        for (Operation op : mList) {
+            if (op instanceof Component) {
+                components.add((Component) op);
+            }
+        }
+    }
+
+    public int getComponentCount() {
+        int count = 0;
+        for (Operation op : mList) {
+            if (op instanceof Component) {
+                count += 1 + ((Component) op).getComponentCount();
+            }
+        }
+        return count;
+    }
+
+    public int getPaintId() {
+        if (mAnimationId != -1) {
+            return mAnimationId;
+        }
+        return mComponentId;
+    }
+
+    public boolean doesNeedsRepaint() {
+        return mNeedsRepaint;
+    }
+
+    public Component getComponent(int cid) {
+        if (mComponentId == cid || mAnimationId == cid) {
+            return this;
+        }
+        for (Operation c : mList) {
+            if (c instanceof Component) {
+                Component search = ((Component) c).getComponent(cid);
+                if (search != null) {
+                    return search;
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentEnd.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentEnd.java
new file mode 100644
index 0000000..8a523a2
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentEnd.java
@@ -0,0 +1,82 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+
+import java.util.List;
+
+public class ComponentEnd implements Operation {
+
+    public static final ComponentEnd.Companion COMPANION = new ComponentEnd.Companion();
+
+    @Override
+    public void write(WireBuffer buffer) {
+        Companion.apply(buffer);
+    }
+
+    @Override
+    public String toString() {
+        return "COMPONENT_END";
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+        // nothing
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return (indent != null ? indent : "") + toString();
+    }
+
+    public static class Companion implements DocumentedCompanionOperation {
+        @Override
+        public String name() {
+            return "ComponentEnd";
+        }
+
+        @Override
+        public int id() {
+            return Operations.COMPONENT_END;
+        }
+
+        public static void apply(WireBuffer buffer) {
+            buffer.start(Operations.COMPONENT_END);
+        }
+
+        public static int size() {
+            return 1 + 4 + 4 + 4;
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            operations.add(new ComponentEnd());
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+                    .description("End tag for components / layouts. This operation marks the end"
+                            + "of a component");
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStart.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStart.java
new file mode 100644
index 0000000..5cfad25
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStart.java
@@ -0,0 +1,193 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.FLOAT;
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.INT;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+
+import java.util.List;
+
+public class ComponentStart implements ComponentStartOperation {
+
+    public static final ComponentStart.Companion COMPANION = new ComponentStart.Companion();
+
+    int mType = DEFAULT;
+    float mX;
+    float mY;
+    float mWidth;
+    float mHeight;
+    int mComponentId;
+
+    public int getType() {
+        return mType;
+    }
+
+    public float getX() {
+        return mX;
+    }
+
+    public float getY() {
+        return mY;
+    }
+
+    public float getWidth() {
+        return mWidth;
+    }
+
+    public float getHeight() {
+        return mHeight;
+    }
+
+    public int getComponentId() {
+        return mComponentId;
+    }
+
+    public ComponentStart(int type, int componentId, float width, float height) {
+        this.mType = type;
+        this.mComponentId = componentId;
+        this.mX = 0f;
+        this.mY = 0f;
+        this.mWidth = width;
+        this.mHeight = height;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        Companion.apply(buffer, mType, mComponentId, mWidth, mHeight);
+    }
+
+    @Override
+    public String toString() {
+        return "COMPONENT_START (type " + mType + " " + Companion.typeDescription(mType)
+                + ") - (" + mX + ", " + mY + " - " + mWidth + " x " + mHeight + ")";
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return (indent != null ? indent : "") + toString();
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+        // nothing
+    }
+
+    public static final int UNKNOWN = -1;
+    public static final int DEFAULT = 0;
+    public static final int ROOT_LAYOUT = 1;
+    public static final int LAYOUT = 2;
+    public static final int LAYOUT_CONTENT = 3;
+    public static final int SCROLL_CONTENT = 4;
+    public static final int BUTTON = 5;
+    public static final int CHECKBOX = 6;
+    public static final int TEXT = 7;
+    public static final int CURVED_TEXT = 8;
+    public static final int STATE_HOST = 9;
+    public static final int CUSTOM = 10;
+    public static final int LOTTIE = 11;
+    public static final int IMAGE = 12;
+    public static final int STATE_BOX_CONTENT = 13;
+    public static final int LAYOUT_BOX = 14;
+    public static final int LAYOUT_ROW = 15;
+    public static final int LAYOUT_COLUMN = 16;
+
+    public static class Companion implements DocumentedCompanionOperation {
+
+
+        public static String typeDescription(int type) {
+            switch (type) {
+                case DEFAULT:
+                    return "DEFAULT";
+                case ROOT_LAYOUT:
+                    return "ROOT_LAYOUT";
+                case LAYOUT:
+                    return "LAYOUT";
+                case LAYOUT_CONTENT:
+                    return "CONTENT";
+                case SCROLL_CONTENT:
+                    return "SCROLL_CONTENT";
+                case BUTTON:
+                    return "BUTTON";
+                case CHECKBOX:
+                    return "CHECKBOX";
+                case TEXT:
+                    return "TEXT";
+                case CURVED_TEXT:
+                    return "CURVED_TEXT";
+                case STATE_HOST:
+                    return "STATE_HOST";
+                case LOTTIE:
+                    return "LOTTIE";
+                case CUSTOM:
+                    return "CUSTOM";
+                case IMAGE:
+                    return "IMAGE";
+                default:
+                    return "UNKNOWN";
+            }
+        }
+
+        @Override
+        public String name() {
+            return "ComponentStart";
+        }
+
+        @Override
+        public int id() {
+            return Operations.COMPONENT_START;
+        }
+
+        public static void apply(WireBuffer buffer, int type, int componentId,
+                                 float width, float height) {
+            buffer.start(Operations.COMPONENT_START);
+            buffer.writeInt(type);
+            buffer.writeInt(componentId);
+            buffer.writeFloat(width);
+            buffer.writeFloat(height);
+        }
+
+        public static int size() {
+            return 1 + 4 + 4 + 4;
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int type = buffer.readInt();
+            int componentId = buffer.readInt();
+            float width = buffer.readFloat();
+            float height = buffer.readFloat();
+            operations.add(new ComponentStart(type, componentId, width, height));
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+                    .description("Basic component encapsulating draw commands."
+                           + "This is not resizable.")
+                    .field(INT, "TYPE", "Type of components")
+                    .field(INT, "COMPONENT_ID", "unique id for this component")
+                    .field(FLOAT, "WIDTH", "width of the component")
+                    .field(FLOAT, "HEIGHT", "height of the component");
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStartOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStartOperation.java
new file mode 100644
index 0000000..67964ef
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ComponentStartOperation.java
@@ -0,0 +1,21 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+
+public interface ComponentStartOperation extends Operation {
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/DecoratorComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/DecoratorComponent.java
new file mode 100644
index 0000000..941666a
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/DecoratorComponent.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout;
+
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+
+/**
+ * Indicates a lightweight component (without children) that is only laid out and not able to be
+ * measured. Eg borders, background, clips, etc.
+ */
+public interface DecoratorComponent {
+    void layout(RemoteContext context, float width, float height);
+    void onClick(float x, float y);
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java
new file mode 100644
index 0000000..f198c4a
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java
@@ -0,0 +1,220 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentModifiers;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.DimensionModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.PaddingModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.WidthModifierOperation;
+
+import java.util.ArrayList;
+
+/**
+ * Component with modifiers and children
+ */
+public class LayoutComponent extends Component {
+
+    protected WidthModifierOperation mWidthModifier = null;
+    protected HeightModifierOperation mHeightModifier = null;
+
+    // Margins
+    protected float mMarginLeft = 0f;
+    protected float mMarginRight = 0f;
+    protected float mMarginTop = 0f;
+    protected float mMarginBottom = 0f;
+
+    protected float mPaddingLeft = 0f;
+    protected float mPaddingRight = 0f;
+    protected float mPaddingTop = 0f;
+    protected float mPaddingBottom = 0f;
+
+    protected ComponentModifiers mComponentModifiers = new ComponentModifiers();
+    protected ArrayList<Component> mChildrenComponents = new ArrayList<>();
+
+    public LayoutComponent(Component parent, int componentId, int animationId,
+                           float x, float y, float width, float height) {
+        super(parent, componentId, animationId, x, y, width, height);
+    }
+
+    public float getMarginLeft() {
+        return mMarginLeft;
+    }
+    public float getMarginRight() {
+        return mMarginRight;
+    }
+    public float getMarginTop() {
+        return mMarginTop;
+    }
+    public float getMarginBottom() {
+        return mMarginBottom;
+    }
+
+    public WidthModifierOperation getWidthModifier() {
+        return mWidthModifier;
+    }
+    public HeightModifierOperation getHeightModifier() {
+        return mHeightModifier;
+    }
+
+    public void inflate() {
+        for (Operation op : mList) {
+            if (op instanceof LayoutComponentContent) {
+                ((LayoutComponentContent) op).mParent = this;
+                mChildrenComponents.clear();
+                ((LayoutComponentContent) op).getComponents(mChildrenComponents);
+                if (mChildrenComponents.isEmpty()) {
+                    mChildrenComponents.add((Component) op);
+                }
+            } else if (op instanceof ModifierOperation) {
+                mComponentModifiers.add((ModifierOperation) op);
+            } else {
+                // nothing
+            }
+        }
+
+        mList.clear();
+        mList.add(mComponentModifiers);
+        for (Component c : mChildrenComponents) {
+            c.mParent = this;
+            mList.add(c);
+        }
+
+        mX = 0f;
+        mY = 0f;
+        mMarginLeft = 0f;
+        mMarginTop = 0f;
+        mMarginRight = 0f;
+        mMarginBottom = 0f;
+        mPaddingLeft = 0f;
+        mPaddingTop = 0f;
+        mPaddingRight = 0f;
+        mPaddingBottom = 0f;
+
+        boolean applyHorizontalMargin = true;
+        boolean applyVerticalMargin = true;
+        for (Operation op : mComponentModifiers.getList()) {
+            if (op instanceof PaddingModifierOperation) {
+                // We are accumulating padding modifiers to compute the margin
+                // until we hit a dimension; the computed padding for the
+                // content simply accumulate all the padding modifiers.
+                float left = ((PaddingModifierOperation) op).getLeft();
+                float right = ((PaddingModifierOperation) op).getRight();
+                float top = ((PaddingModifierOperation) op).getTop();
+                float bottom = ((PaddingModifierOperation) op).getBottom();
+                if (applyHorizontalMargin) {
+                    mMarginLeft += left;
+                    mMarginRight += right;
+                }
+                if (applyVerticalMargin) {
+                    mMarginTop += top;
+                    mMarginBottom += bottom;
+                }
+                mPaddingLeft += left;
+                mPaddingTop += top;
+                mPaddingRight += right;
+                mPaddingBottom += bottom;
+            }
+            if (op instanceof WidthModifierOperation && mWidthModifier == null) {
+                mWidthModifier = (WidthModifierOperation) op;
+                applyHorizontalMargin = false;
+            }
+            if (op instanceof HeightModifierOperation && mHeightModifier == null) {
+                mHeightModifier = (HeightModifierOperation) op;
+                applyVerticalMargin = false;
+            }
+        }
+        if (mWidthModifier == null) {
+            mWidthModifier = new WidthModifierOperation(DimensionModifierOperation.Type.WRAP);
+        }
+        if (mHeightModifier == null) {
+            mHeightModifier = new HeightModifierOperation(DimensionModifierOperation.Type.WRAP);
+        }
+        mWidth = computeModifierDefinedWidth();
+        mHeight = computeModifierDefinedHeight();
+    }
+
+    @Override
+    public String toString() {
+        return "UNKNOWN LAYOUT_COMPONENT";
+    }
+
+    @Override
+    public void paintingComponent(PaintContext context) {
+        context.save();
+        context.translate(mX, mY);
+        mComponentModifiers.paint(context);
+        float tx = mPaddingLeft;
+        float ty = mPaddingTop;
+        context.translate(tx, ty);
+        for (Component child : mChildrenComponents) {
+            child.paint(context);
+        }
+        context.translate(-tx, -ty);
+        context.restore();
+    }
+
+    /**
+     * Traverse the modifiers to compute indicated dimension
+     */
+    public float computeModifierDefinedWidth() {
+        float s = 0f;
+        float e = 0f;
+        float w = 0f;
+        for (Operation c : mComponentModifiers.getList()) {
+            if (c instanceof WidthModifierOperation) {
+                WidthModifierOperation o = (WidthModifierOperation) c;
+                if (o.getType() == DimensionModifierOperation.Type.EXACT) {
+                    w = o.getValue();
+                }
+                break;
+            }
+            if (c instanceof PaddingModifierOperation) {
+                PaddingModifierOperation pop = (PaddingModifierOperation) c;
+                s += pop.getLeft();
+                e += pop.getRight();
+            }
+        }
+        return s + w + e;
+    }
+
+    /**
+     * Traverse the modifiers to compute indicated dimension
+     */
+    public float computeModifierDefinedHeight() {
+        float t = 0f;
+        float b = 0f;
+        float h = 0f;
+        for (Operation c : mComponentModifiers.getList()) {
+            if (c instanceof HeightModifierOperation) {
+                HeightModifierOperation o = (HeightModifierOperation) c;
+                if (o.getType() == DimensionModifierOperation.Type.EXACT) {
+                    h = o.getValue();
+                }
+                break;
+            }
+            if (c instanceof PaddingModifierOperation) {
+                PaddingModifierOperation pop = (PaddingModifierOperation) c;
+                t += pop.getTop();
+                b += pop.getBottom();
+            }
+        }
+        return t + h + b;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponentContent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponentContent.java
new file mode 100644
index 0000000..769ff6a
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponentContent.java
@@ -0,0 +1,68 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+
+import java.util.List;
+
+/**
+ * Represents the content of a LayoutComponent (i.e. the children components)
+ */
+public class LayoutComponentContent extends Component implements ComponentStartOperation {
+
+    public static final LayoutComponentContent.Companion COMPANION =
+            new LayoutComponentContent.Companion();
+
+    public LayoutComponentContent(int componentId, float x, float y,
+                                  float width, float height, Component parent, int animationId) {
+        super(parent, componentId, animationId, x, y, width, height);
+    }
+
+    public static class Companion implements DocumentedCompanionOperation {
+        @Override
+        public String name() {
+            return "LayoutContent";
+        }
+
+        @Override
+        public int id() {
+            return Operations.LAYOUT_CONTENT;
+        }
+
+        public void apply(WireBuffer buffer) {
+            buffer.start(Operations.LAYOUT_CONTENT);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            operations.add(new LayoutComponentContent(
+                    -1, 0, 0, 0, 0, null, -1));
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+                    .description("Container for components. BoxLayout, RowLayout and ColumnLayout "
+                           + "expects a LayoutComponentContent as a child, encapsulating the "
+                           + "components that needs to be laid out.");
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/RootLayoutComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/RootLayoutComponent.java
new file mode 100644
index 0000000..dc13768
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/RootLayoutComponent.java
@@ -0,0 +1,173 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Measurable;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentModifiers;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/**
+ * Represents the root layout component. Entry point to the component tree layout/paint.
+ */
+public class RootLayoutComponent extends Component implements ComponentStartOperation {
+
+    public static final RootLayoutComponent.Companion COMPANION =
+            new RootLayoutComponent.Companion();
+
+    int mCurrentId = -1;
+
+    public RootLayoutComponent(int componentId, float x, float y,
+                               float width, float height, Component parent, int animationId) {
+        super(parent, componentId, animationId, x, y, width, height);
+    }
+
+    public RootLayoutComponent(int componentId, float x, float y,
+                               float width, float height, Component parent) {
+        super(parent, componentId, -1, x, y, width, height);
+    }
+
+    @Override
+    public String toString() {
+        return "ROOT (" + mX + ", " + mY + " - " + mWidth + " x " + mHeight + ") " + mVisibility;
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(indent, "ROOT [" + mComponentId + ":" + mAnimationId
+                + "] = [" + mX + ", " + mY + ", " + mWidth + ", " + mHeight + "] " + mVisibility);
+    }
+
+    public int getNextId() {
+        mCurrentId--;
+        return mCurrentId;
+    }
+
+    public void assignIds() {
+        assignId(this);
+    }
+
+    void assignId(Component component) {
+        if (component.mComponentId == -1) {
+            component.mComponentId = getNextId();
+        }
+        for (Operation op : component.mList) {
+            if (op instanceof Component) {
+                assignId((Component) op);
+            }
+        }
+    }
+
+    /**
+     * This will measure then layout the tree of components
+     */
+    public void layout(RemoteContext context) {
+        if (!mNeedsMeasure) {
+            return;
+        }
+        context.lastComponent = this;
+        mWidth = context.mWidth;
+        mHeight = context.mHeight;
+
+        // TODO: reuse MeasurePass
+        MeasurePass measurePass = new MeasurePass();
+        for (Operation op : mList) {
+            if (op instanceof Measurable) {
+                Measurable m = (Measurable) op;
+                m.measure(context.getPaintContext(),
+                        0f, mWidth, 0f, mHeight, measurePass);
+                m.layout(context, measurePass);
+            }
+        }
+        mNeedsMeasure = false;
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        mNeedsRepaint = false;
+        context.getContext().lastComponent = this;
+        context.save();
+
+        if (mParent == null) { // root layout
+            context.clipRect(0f, 0f, mWidth, mHeight);
+        }
+
+        for (Operation op : mList) {
+            if (op instanceof PaintOperation) {
+                ((PaintOperation) op).paint(context);
+            }
+        }
+
+        context.restore();
+    }
+
+    public String displayHierarchy() {
+        StringSerializer serializer = new StringSerializer();
+        displayHierarchy(this, 0, serializer);
+        return serializer.toString();
+    }
+
+    public void displayHierarchy(Component component, int indent, StringSerializer serializer) {
+        component.serializeToString(indent, serializer);
+        for (Operation c : component.mList) {
+            if (c instanceof ComponentModifiers) {
+                ((ComponentModifiers) c).serializeToString(indent + 1, serializer);
+            }
+            if (c instanceof Component) {
+                displayHierarchy((Component) c, indent + 1, serializer);
+            }
+        }
+    }
+
+    public static class Companion implements DocumentedCompanionOperation {
+        @Override
+        public String name() {
+            return "RootLayout";
+        }
+
+        @Override
+        public int id() {
+            return Operations.LAYOUT_ROOT;
+        }
+
+        public void apply(WireBuffer buffer) {
+            buffer.start(Operations.LAYOUT_ROOT);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            operations.add(new RootLayoutComponent(
+                    -1, 0, 0, 0, 0, null, -1));
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+                    .description("Root element for a document. Other components / layout managers "
+                         + "are children in the component tree starting from this Root component.");
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java
new file mode 100644
index 0000000..7c6bef4
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java
@@ -0,0 +1,311 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.animation;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.DecoratorComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.PaddingModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+import com.android.internal.widget.remotecompose.core.operations.utilities.easing.FloatAnimation;
+import com.android.internal.widget.remotecompose.core.operations.utilities.easing.GeneralEasing;
+
+/**
+ * Basic interpolation manager between two ComponentMeasures
+ *
+ * Handles position, size and visibility
+ */
+public class AnimateMeasure {
+    long mStartTime = System.currentTimeMillis();
+    Component mComponent;
+    ComponentMeasure mOriginal;
+    ComponentMeasure mTarget;
+    int mDuration;
+    int mDurationVisibilityChange = mDuration;
+    AnimationSpec.ANIMATION mEnterAnimation = AnimationSpec.ANIMATION.FADE_IN;
+    AnimationSpec.ANIMATION mExitAnimation = AnimationSpec.ANIMATION.FADE_OUT;
+    int mMotionEasingType = GeneralEasing.CUBIC_STANDARD;
+    int mVisibilityEasingType = GeneralEasing.CUBIC_ACCELERATE;
+
+    float mP = 0f;
+    float mVp = 0f;
+    FloatAnimation mMotionEasing = new FloatAnimation(mMotionEasingType,
+            mDuration / 1000f, null, 0f, Float.NaN);
+    FloatAnimation mVisibilityEasing = new FloatAnimation(mVisibilityEasingType,
+            mDurationVisibilityChange / 1000f,
+            null, 0f, Float.NaN);
+    ParticleAnimation mParticleAnimation;
+
+    public AnimateMeasure(long startTime, Component component, ComponentMeasure original,
+                          ComponentMeasure target, int duration, int durationVisibilityChange,
+                          AnimationSpec.ANIMATION enterAnimation,
+                          AnimationSpec.ANIMATION exitAnimation,
+                          int motionEasingType, int visibilityEasingType) {
+        this.mStartTime = startTime;
+        this.mComponent = component;
+        this.mOriginal = original;
+        this.mTarget = target;
+        this.mDuration = duration;
+        this.mDurationVisibilityChange = durationVisibilityChange;
+        this.mEnterAnimation = enterAnimation;
+        this.mExitAnimation = exitAnimation;
+
+        mMotionEasing.setTargetValue(1f);
+        mVisibilityEasing.setTargetValue(1f);
+        component.mVisibility = target.getVisibility();
+    }
+
+    public void update(long currentTime) {
+        long elapsed = currentTime - mStartTime;
+        mP = Math.min(elapsed / (float) mDuration, 1f);
+        //mP = motionEasing.get(mP);
+        mVp = Math.min(elapsed / (float) mDurationVisibilityChange, 1f);
+        mVp = mVisibilityEasing.get(mVp);
+    }
+
+    public PaintBundle paint = new PaintBundle();
+
+    public void apply(PaintContext context) {
+        update(context.getContext().currentTime);
+
+        mComponent.setX(getX());
+        mComponent.setY(getY());
+        mComponent.setWidth(getWidth());
+        mComponent.setHeight(getHeight());
+
+        float w = mComponent.getWidth();
+        float h = mComponent.getHeight();
+        for (Operation op : mComponent.mList) {
+            if (op instanceof PaddingModifierOperation) {
+                PaddingModifierOperation pop = (PaddingModifierOperation) op;
+                w -= pop.getLeft() + pop.getRight();
+                h -= pop.getTop() + pop.getBottom();
+            }
+            if (op instanceof DecoratorComponent) {
+                ((DecoratorComponent) op).layout(context.getContext(), w, h);
+            }
+        }
+
+        mComponent.mVisibility = mTarget.getVisibility();
+        if (mOriginal.getVisibility() != mTarget.getVisibility()) {
+            if (mTarget.getVisibility() == Component.Visibility.GONE) {
+                switch (mExitAnimation) {
+                    case PARTICLE:
+                        // particleAnimation(context, component, original, target, vp)
+                        if (mParticleAnimation == null) {
+                            mParticleAnimation = new ParticleAnimation();
+                        }
+                        mParticleAnimation.animate(context, mComponent, mOriginal, mTarget, mVp);
+                        break;
+                    case FADE_OUT:
+                        context.save();
+                        context.savePaint();
+                        paint.reset();
+                        paint.setColor(0f, 0f, 0f, 1f - mVp);
+                        context.applyPaint(paint);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restorePaint();
+                        context.restore();
+                        break;
+                    case SLIDE_LEFT:
+                        context.save();
+                        context.translate(-mVp * mComponent.getParent().getWidth(), 0f);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    case SLIDE_RIGHT:
+                        context.save();
+                        context.savePaint();
+                        paint.reset();
+                        paint.setColor(0f, 0f, 0f, 1f);
+                        context.applyPaint(paint);
+                        context.translate(mVp * mComponent.getParent().getWidth(), 0f);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restorePaint();
+                        context.restore();
+                        break;
+                    case SLIDE_TOP:
+                        context.save();
+                        context.translate(0f,
+                                -mVp * mComponent.getParent().getHeight());
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    case SLIDE_BOTTOM:
+                        context.save();
+                        context.translate(0f,
+                                mVp * mComponent.getParent().getHeight());
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    default:
+                        //            particleAnimation(context, component, original, target, vp)
+                        if (mParticleAnimation == null) {
+                            mParticleAnimation = new ParticleAnimation();
+                        }
+                        mParticleAnimation.animate(context, mComponent, mOriginal, mTarget, mVp);
+                        break;
+                }
+            } else if (mOriginal.getVisibility() == Component.Visibility.GONE
+                    && mTarget.getVisibility() == Component.Visibility.VISIBLE) {
+                switch (mEnterAnimation) {
+                    case ROTATE:
+                        float px = mTarget.getX() + mTarget.getW() / 2f;
+                        float py = mTarget.getY() + mTarget.getH() / 2f;
+
+                        context.save();
+                        context.savePaint();
+                        context.matrixRotate(mVp * 360f, px, py);
+                        context.matrixScale(1f * mVp, 1f * mVp, px, py);
+                        paint.reset();
+                        paint.setColor(0f, 0f, 0f, mVp);
+                        context.applyPaint(paint);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restorePaint();
+                        context.restore();
+                        break;
+                    case FADE_IN:
+                        context.save();
+                        context.savePaint();
+                        paint.reset();
+                        paint.setColor(0f, 0f, 0f, mVp);
+                        context.applyPaint(paint);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restorePaint();
+                        context.restore();
+                        break;
+                    case SLIDE_LEFT:
+                        context.save();
+                        context.translate(
+                                (1f - mVp) * mComponent.getParent().getWidth(), 0f);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    case SLIDE_RIGHT:
+                        context.save();
+                        context.translate(
+                                -(1f - mVp) * mComponent.getParent().getWidth(), 0f);
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    case SLIDE_TOP:
+                        context.save();
+                        context.translate(0f,
+                                (1f - mVp) * mComponent.getParent().getHeight());
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    case SLIDE_BOTTOM:
+                        context.save();
+                        context.translate(0f,
+                                -(1f - mVp) * mComponent.getParent().getHeight());
+                        context.saveLayer(mComponent.getX(), mComponent.getY(),
+                                mComponent.getWidth(), mComponent.getHeight());
+                        mComponent.paintingComponent(context);
+                        context.restore();
+                        context.restore();
+                        break;
+                    default:
+                        break;
+                }
+            } else {
+                mComponent.paintingComponent(context);
+            }
+        } else {
+            mComponent.paintingComponent(context);
+        }
+
+        if (mP >= 1f && mVp >= 1f) {
+            mComponent.mAnimateMeasure = null;
+            mComponent.mVisibility = mTarget.getVisibility();
+        }
+    }
+
+    public boolean isDone() {
+        return mP >= 1f && mVp >= 1f;
+    }
+
+    public float getX() {
+        return mOriginal.getX() * (1 - mP) + mTarget.getX() * mP;
+    }
+
+    public float getY() {
+        return mOriginal.getY() * (1 - mP) + mTarget.getY() * mP;
+    }
+
+    public float getWidth() {
+        return mOriginal.getW() * (1 - mP) + mTarget.getW() * mP;
+    }
+
+    public float getHeight() {
+        return mOriginal.getH() * (1 - mP) + mTarget.getH() * mP;
+    }
+
+    public float getVisibility() {
+        if (mOriginal.getVisibility() == mTarget.getVisibility()) {
+            return 1f;
+        } else if (mTarget.getVisibility() == Component.Visibility.VISIBLE) {
+            return mVp;
+        } else {
+            return 1 - mVp;
+        }
+    }
+
+    public void updateTarget(ComponentMeasure measure, long currentTime) {
+        mOriginal.setX(getX());
+        mOriginal.setY(getY());
+        mOriginal.setW(getWidth());
+        mOriginal.setH(getHeight());
+        mTarget.setX(measure.getX());
+        mTarget.setY(measure.getY());
+        mTarget.setW(measure.getW());
+        mTarget.setH(measure.getH());
+        mTarget.setVisibility(measure.getVisibility());
+        mStartTime = currentTime;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java
new file mode 100644
index 0000000..386d365
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java
@@ -0,0 +1,186 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.animation;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.utilities.easing.GeneralEasing;
+
+import java.util.List;
+
+/**
+ * Basic component animation spec
+ */
+public class AnimationSpec implements Operation {
+
+    public static final AnimationSpec.Companion COMPANION = new AnimationSpec.Companion();
+
+    int mAnimationId = -1;
+    int mMotionDuration = 300;
+    int mMotionEasingType = GeneralEasing.CUBIC_STANDARD;
+    int mVisibilityDuration = 300;
+    int mVisibilityEasingType = GeneralEasing.CUBIC_STANDARD;
+    ANIMATION mEnterAnimation = ANIMATION.FADE_IN;
+    ANIMATION mExitAnimation = ANIMATION.FADE_OUT;
+
+    public AnimationSpec(int animationId, int motionDuration, int motionEasingType,
+                         int visibilityDuration, int visibilityEasingType,
+                         ANIMATION enterAnimation, ANIMATION exitAnimation) {
+        this.mAnimationId = animationId;
+        this.mMotionDuration = motionDuration;
+        this.mMotionEasingType = motionEasingType;
+        this.mVisibilityDuration = visibilityDuration;
+        this.mVisibilityEasingType = visibilityEasingType;
+        this.mEnterAnimation = enterAnimation;
+        this.mExitAnimation = exitAnimation;
+    }
+
+    public AnimationSpec() {
+        this(-1, 300, GeneralEasing.CUBIC_STANDARD,
+                300, GeneralEasing.CUBIC_STANDARD,
+                ANIMATION.FADE_IN, ANIMATION.FADE_OUT);
+    }
+
+    public int getAnimationId() {
+        return mAnimationId;
+    }
+
+    public int getMotionDuration() {
+        return mMotionDuration;
+    }
+
+    public int getMotionEasingType() {
+        return mMotionEasingType;
+    }
+
+    public int getVisibilityDuration() {
+        return mVisibilityDuration;
+    }
+
+    public int getVisibilityEasingType() {
+        return mVisibilityEasingType;
+    }
+
+    public ANIMATION getEnterAnimation() {
+        return mEnterAnimation;
+    }
+
+    public ANIMATION getExitAnimation() {
+        return mExitAnimation;
+    }
+
+    @Override
+    public String toString() {
+        return "ANIMATION_SPEC (" + mMotionDuration + " ms)";
+    }
+
+    public enum ANIMATION {
+        FADE_IN,
+        FADE_OUT,
+        SLIDE_LEFT,
+        SLIDE_RIGHT,
+        SLIDE_TOP,
+        SLIDE_BOTTOM,
+        ROTATE,
+        PARTICLE
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        Companion.apply(buffer, mAnimationId, mMotionDuration, mMotionEasingType,
+                mVisibilityDuration, mVisibilityEasingType, mEnterAnimation, mExitAnimation);
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+        // nothing here
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return (indent != null ? indent : "") + toString();
+    }
+
+    public static class Companion implements CompanionOperation {
+        @Override
+        public String name() {
+            return "AnimationSpec";
+        }
+
+        @Override
+        public int id() {
+            return Operations.ANIMATION_SPEC;
+        }
+
+        public static int animationToInt(ANIMATION animation) {
+            return animation.ordinal();
+        }
+
+        public static ANIMATION intToAnimation(int value) {
+            switch (value) {
+                case 0:
+                    return ANIMATION.FADE_IN;
+                case 1:
+                    return ANIMATION.FADE_OUT;
+                case 2:
+                    return ANIMATION.SLIDE_LEFT;
+                case 3:
+                    return ANIMATION.SLIDE_RIGHT;
+                case 4:
+                    return ANIMATION.SLIDE_TOP;
+                case 5:
+                    return ANIMATION.SLIDE_BOTTOM;
+                case 6:
+                    return ANIMATION.ROTATE;
+                case 7:
+                    return ANIMATION.PARTICLE;
+                default:
+                    return ANIMATION.FADE_IN;
+            }
+        }
+
+        public static void apply(WireBuffer buffer, int animationId, int motionDuration,
+                                 int motionEasingType, int visibilityDuration,
+                                 int visibilityEasingType, ANIMATION enterAnimation,
+                                 ANIMATION exitAnimation) {
+            buffer.start(Operations.ANIMATION_SPEC);
+            buffer.writeInt(animationId);
+            buffer.writeInt(motionDuration);
+            buffer.writeInt(motionEasingType);
+            buffer.writeInt(visibilityDuration);
+            buffer.writeInt(visibilityEasingType);
+            buffer.writeInt(animationToInt(enterAnimation));
+            buffer.writeInt(animationToInt(exitAnimation));
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int animationId = buffer.readInt();
+            int motionDuration = buffer.readInt();
+            int motionEasingType = buffer.readInt();
+            int visibilityDuration = buffer.readInt();
+            int visibilityEasingType = buffer.readInt();
+            ANIMATION enterAnimation = intToAnimation(buffer.readInt());
+            ANIMATION exitAnimation = intToAnimation(buffer.readInt());
+            AnimationSpec op = new AnimationSpec(animationId, motionDuration, motionEasingType,
+                    visibilityDuration, visibilityEasingType, enterAnimation, exitAnimation);
+            operations.add(op);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/Particle.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/Particle.java
new file mode 100644
index 0000000..4562692
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/Particle.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.animation;
+
+public class Particle {
+    public final float x;
+    public final float y;
+    public float radius;
+    public float r;
+    public float g;
+    public float b;
+
+    public Particle(float x, float y, float radius, float r, float g, float b) {
+        this.x = x;
+        this.y = y;
+        this.radius = radius;
+        this.r = r;
+        this.g = g;
+        this.b = b;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java
new file mode 100644
index 0000000..5c5d056
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java
@@ -0,0 +1,64 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.animation;
+
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class ParticleAnimation {
+    HashMap<Integer, ArrayList<Particle>> mAllParticles = new HashMap<>();
+
+    PaintBundle mPaint = new PaintBundle();
+    public void animate(PaintContext context, Component component,
+                        ComponentMeasure start, ComponentMeasure end,
+                        float progress) {
+        ArrayList<Particle> particles = mAllParticles.get(component.getComponentId());
+        if (particles == null) {
+            particles = new ArrayList<Particle>();
+            for (int i = 0; i < 20; i++) {
+                float x = (float) Math.random();
+                float y = (float) Math.random();
+                float radius = (float) Math.random();
+                float r = 250f;
+                float g = 250f;
+                float b = 250f;
+                particles.add(new Particle(x, y, radius, r, g, b));
+            }
+            mAllParticles.put(component.getComponentId(), particles);
+        }
+        context.save();
+        context.savePaint();
+        for (int i = 0; i < particles.size(); i++) {
+            Particle particle = particles.get(i);
+            mPaint.reset();
+            mPaint.setColor(particle.r, particle.g, particle.b,
+                    200 * (1 - progress));
+            context.applyPaint(mPaint);
+            float dx = start.getX() + component.getWidth() * particle.x;
+            float dy = start.getY() + component.getHeight() * particle.y
+                    + progress * 0.01f * component.getHeight();
+            float dr = (component.getHeight() + 60) * 0.15f * particle.radius + (30 * progress);
+            context.drawCircle(dx, dy, dr);
+        }
+        context.restorePaint();
+        context.restore();
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java
new file mode 100644
index 0000000..fea8dd2
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.managers;
+
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.INT;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStartOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size;
+
+import java.util.List;
+
+/**
+ * Simple Box layout implementation
+ */
+public class BoxLayout extends LayoutManager implements ComponentStartOperation {
+
+    public static final int START = 1;
+    public static final int CENTER = 2;
+    public static final int END = 3;
+    public static final int TOP = 4;
+    public static final int BOTTOM = 5;
+
+    public static final BoxLayout.Companion COMPANION = new BoxLayout.Companion();
+
+    int mHorizontalPositioning;
+    int mVerticalPositioning;
+
+    public BoxLayout(Component parent, int componentId, int animationId,
+                     float x, float y, float width, float height,
+                     int horizontalPositioning, int verticalPositioning) {
+        super(parent, componentId, animationId, x, y, width, height);
+        mHorizontalPositioning = horizontalPositioning;
+        mVerticalPositioning = verticalPositioning;
+    }
+
+    public BoxLayout(Component parent, int componentId, int animationId,
+                     int horizontalPositioning, int verticalPositioning) {
+        this(parent, componentId, animationId, 0, 0, 0, 0,
+                horizontalPositioning, verticalPositioning);
+    }
+
+    @Override
+    public String toString() {
+        return "BOX [" + mComponentId + ":" + mAnimationId + "] (" + mX + ", "
+                + mY + " - " + mWidth + " x " + mHeight + ") " + mVisibility;
+    }
+
+    protected String getSerializedName() {
+        return "BOX";
+    }
+
+    @Override
+    public void computeWrapSize(PaintContext context, float maxWidth, float maxHeight,
+                                MeasurePass measure, Size size) {
+        for (Component c : mChildrenComponents) {
+            c.measure(context, 0f, maxWidth, 0f, maxHeight, measure);
+            ComponentMeasure m = measure.get(c);
+            size.setWidth(Math.max(size.getWidth(), m.getW()));
+            size.setHeight(Math.max(size.getHeight(), m.getH()));
+        }
+        // add padding
+        size.setWidth(Math.max(size.getWidth(), computeModifierDefinedWidth()));
+        size.setHeight(Math.max(size.getHeight(), computeModifierDefinedHeight()));
+    }
+
+    @Override
+    public void computeSize(PaintContext context, float minWidth, float maxWidth,
+                            float minHeight, float maxHeight, MeasurePass measure) {
+        for (Component child : mChildrenComponents) {
+            child.measure(context, minWidth, maxWidth, minHeight, maxHeight, measure);
+        }
+    }
+
+    @Override
+    public void internalLayoutMeasure(PaintContext context,
+                                      MeasurePass measure) {
+        ComponentMeasure selfMeasure = measure.get(this);
+        float selfWidth = selfMeasure.getW() - mPaddingLeft - mPaddingRight;
+        float selfHeight = selfMeasure.getH() - mPaddingTop - mPaddingBottom;
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure m = measure.get(child);
+            float tx = 0f;
+            float ty = 0f;
+            switch (mVerticalPositioning) {
+                case TOP:
+                    ty = 0f;
+                    break;
+                case CENTER:
+                    ty = (selfHeight - m.getH()) / 2f;
+                    break;
+                case BOTTOM:
+                    ty = selfHeight - m.getH();
+                    break;
+            }
+            switch (mHorizontalPositioning) {
+                case START:
+                    tx = 0f;
+                    break;
+                case CENTER:
+                    tx = (selfWidth - m.getW()) / 2f;
+                    break;
+                case END:
+                    tx = selfWidth - m.getW();
+                    break;
+            }
+            m.setX(tx);
+            m.setY(ty);
+            m.setVisibility(child.mVisibility);
+        }
+    }
+
+    public static class Companion implements DocumentedCompanionOperation {
+        @Override
+        public String name() {
+            return "BoxLayout";
+        }
+
+        @Override
+        public int id() {
+            return Operations.LAYOUT_BOX;
+        }
+
+        public void apply(WireBuffer buffer, int componentId, int animationId,
+                          int horizontalPositioning, int verticalPositioning) {
+            buffer.start(Operations.LAYOUT_BOX);
+            buffer.writeInt(componentId);
+            buffer.writeInt(animationId);
+            buffer.writeInt(horizontalPositioning);
+            buffer.writeInt(verticalPositioning);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int componentId = buffer.readInt();
+            int animationId = buffer.readInt();
+            int horizontalPositioning = buffer.readInt();
+            int verticalPositioning = buffer.readInt();
+            operations.add(new BoxLayout(null, componentId, animationId,
+                    horizontalPositioning, verticalPositioning));
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+                .description("Box layout implementation.\n\n"
+                      + "Child components are laid out independently from one another,\n"
+                      + " and painted in their hierarchy order (first children drawn"
+                      + "before the latter). Horizontal and Vertical positioning"
+                      + "are supported.")
+                .examplesDimension(150, 100)
+                .exampleImage("Top", "layout-BoxLayout-start-top.png")
+                .exampleImage("Center", "layout-BoxLayout-center-center.png")
+                .exampleImage("Bottom", "layout-BoxLayout-end-bottom.png")
+                .field(INT, "COMPONENT_ID", "unique id for this component")
+                .field(INT, "ANIMATION_ID", "id used to match components,"
+                      + " for animation purposes")
+                .field(INT, "HORIZONTAL_POSITIONING", "horizontal positioning value")
+                    .possibleValues("START", BoxLayout.START)
+                    .possibleValues("CENTER", BoxLayout.CENTER)
+                    .possibleValues("END", BoxLayout.END)
+                .field(INT, "VERTICAL_POSITIONING", "vertical positioning value")
+                    .possibleValues("TOP", BoxLayout.TOP)
+                    .possibleValues("CENTER", BoxLayout.CENTER)
+                    .possibleValues("BOTTOM", BoxLayout.BOTTOM);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java
new file mode 100644
index 0000000..a1a2de5
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java
@@ -0,0 +1,291 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.managers;
+
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.FLOAT;
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.INT;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStartOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size;
+import com.android.internal.widget.remotecompose.core.operations.layout.utils.DebugLog;
+
+import java.util.List;
+
+/**
+ * Simple Column layout implementation
+ * - also supports weight and horizontal/vertical positioning
+ */
+public class ColumnLayout extends LayoutManager implements ComponentStartOperation {
+    public static final int START = 1;
+    public static final int CENTER = 2;
+    public static final int END = 3;
+    public static final int TOP = 4;
+    public static final int BOTTOM = 5;
+    public static final int SPACE_BETWEEN = 6;
+    public static final int SPACE_EVENLY = 7;
+    public static final int SPACE_AROUND = 8;
+
+    public static final ColumnLayout.Companion COMPANION = new ColumnLayout.Companion();
+
+    int mHorizontalPositioning;
+    int mVerticalPositioning;
+    float mSpacedBy = 0f;
+
+    public ColumnLayout(Component parent, int componentId, int animationId,
+                        float x, float y, float width, float height,
+                        int horizontalPositioning, int verticalPositioning, float spacedBy) {
+        super(parent, componentId, animationId, x, y, width, height);
+        mHorizontalPositioning = horizontalPositioning;
+        mVerticalPositioning = verticalPositioning;
+        mSpacedBy = spacedBy;
+    }
+
+    public ColumnLayout(Component parent, int componentId, int animationId,
+                     int horizontalPositioning, int verticalPositioning, float spacedBy) {
+        this(parent, componentId, animationId, 0, 0, 0, 0,
+                horizontalPositioning, verticalPositioning, spacedBy);
+    }
+
+    @Override
+    public String toString() {
+        return "COLUMN [" + mComponentId + ":" + mAnimationId + "] (" + mX + ", "
+                + mY + " - " + mWidth + " x " + mHeight + ") " + mVisibility;
+    }
+
+    protected String getSerializedName() {
+        return "COLUMN";
+    }
+
+    @Override
+    public void computeWrapSize(PaintContext context, float maxWidth, float maxHeight,
+                                MeasurePass measure, Size size) {
+        DebugLog.s(() -> "COMPUTE WRAP SIZE in " + this + " (" + mComponentId + ")");
+        for (Component c : mChildrenComponents) {
+            c.measure(context, 0f, maxWidth,
+                    0f, maxHeight, measure);
+            ComponentMeasure m = measure.get(c);
+            size.setWidth(Math.max(size.getWidth(), m.getW()));
+            size.setHeight(size.getHeight() + m.getH());
+        }
+        if (!mChildrenComponents.isEmpty()) {
+            size.setHeight(size.getHeight()
+                    + (mSpacedBy * (mChildrenComponents.size() - 1)));
+        }
+        DebugLog.e();
+    }
+
+    @Override
+    public void computeSize(PaintContext context, float minWidth, float maxWidth,
+                            float minHeight, float maxHeight, MeasurePass measure) {
+        DebugLog.s(() -> "COMPUTE SIZE in " + this + " (" + mComponentId + ")");
+        float mh = maxHeight;
+        for (Component child : mChildrenComponents) {
+            child.measure(context, minWidth, maxWidth, minHeight, mh, measure);
+            ComponentMeasure m = measure.get(child);
+            mh -= m.getH();
+        }
+        DebugLog.e();
+    }
+
+    @Override
+    public void internalLayoutMeasure(PaintContext context,
+                                      MeasurePass measure) {
+        ComponentMeasure selfMeasure = measure.get(this);
+        DebugLog.s(() -> "INTERNAL LAYOUT " + this + " (" + mComponentId + ") children: "
+                + mChildrenComponents.size() + " size (" + selfMeasure.getW()
+                + " x " + selfMeasure.getH() + ")");
+        if (mChildrenComponents.isEmpty()) {
+            DebugLog.e();
+            return;
+        }
+        float selfWidth = selfMeasure.getW() - mPaddingLeft - mPaddingRight;
+        float selfHeight = selfMeasure.getH() - mPaddingTop - mPaddingBottom;
+        float childrenWidth = 0f;
+        float childrenHeight = 0f;
+
+        boolean hasWeights = false;
+        float totalWeights = 0f;
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure childMeasure = measure.get(child);
+            if (child instanceof LayoutComponent
+                    && ((LayoutComponent) child).getHeightModifier().hasWeight()) {
+                hasWeights = true;
+                totalWeights += ((LayoutComponent) child).getHeightModifier().getValue();
+            } else {
+                childrenHeight += childMeasure.getH();
+            }
+        }
+        if (hasWeights) {
+            float availableSpace = selfHeight - childrenHeight;
+            for (Component child : mChildrenComponents) {
+                if (child instanceof LayoutComponent
+                        && ((LayoutComponent) child).getHeightModifier().hasWeight()) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    float weight = ((LayoutComponent) child).getHeightModifier().getValue();
+                    childMeasure.setH((weight * availableSpace) / totalWeights);
+                    child.measure(context, childMeasure.getW(),
+                            childMeasure.getW(), childMeasure.getH(), childMeasure.getH(), measure);
+                }
+            }
+        }
+
+        childrenHeight = 0f;
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure childMeasure = measure.get(child);
+            childrenWidth = Math.max(childrenWidth, childMeasure.getW());
+            childrenHeight += childMeasure.getH();
+        }
+        childrenHeight += mSpacedBy * (mChildrenComponents.size() - 1);
+
+        float tx = 0f;
+        float ty = 0f;
+
+        float verticalGap = 0f;
+        float total = 0f;
+        switch (mVerticalPositioning) {
+            case TOP:
+                ty = 0f;
+                break;
+            case CENTER:
+                ty = (selfHeight - childrenHeight) / 2f;
+                break;
+            case BOTTOM:
+                ty = selfHeight - childrenHeight;
+                break;
+            case SPACE_BETWEEN:
+                for (Component child : mChildrenComponents) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    total += childMeasure.getH();
+                }
+                verticalGap = (selfHeight - total) / (mChildrenComponents.size() - 1);
+                break;
+            case SPACE_EVENLY:
+                for (Component child : mChildrenComponents) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    total += childMeasure.getH();
+                }
+                verticalGap = (selfHeight - total) / (mChildrenComponents.size() + 1);
+                ty = verticalGap;
+                break;
+            case SPACE_AROUND:
+                for (Component child : mChildrenComponents) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    total += childMeasure.getH();
+                }
+                verticalGap = (selfHeight - total) / (mChildrenComponents.size());
+                ty = verticalGap / 2f;
+                break;
+        }
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure childMeasure = measure.get(child);
+            switch (mHorizontalPositioning) {
+                case START:
+                    tx = 0f;
+                    break;
+                case CENTER:
+                    tx = (selfWidth - childMeasure.getW()) / 2f;
+                    break;
+                case END:
+                    tx = selfWidth - childMeasure.getW();
+                    break;
+            }
+            childMeasure.setX(tx);
+            childMeasure.setY(ty);
+            childMeasure.setVisibility(child.mVisibility);
+            ty += childMeasure.getH();
+            if (mVerticalPositioning == SPACE_BETWEEN
+                    || mVerticalPositioning == SPACE_AROUND
+                    || mVerticalPositioning == SPACE_EVENLY) {
+                ty += verticalGap;
+            }
+            ty += mSpacedBy;
+        }
+        DebugLog.e();
+    }
+
+    public static class Companion implements DocumentedCompanionOperation {
+        @Override
+        public String name() {
+            return "ColumnLayout";
+        }
+
+        @Override
+        public int id() {
+            return Operations.LAYOUT_COLUMN;
+        }
+
+        public void apply(WireBuffer buffer, int componentId, int animationId,
+                          int horizontalPositioning, int verticalPositioning, float spacedBy) {
+            buffer.start(Operations.LAYOUT_COLUMN);
+            buffer.writeInt(componentId);
+            buffer.writeInt(animationId);
+            buffer.writeInt(horizontalPositioning);
+            buffer.writeInt(verticalPositioning);
+            buffer.writeFloat(spacedBy);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int componentId = buffer.readInt();
+            int animationId = buffer.readInt();
+            int horizontalPositioning = buffer.readInt();
+            int verticalPositioning = buffer.readInt();
+            float spacedBy = buffer.readFloat();
+            operations.add(new ColumnLayout(null, componentId, animationId,
+                    horizontalPositioning, verticalPositioning, spacedBy));
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+               .description("Column layout implementation, positioning components one"
+                       + " after the other vertically.\n\n"
+                       + "It supports weight and horizontal/vertical positioning.")
+               .examplesDimension(100, 400)
+               .exampleImage("Top", "layout-ColumnLayout-start-top.png")
+               .exampleImage("Center", "layout-ColumnLayout-start-center.png")
+               .exampleImage("Bottom", "layout-ColumnLayout-start-bottom.png")
+               .exampleImage("SpaceEvenly", "layout-ColumnLayout-start-space-evenly.png")
+               .exampleImage("SpaceAround", "layout-ColumnLayout-start-space-around.png")
+               .exampleImage("SpaceBetween", "layout-ColumnLayout-start-space-between.png")
+               .field(INT, "COMPONENT_ID", "unique id for this component")
+               .field(INT, "ANIMATION_ID", "id used to match components,"
+                       + " for animation purposes")
+               .field(INT, "HORIZONTAL_POSITIONING", "horizontal positioning value")
+               .possibleValues("START", ColumnLayout.START)
+               .possibleValues("CENTER", ColumnLayout.CENTER)
+               .possibleValues("END", ColumnLayout.END)
+               .field(INT, "VERTICAL_POSITIONING", "vertical positioning value")
+               .possibleValues("TOP", ColumnLayout.TOP)
+               .possibleValues("CENTER", ColumnLayout.CENTER)
+               .possibleValues("BOTTOM", ColumnLayout.BOTTOM)
+               .possibleValues("SPACE_BETWEEN", ColumnLayout.SPACE_BETWEEN)
+               .possibleValues("SPACE_EVENLY", ColumnLayout.SPACE_EVENLY)
+               .possibleValues("SPACE_AROUND", ColumnLayout.SPACE_AROUND)
+                    .field(FLOAT, "SPACED_BY", "Horizontal spacing between components");
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java
new file mode 100644
index 0000000..4890683
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java
@@ -0,0 +1,136 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.managers;
+
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Measurable;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size;
+
+/**
+ * Base class for layout managers -- resizable components.
+ */
+public abstract class LayoutManager extends LayoutComponent implements Measurable {
+
+    Size mCachedWrapSize = new Size(0f, 0f);
+
+    public LayoutManager(Component parent, int componentId, int animationId,
+                         float x, float y, float width, float height) {
+        super(parent, componentId, animationId, x, y, width, height);
+    }
+
+    /**
+     * Implemented by subclasses to provide a layout/measure pass
+     */
+    public void internalLayoutMeasure(PaintContext context,
+                                      MeasurePass measure) {
+        // nothing here
+    }
+
+    /**
+     * Subclasses can implement this to provide wrap sizing
+     */
+    public void computeWrapSize(PaintContext context, float maxWidth, float maxHeight,
+                                MeasurePass measure, Size size) {
+        // nothing here
+    }
+
+    /**
+     * Subclasses can implement this when not in wrap sizing
+     */
+    public void computeSize(PaintContext context, float minWidth, float maxWidth,
+                            float minHeight, float maxHeight, MeasurePass measure) {
+        // nothing here
+    }
+
+    /**
+     * Base implementation of the measure resolution
+     */
+    @Override
+    public void measure(PaintContext context, float minWidth, float maxWidth,
+                        float minHeight, float maxHeight, MeasurePass measure) {
+        boolean hasWrap = true;
+        float measuredWidth = Math.min(maxWidth,
+                computeModifierDefinedWidth() - mMarginLeft - mMarginRight);
+        float measuredHeight = Math.min(maxHeight,
+                computeModifierDefinedHeight() - mMarginTop - mMarginBottom);
+        float insetMaxWidth = maxWidth - mMarginLeft - mMarginRight;
+        float insetMaxHeight = maxHeight - mMarginTop - mMarginBottom;
+        if (mWidthModifier.isWrap() || mHeightModifier.isWrap()) {
+            mCachedWrapSize.setWidth(0f);
+            mCachedWrapSize.setHeight(0f);
+            computeWrapSize(context, maxWidth, maxHeight, measure, mCachedWrapSize);
+            measuredWidth = mCachedWrapSize.getWidth();
+            measuredHeight = mCachedWrapSize.getHeight();
+        } else {
+            hasWrap = false;
+        }
+        if (mWidthModifier.isFill()) {
+            measuredWidth = insetMaxWidth;
+        } else if (mWidthModifier.hasWeight()) {
+            measuredWidth = Math.max(measuredWidth, computeModifierDefinedWidth());
+        } else {
+            measuredWidth = Math.max(measuredWidth, minWidth);
+            measuredWidth = Math.min(measuredWidth, insetMaxWidth);
+        }
+        if (mHeightModifier.isFill()) {
+            measuredHeight = insetMaxHeight;
+        } else if (mHeightModifier.hasWeight()) {
+            measuredHeight = Math.max(measuredHeight, computeModifierDefinedHeight());
+        } else {
+            measuredHeight = Math.max(measuredHeight, minHeight);
+            measuredHeight = Math.min(measuredHeight, insetMaxHeight);
+        }
+        if (minWidth == maxWidth) {
+            measuredWidth = maxWidth;
+        }
+        if (minHeight == maxHeight) {
+            measuredHeight = maxHeight;
+        }
+        measuredWidth = Math.min(measuredWidth, insetMaxWidth);
+        measuredHeight = Math.min(measuredHeight, insetMaxHeight);
+        if (!hasWrap) {
+            computeSize(context, 0f, measuredWidth, 0f, measuredHeight, measure);
+        }
+        measuredWidth += mMarginLeft + mMarginRight;
+        measuredHeight += mMarginTop + mMarginBottom;
+
+        ComponentMeasure m = measure.get(this);
+        m.setW(measuredWidth);
+        m.setH(measuredHeight);
+
+        internalLayoutMeasure(context, measure);
+    }
+
+    /**
+     * basic layout of internal components
+     */
+    @Override
+    public void layout(RemoteContext context, MeasurePass measure) {
+        super.layout(context, measure);
+        ComponentMeasure self = measure.get(this);
+
+        mComponentModifiers.layout(context, self.getW(), self.getH());
+        for (Component c : mChildrenComponents) {
+            c.layout(context, measure);
+        }
+        this.mNeedsMeasure = false;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java
new file mode 100644
index 0000000..07e2ea1
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.managers;
+
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.FLOAT;
+import static com.android.internal.widget.remotecompose.core.documentation.Operation.INT;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentedCompanionOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStartOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size;
+import com.android.internal.widget.remotecompose.core.operations.layout.utils.DebugLog;
+
+import java.util.List;
+
+/**
+ * Simple Row layout implementation
+ * - also supports weight and horizontal/vertical positioning
+ */
+public class RowLayout extends LayoutManager implements ComponentStartOperation {
+    public static final int START = 1;
+    public static final int CENTER = 2;
+    public static final int END = 3;
+    public static final int TOP = 4;
+    public static final int BOTTOM = 5;
+    public static final int SPACE_BETWEEN = 6;
+    public static final int SPACE_EVENLY = 7;
+    public static final int SPACE_AROUND = 8;
+
+    public static final RowLayout.Companion COMPANION = new RowLayout.Companion();
+
+    int mHorizontalPositioning;
+    int mVerticalPositioning;
+    float mSpacedBy = 0f;
+
+    public RowLayout(Component parent, int componentId, int animationId,
+                     float x, float y, float width, float height,
+                     int horizontalPositioning, int verticalPositioning, float spacedBy) {
+        super(parent, componentId, animationId, x, y, width, height);
+        mHorizontalPositioning = horizontalPositioning;
+        mVerticalPositioning = verticalPositioning;
+        mSpacedBy = spacedBy;
+    }
+
+    public RowLayout(Component parent, int componentId, int animationId,
+                     int horizontalPositioning, int verticalPositioning, float spacedBy) {
+        this(parent, componentId, animationId, 0, 0, 0, 0,
+                horizontalPositioning, verticalPositioning, spacedBy);
+    }
+    @Override
+    public String toString() {
+        return "ROW [" + mComponentId + ":" + mAnimationId + "] (" + mX + ", "
+                + mY + " - " + mWidth + " x " + mHeight + ") " + mVisibility;
+    }
+
+    protected String getSerializedName() {
+        return "ROW";
+    }
+
+    @Override
+    public void computeWrapSize(PaintContext context, float maxWidth, float maxHeight,
+                                MeasurePass measure, Size size) {
+        DebugLog.s(() -> "COMPUTE WRAP SIZE in " + this + " (" + mComponentId + ")");
+        for (Component c : mChildrenComponents) {
+            c.measure(context, 0f, maxWidth, 0f, maxHeight, measure);
+            ComponentMeasure m = measure.get(c);
+            size.setWidth(size.getWidth() + m.getW());
+            size.setHeight(Math.max(size.getHeight(), m.getH()));
+        }
+        if (!mChildrenComponents.isEmpty()) {
+            size.setWidth(size.getWidth()
+                    + (mSpacedBy * (mChildrenComponents.size() - 1)));
+        }
+        DebugLog.e();
+    }
+
+    @Override
+    public void computeSize(PaintContext context, float minWidth, float maxWidth,
+                            float minHeight, float maxHeight, MeasurePass measure) {
+        DebugLog.s(() -> "COMPUTE SIZE in " + this + " (" + mComponentId + ")");
+        float mw = maxWidth;
+        for (Component child : mChildrenComponents) {
+            child.measure(context, minWidth, mw, minHeight, maxHeight, measure);
+            ComponentMeasure m = measure.get(child);
+            mw -= m.getW();
+        }
+        DebugLog.e();
+    }
+
+    @Override
+    public void internalLayoutMeasure(PaintContext context,
+                                      MeasurePass measure) {
+        ComponentMeasure selfMeasure = measure.get(this);
+        DebugLog.s(() -> "INTERNAL LAYOUT " + this + " (" + mComponentId + ") children: "
+                + mChildrenComponents.size() + " size (" + selfMeasure.getW()
+                + " x " + selfMeasure.getH() + ")");
+        if (mChildrenComponents.isEmpty()) {
+            DebugLog.e();
+            return;
+        }
+        float selfWidth = selfMeasure.getW() - mPaddingLeft - mPaddingRight;
+        float selfHeight = selfMeasure.getH() - mPaddingTop - mPaddingBottom;
+        float childrenWidth = 0f;
+        float childrenHeight = 0f;
+
+        boolean hasWeights = false;
+        float totalWeights = 0f;
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure childMeasure = measure.get(child);
+            if (child instanceof LayoutComponent
+                    && ((LayoutComponent) child).getWidthModifier().hasWeight()) {
+                hasWeights = true;
+                totalWeights += ((LayoutComponent) child).getWidthModifier().getValue();
+            } else {
+                childrenWidth += childMeasure.getW();
+            }
+        }
+
+        // TODO: need to move the weight measuring in the measure function,
+        // currently we'll measure unnecessarily
+        if (hasWeights) {
+            float availableSpace = selfWidth - childrenWidth;
+            for (Component child : mChildrenComponents) {
+                if (child instanceof LayoutComponent
+                        && ((LayoutComponent) child).getWidthModifier().hasWeight()) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    float weight = ((LayoutComponent) child).getWidthModifier().getValue();
+                    childMeasure.setW((weight * availableSpace) / totalWeights);
+                    child.measure(context, childMeasure.getW(),
+                            childMeasure.getW(), childMeasure.getH(), childMeasure.getH(), measure);
+                }
+            }
+        }
+
+        childrenWidth = 0f;
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure childMeasure = measure.get(child);
+            childrenWidth += childMeasure.getW();
+            childrenHeight = Math.max(childrenHeight, childMeasure.getH());
+        }
+        childrenWidth += mSpacedBy * (mChildrenComponents.size() - 1);
+
+        float tx = 0f;
+        float ty = 0f;
+
+        float horizontalGap = 0f;
+        float total = 0f;
+
+        switch (mHorizontalPositioning) {
+            case START:
+                tx = 0f;
+                break;
+            case END:
+                tx = selfWidth - childrenWidth;
+                break;
+            case CENTER:
+                tx = (selfWidth - childrenWidth) / 2f;
+                break;
+            case SPACE_BETWEEN:
+                for (Component child : mChildrenComponents) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    total += childMeasure.getW();
+                }
+                horizontalGap = (selfWidth - total) / (mChildrenComponents.size() - 1);
+                break;
+            case SPACE_EVENLY:
+                for (Component child : mChildrenComponents) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    total += childMeasure.getW();
+                }
+                horizontalGap = (selfWidth - total) / (mChildrenComponents.size() + 1);
+                tx = horizontalGap;
+                break;
+            case SPACE_AROUND:
+                for (Component child : mChildrenComponents) {
+                    ComponentMeasure childMeasure = measure.get(child);
+                    total += childMeasure.getW();
+                }
+                horizontalGap = (selfWidth - total) / (mChildrenComponents.size());
+                tx = horizontalGap / 2f;
+                break;
+        }
+
+        for (Component child : mChildrenComponents) {
+            ComponentMeasure childMeasure = measure.get(child);
+            switch (mVerticalPositioning) {
+                case TOP:
+                    ty = 0f;
+                    break;
+                case CENTER:
+                    ty = (selfHeight - childMeasure.getH()) / 2f;
+                    break;
+                case BOTTOM:
+                    ty = selfHeight - childMeasure.getH();
+                    break;
+            }
+            childMeasure.setX(tx);
+            childMeasure.setY(ty);
+            childMeasure.setVisibility(child.mVisibility);
+            tx += childMeasure.getW();
+            if (mHorizontalPositioning == SPACE_BETWEEN
+                    || mHorizontalPositioning == SPACE_AROUND
+                    || mHorizontalPositioning == SPACE_EVENLY) {
+                tx += horizontalGap;
+            }
+            tx += mSpacedBy;
+        }
+        DebugLog.e();
+    }
+
+    public static class Companion implements DocumentedCompanionOperation {
+        @Override
+        public String name() {
+            return "RowLayout";
+        }
+
+        @Override
+        public int id() {
+            return Operations.LAYOUT_ROW;
+        }
+
+        public void apply(WireBuffer buffer, int componentId, int animationId,
+                          int horizontalPositioning, int verticalPositioning, float spacedBy) {
+            buffer.start(Operations.LAYOUT_ROW);
+            buffer.writeInt(componentId);
+            buffer.writeInt(animationId);
+            buffer.writeInt(horizontalPositioning);
+            buffer.writeInt(verticalPositioning);
+            buffer.writeFloat(spacedBy);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int componentId = buffer.readInt();
+            int animationId = buffer.readInt();
+            int horizontalPositioning = buffer.readInt();
+            int verticalPositioning = buffer.readInt();
+            float spacedBy = buffer.readFloat();
+            operations.add(new RowLayout(null, componentId, animationId,
+                    horizontalPositioning, verticalPositioning, spacedBy));
+        }
+
+        @Override
+        public void documentation(DocumentationBuilder doc) {
+            doc.operation("Layout Operations", id(), name())
+                    .description("Row layout implementation, positioning components one"
+                            + " after the other horizontally.\n\n"
+                            + "It supports weight and horizontal/vertical positioning.")
+                    .examplesDimension(400, 100)
+                    .exampleImage("Start", "layout-RowLayout-start-top.png")
+                    .exampleImage("Center", "layout-RowLayout-center-top.png")
+                    .exampleImage("End", "layout-RowLayout-end-top.png")
+                    .exampleImage("SpaceEvenly", "layout-RowLayout-space-evenly-top.png")
+                    .exampleImage("SpaceAround", "layout-RowLayout-space-around-top.png")
+                    .exampleImage("SpaceBetween", "layout-RowLayout-space-between-top.png")
+                    .field(INT, "COMPONENT_ID", "unique id for this component")
+                    .field(INT, "ANIMATION_ID", "id used to match components,"
+                          + " for animation purposes")
+                    .field(INT, "HORIZONTAL_POSITIONING", "horizontal positioning value")
+                    .possibleValues("START", RowLayout.START)
+                    .possibleValues("CENTER", RowLayout.CENTER)
+                    .possibleValues("END", RowLayout.END)
+                    .possibleValues("SPACE_BETWEEN", RowLayout.SPACE_BETWEEN)
+                    .possibleValues("SPACE_EVENLY", RowLayout.SPACE_EVENLY)
+                    .possibleValues("SPACE_AROUND", RowLayout.SPACE_AROUND)
+                    .field(INT, "VERTICAL_POSITIONING", "vertical positioning value")
+                    .possibleValues("TOP", RowLayout.TOP)
+                    .possibleValues("CENTER", RowLayout.CENTER)
+                    .possibleValues("BOTTOM", RowLayout.BOTTOM)
+                    .field(FLOAT, "SPACED_BY", "Horizontal spacing between components");
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java
new file mode 100644
index 0000000..8dc10d5
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget.remotecompose.core.operations.layout.measure;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+
+/**
+ * Encapsulate the result of a measure pass for a component
+ */
+public class ComponentMeasure {
+    int mId = -1;
+    float mX;
+    float mY;
+    float mW;
+    float mH;
+    Component.Visibility mVisibility = Component.Visibility.VISIBLE;
+
+    public void setX(float value) {
+        mX = value;
+    }
+    public void setY(float value) {
+        mY = value;
+    }
+    public void setW(float value) {
+        mW = value;
+    }
+    public void setH(float value) {
+        mH = value;
+    }
+    public float getX() {
+        return mX;
+    }
+    public float getY() {
+        return mY;
+    }
+    public float getW() {
+        return mW;
+    }
+    public float getH() {
+        return mH;
+    }
+
+    public Component.Visibility getVisibility() {
+        return mVisibility;
+    }
+
+    public void setVisibility(Component.Visibility visibility) {
+        mVisibility = visibility;
+    }
+
+    public ComponentMeasure(int id, float x, float y, float w, float h,
+                            Component.Visibility visibility) {
+        this.mId = id;
+        this.mX = x;
+        this.mY = y;
+        this.mW = w;
+        this.mH = h;
+        this.mVisibility = visibility;
+    }
+
+    public ComponentMeasure(int id, float x, float y, float w, float h) {
+        this(id, x, y, w, h, Component.Visibility.VISIBLE);
+    }
+
+    public ComponentMeasure(Component component) {
+        this(component.getComponentId(), component.getX(), component.getY(),
+                component.getWidth(), component.getHeight(),
+                component.mVisibility);
+    }
+
+    public void copyFrom(ComponentMeasure m) {
+        mX = m.mX;
+        mY = m.mY;
+        mW = m.mW;
+        mH = m.mH;
+        mVisibility = m.mVisibility;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Measurable.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Measurable.java
new file mode 100644
index 0000000..d167d9b
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Measurable.java
@@ -0,0 +1,45 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.measure;
+
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+
+/**
+ * Interface describing the measure/layout contract for components
+ */
+public interface Measurable {
+
+    /**
+     * Measure a component and store the result of the measure in the provided MeasurePass.
+     * This does not apply the measure to the component.
+     */
+    void measure(PaintContext context, float minWidth, float maxWidth,
+                 float minHeight, float maxHeight, MeasurePass measure);
+
+    /**
+     * Apply a given measure to the component
+     */
+    void layout(RemoteContext context, MeasurePass measure);
+
+    /**
+     * Return true if the component needs to be remeasured
+     * @return true if need to remeasured, false otherwise
+     */
+    boolean needsMeasure();
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java
new file mode 100644
index 0000000..6801deb
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java
@@ -0,0 +1,63 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.measure;
+
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+
+import java.util.HashMap;
+
+/**
+ * Represents the result of a measure pass on the entire hierarchy
+ * TODO: optimize to use a flat array vs the current hashmap
+ */
+public class MeasurePass {
+    HashMap<Integer, ComponentMeasure> mList = new HashMap<>();
+
+    public void clear() {
+        mList.clear();
+    }
+
+    public void add(ComponentMeasure measure) throws Exception {
+        if (measure.mId == -1) {
+            throw new Exception("Component has no id!");
+        }
+        mList.put(measure.mId, measure);
+    }
+
+    public boolean contains(int id) {
+        return mList.containsKey(id);
+    }
+
+    public ComponentMeasure get(Component c) {
+        if (!mList.containsKey(c.getComponentId())) {
+            ComponentMeasure measure = new ComponentMeasure(c.getComponentId(),
+                    c.getX(), c.getY(), c.getWidth(), c.getHeight());
+            mList.put(c.getComponentId(), measure);
+            return measure;
+        }
+        return mList.get(c.getComponentId());
+    }
+
+    public ComponentMeasure get(int id) {
+        if (!mList.containsKey(id)) {
+            ComponentMeasure measure = new ComponentMeasure(id,
+                    0f, 0f, 0f, 0f, Component.Visibility.GONE);
+            mList.put(id, measure);
+            return measure;
+        }
+        return mList.get(id);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Size.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Size.java
new file mode 100644
index 0000000..b11d8e8
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/Size.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 com.android.internal.widget.remotecompose.core.operations.layout.measure;
+
+/**
+ * Basic data class representing a component size, used during layout computations.
+ */
+public class Size {
+    float mWidth;
+    float mHeight;
+    public Size(float width, float height) {
+        this.mWidth = width;
+        this.mHeight = height;
+    }
+
+    public void setWidth(float value) {
+        mWidth = value;
+    }
+
+    public void setHeight(float value) {
+        mHeight = value;
+    }
+
+    public float getWidth() {
+        return mWidth;
+    }
+
+    public float getHeight() {
+        return mHeight;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java
new file mode 100644
index 0000000..6f48aee
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java
@@ -0,0 +1,146 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/**
+ * Component size-aware background draw
+ */
+public class BackgroundModifierOperation extends DecoratorModifierOperation {
+
+    public static final BackgroundModifierOperation.Companion COMPANION =
+            new BackgroundModifierOperation.Companion();
+
+    float mX;
+    float mY;
+    float mWidth;
+    float mHeight;
+    float mR;
+    float mG;
+    float mB;
+    float mA;
+    int mShapeType = ShapeType.RECTANGLE;
+
+    public PaintBundle mPaint = new PaintBundle();
+
+    public BackgroundModifierOperation(float x, float y, float width, float height,
+                                       float r, float g, float b, float a,
+                                       int shapeType) {
+        this.mX = x;
+        this.mY = y;
+        this.mWidth = width;
+        this.mHeight = height;
+        this.mR = r;
+        this.mG = g;
+        this.mB = b;
+        this.mA = a;
+        this.mShapeType = shapeType;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mX, mY, mWidth, mHeight, mR, mG, mB, mA, mShapeType);
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(indent, "BACKGROUND = [" + mX + ", "
+                + mY + ", " + mWidth + ", " + mHeight
+                + "] color [" + mR + ", " + mG + ", " + mB + ", " + mA
+                + "] shape [" + mShapeType + "]");
+    }
+
+    @Override
+    public void layout(RemoteContext context, float width, float height) {
+        this.mWidth = width;
+        this.mHeight = height;
+    }
+
+    @Override
+    public String toString() {
+        return "BackgroundModifierOperation(" + mWidth + " x " + mHeight + ")";
+    }
+
+    public static class Companion implements CompanionOperation {
+
+
+        @Override
+        public String name() {
+            return "OrigamiBackground";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MODIFIER_BACKGROUND;
+        }
+
+        public void apply(WireBuffer buffer, float x, float y, float width, float height,
+                                 float r, float g, float b, float a, int shapeType) {
+            buffer.start(Operations.MODIFIER_BACKGROUND);
+            buffer.writeFloat(x);
+            buffer.writeFloat(y);
+            buffer.writeFloat(width);
+            buffer.writeFloat(height);
+            buffer.writeFloat(r);
+            buffer.writeFloat(g);
+            buffer.writeFloat(b);
+            buffer.writeFloat(a);
+            // shape type
+            buffer.writeInt(shapeType);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float x = buffer.readFloat();
+            float y = buffer.readFloat();
+            float width = buffer.readFloat();
+            float height = buffer.readFloat();
+            float r = buffer.readFloat();
+            float g = buffer.readFloat();
+            float b = buffer.readFloat();
+            float a = buffer.readFloat();
+            // shape type
+            int shapeType = buffer.readInt();
+            operations.add(new BackgroundModifierOperation(x, y, width, height,
+                    r, g, b, a, shapeType));
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.savePaint();
+        mPaint.reset();
+        mPaint.setColor(mR, mG, mB, mA);
+        context.applyPaint(mPaint);
+        if (mShapeType == ShapeType.RECTANGLE) {
+            context.drawRect(0f, 0f, mWidth, mHeight);
+        } else if (mShapeType == ShapeType.CIRCLE) {
+            context.drawCircle(mWidth / 2f, mHeight / 2f,
+                    Math.min(mWidth, mHeight) / 2f);
+        }
+        context.restorePaint();
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java
new file mode 100644
index 0000000..0b9c01b
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/**
+ * Component size-aware border draw
+ */
+public class BorderModifierOperation extends DecoratorModifierOperation {
+
+    public static final BorderModifierOperation.Companion COMPANION =
+            new BorderModifierOperation.Companion();
+
+    float mX;
+    float mY;
+    float mWidth;
+    float mHeight;
+    float mBorderWidth;
+    float mRoundedCorner;
+    float mR;
+    float mG;
+    float mB;
+    float mA;
+    int mShapeType = ShapeType.RECTANGLE;
+
+    public PaintBundle paint = new PaintBundle();
+
+    public BorderModifierOperation(float x, float y, float width, float height,
+                                   float borderWidth, float roundedCorner,
+                                   float r, float g, float b, float a, int shapeType) {
+        this.mX = x;
+        this.mY = y;
+        this.mWidth = width;
+        this.mHeight = height;
+        this.mBorderWidth = borderWidth;
+        this.mRoundedCorner = roundedCorner;
+        this.mR = r;
+        this.mG = g;
+        this.mB = b;
+        this.mA = a;
+        this.mShapeType = shapeType;
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(indent, "BORDER = [" + mX + ", " + mY + ", "
+                + mWidth + ", " + mHeight + "] "
+                + "color [" + mR + ", " + mG + ", " + mB + ", " + mA + "] "
+                + "border [" + mBorderWidth + ", " + mRoundedCorner + "] "
+                + "shape [" + mShapeType + "]");
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mX, mY, mWidth, mHeight, mBorderWidth, mRoundedCorner,
+                mR, mG, mB, mA, mShapeType);
+    }
+
+    @Override
+    public void layout(RemoteContext context, float width, float height) {
+        this.mWidth = width;
+        this.mHeight = height;
+    }
+
+    @Override
+    public String toString() {
+        return "BorderModifierOperation(" + mX + "," + mY + " - " + mWidth + " x " + mHeight + ") "
+                + "borderWidth(" + mBorderWidth + ") "
+                + "color(" + mR + "," + mG + "," + mB + "," + mA + ")";
+    }
+
+    public static class Companion implements CompanionOperation {
+
+        @Override
+        public String name() {
+            return "BorderModifier";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MODIFIER_BORDER;
+        }
+
+        public void apply(WireBuffer buffer, float x, float y, float width, float height,
+                                 float borderWidth, float roundedCorner,
+                                 float r, float g, float b, float a,
+                                 int shapeType) {
+            buffer.start(Operations.MODIFIER_BORDER);
+            buffer.writeFloat(x);
+            buffer.writeFloat(y);
+            buffer.writeFloat(width);
+            buffer.writeFloat(height);
+            buffer.writeFloat(borderWidth);
+            buffer.writeFloat(roundedCorner);
+            buffer.writeFloat(r);
+            buffer.writeFloat(g);
+            buffer.writeFloat(b);
+            buffer.writeFloat(a);
+            // shape type
+            buffer.writeInt(shapeType);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float x = buffer.readFloat();
+            float y = buffer.readFloat();
+            float width = buffer.readFloat();
+            float height = buffer.readFloat();
+            float bw = buffer.readFloat();
+            float rc = buffer.readFloat();
+            float r = buffer.readFloat();
+            float g = buffer.readFloat();
+            float b = buffer.readFloat();
+            float a = buffer.readFloat();
+            // shape type
+            int shapeType = buffer.readInt();
+            operations.add(new BorderModifierOperation(x, y, width, height, bw,
+                    rc, r, g, b, a, shapeType));
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.savePaint();
+        paint.reset();
+        paint.setColor(mR, mG, mB, mA);
+        paint.setStrokeWidth(mBorderWidth);
+        paint.setStyle(PaintBundle.STYLE_STROKE);
+        context.applyPaint(paint);
+        if (mShapeType == ShapeType.RECTANGLE) {
+            context.drawRect(0f, 0f, mWidth, mHeight);
+        } else {
+            float size = mRoundedCorner;
+            if (mShapeType == ShapeType.CIRCLE) {
+                size = Math.min(mWidth, mHeight) / 2f;
+            }
+            context.drawRoundRect(0f, 0f, mWidth, mHeight, size, size);
+        }
+        context.restorePaint();
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java
new file mode 100644
index 0000000..30357af
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java
@@ -0,0 +1,88 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/**
+ * Support modifier clip with a rectangle
+ */
+public class ClipRectModifierOperation extends DecoratorModifierOperation {
+
+    public static final ClipRectModifierOperation.Companion COMPANION =
+            new ClipRectModifierOperation.Companion();
+
+    float mWidth;
+    float mHeight;
+
+
+    @Override
+    public void paint(PaintContext context) {
+        context.clipRect(0f, 0f, mWidth, mHeight);
+    }
+
+    @Override
+    public void layout(RemoteContext context, float width, float height) {
+        this.mWidth = width;
+        this.mHeight = height;
+    }
+
+    @Override
+    public void onClick(float x, float y) {
+        // nothing
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(
+                indent, "CLIP_RECT = [" + mWidth + ", " + mHeight + "]");
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer);
+    }
+
+    public static class Companion implements CompanionOperation {
+
+        @Override
+        public String name() {
+            return "ClipRectModifier";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MODIFIER_CLIP_RECT;
+        }
+
+        public void apply(WireBuffer buffer) {
+            buffer.start(Operations.MODIFIER_CLIP_RECT);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            operations.add(new ClipRectModifierOperation());
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java
new file mode 100644
index 0000000..2ef0b9d
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java
@@ -0,0 +1,109 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.MatrixRestore;
+import com.android.internal.widget.remotecompose.core.operations.MatrixSave;
+import com.android.internal.widget.remotecompose.core.operations.layout.DecoratorComponent;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.ArrayList;
+
+/**
+ * Maintain a list of modifiers
+ */
+public class ComponentModifiers extends PaintOperation implements DecoratorComponent {
+    ArrayList<ModifierOperation> mList = new ArrayList<>();
+
+    public ArrayList<ModifierOperation> getList() {
+        return mList;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        // nothing
+    }
+
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(indent, "MODIFIERS");
+        for (ModifierOperation m : mList) {
+            m.serializeToString(indent + 1, serializer);
+        }
+    }
+
+    public void add(ModifierOperation operation) {
+        mList.add(operation);
+    }
+
+    public int size() {
+        return mList.size();
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        float tx = 0f;
+        float ty = 0f;
+        for (ModifierOperation op : mList) {
+            if (op instanceof PaddingModifierOperation) {
+                PaddingModifierOperation pop = (PaddingModifierOperation) op;
+                context.translate(pop.getLeft(), pop.getTop());
+                tx += pop.getLeft();
+                ty += pop.getTop();
+            }
+            if (op instanceof MatrixSave || op instanceof MatrixRestore) {
+                continue;
+            }
+            if (op instanceof PaintOperation) {
+                ((PaintOperation) op).paint(context);
+            }
+        }
+        // Back out the translates created by paddings
+        // TODO: we should be able to get rid of this when drawing the content of a component
+        context.translate(-tx, -ty);
+    }
+
+    @Override
+    public void layout(RemoteContext context, float width, float height) {
+        float w = width;
+        float h = height;
+        for (ModifierOperation op : mList) {
+            if (op instanceof PaddingModifierOperation) {
+                PaddingModifierOperation pop = (PaddingModifierOperation) op;
+                w -= pop.getLeft() + pop.getRight();
+                h -= pop.getTop() + pop.getBottom();
+            }
+            if (op instanceof DecoratorComponent) {
+                ((DecoratorComponent) op).layout(context, w, h);
+            }
+        }
+    }
+
+    public void addAll(ArrayList<ModifierOperation> operations) {
+        mList.addAll(operations);
+    }
+
+    public void onClick(float x, float y) {
+        for (ModifierOperation op : mList) {
+            if (op instanceof DecoratorComponent) {
+                ((DecoratorComponent) op).onClick(x, y);
+            }
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DecoratorModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DecoratorModifierOperation.java
new file mode 100644
index 0000000..bf9b27b
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DecoratorModifierOperation.java
@@ -0,0 +1,32 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.DecoratorComponent;
+
+/**
+ * Represents a decorator modifier (lightweight component), ie a modifier
+ * that impacts the visual output (background, border...)
+ */
+public abstract class DecoratorModifierOperation extends PaintOperation
+        implements ModifierOperation, DecoratorComponent {
+
+    @Override
+    public void onClick(float x, float y) {
+        // nothing
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java
new file mode 100644
index 0000000..04e9431
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java
@@ -0,0 +1,159 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/**
+ * Base class for dimension modifiers
+ */
+public class DimensionModifierOperation implements ModifierOperation {
+
+    public static final DimensionModifierOperation.Companion COMPANION =
+            new DimensionModifierOperation.Companion(0, "DIMENSION");
+
+    public enum Type {
+        EXACT, FILL, WRAP, WEIGHT, INTRINSIC_MIN, INTRINSIC_MAX;
+
+        static Type fromInt(int value) {
+            switch (value) {
+                case 0: return EXACT;
+                case 1: return FILL;
+                case 2: return WRAP;
+                case 3: return WEIGHT;
+                case 4: return INTRINSIC_MIN;
+                case 5: return INTRINSIC_MAX;
+            }
+            return EXACT;
+        }
+    }
+
+    Type mType = Type.EXACT;
+    float mValue = Float.NaN;
+
+    public DimensionModifierOperation(Type type, float value) {
+        mType = type;
+        mValue = value;
+    }
+
+    public DimensionModifierOperation(Type type) {
+        this(type, Float.NaN);
+    }
+
+    public DimensionModifierOperation(float value) {
+        this(Type.EXACT, value);
+    }
+
+
+    public boolean hasWeight() {
+        return mType == Type.WEIGHT;
+    }
+
+    public boolean isWrap() {
+        return mType == Type.WRAP;
+    }
+
+    public boolean isFill() {
+        return mType == Type.FILL;
+    }
+
+    public Type getType() {
+        return mType;
+    }
+
+    public float getValue() {
+        return mValue;
+    }
+
+    public void setValue(float value) {
+        this.mValue = value;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mType.ordinal(), mValue);
+    }
+
+    public String serializedName() {
+        return "DIMENSION";
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        if (mType == Type.EXACT) {
+            serializer.append(indent, serializedName() + " = " + mValue);
+        }
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return (indent != null ? indent : "") + toString();
+    }
+
+    @Override
+    public String toString() {
+        return "DimensionModifierOperation(" + mValue + ")";
+    }
+
+    public static class Companion implements CompanionOperation {
+
+        int mOperation;
+        String mName;
+
+        public Companion(int operation, String name) {
+            mOperation = operation;
+            mName = name;
+        }
+
+        @Override
+        public String name() {
+            return mName;
+        }
+
+        @Override
+        public int id() {
+            return mOperation;
+        }
+
+        public void apply(WireBuffer buffer, int type, float value) {
+            buffer.start(mOperation);
+            buffer.writeInt(type);
+            buffer.writeFloat(value);
+        }
+
+        public Operation construct(Type type, float value) {
+            return null;
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            Type type = Type.fromInt(buffer.readInt());
+            float value = buffer.readFloat();
+            Operation op = construct(type, value);
+            operations.add(op);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java
new file mode 100644
index 0000000..81173c3
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+
+/**
+ * Set the height dimension on a component
+ */
+public class HeightModifierOperation extends DimensionModifierOperation {
+
+    public static final DimensionModifierOperation.Companion COMPANION =
+            new DimensionModifierOperation.Companion(Operations.MODIFIER_HEIGHT, "WIDTH") {
+                @Override
+                public Operation construct(DimensionModifierOperation.Type type, float value) {
+                    return new HeightModifierOperation(type, value);
+                }
+            };
+
+    public HeightModifierOperation(Type type, float value) {
+        super(type, value);
+    }
+
+    public HeightModifierOperation(Type type) {
+        super(type);
+    }
+
+    public HeightModifierOperation(float value) {
+        super(value);
+    }
+
+    @Override
+    public String toString() {
+        return "Height(" + mValue + ")";
+    }
+
+    @Override
+    public String serializedName() {
+        return "HEIGHT";
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java
new file mode 100644
index 0000000..5299719
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+/**
+ * Represents a modifier
+ */
+public interface ModifierOperation extends Operation {
+    void serializeToString(int indent, StringSerializer serializer);
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java
new file mode 100644
index 0000000..5ea6a97
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java
@@ -0,0 +1,135 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+/**
+ * Represents a padding modifier.
+ * Padding modifiers can be chained and will impact following modifiers.
+ */
+public class PaddingModifierOperation implements ModifierOperation {
+
+    public static final PaddingModifierOperation.Companion COMPANION =
+            new PaddingModifierOperation.Companion();
+
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+
+    public PaddingModifierOperation(float left, float top, float right, float bottom) {
+        this.mLeft = left;
+        this.mTop = top;
+        this.mRight = right;
+        this.mBottom = bottom;
+    }
+
+    public float getLeft() {
+        return mLeft;
+    }
+
+    public float getTop() {
+        return mTop;
+    }
+
+    public float getRight() {
+        return mRight;
+    }
+
+    public float getBottom() {
+        return mBottom;
+    }
+
+    public void setLeft(float left) {
+        this.mLeft = left;
+    }
+
+    public void setTop(float top) {
+        this.mTop = top;
+    }
+
+    public void setRight(float right) {
+        this.mRight = right;
+    }
+
+    public void setBottom(float bottom) {
+        this.mBottom = bottom;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mLeft, mTop, mRight, mBottom);
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(indent, "PADDING = [" + mLeft + ", " + mTop + ", "
+                + mRight + ", " + mBottom + "]");
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return (indent != null ? indent : "") + toString();
+    }
+
+    @Override
+    public String toString() {
+        return "PaddingModifierOperation(" + mLeft + ", " + mTop
+                + ", " + mRight + ", " + mBottom + ")";
+    }
+
+    public static class Companion implements CompanionOperation {
+        @Override
+        public String name() {
+            return "PaddingModifierOperation";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MODIFIER_PADDING;
+        }
+
+        public void apply(WireBuffer buffer,
+                                 float left, float top, float right, float bottom) {
+            buffer.start(Operations.MODIFIER_PADDING);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float left = buffer.readFloat();
+            float top = buffer.readFloat();
+            float right = buffer.readFloat();
+            float bottom = buffer.readFloat();
+            operations.add(new PaddingModifierOperation(left, top, right, bottom));
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RoundedClipRectModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RoundedClipRectModifierOperation.java
new file mode 100644
index 0000000..9c57c6a
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RoundedClipRectModifierOperation.java
@@ -0,0 +1,78 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.operations.DrawBase4;
+import com.android.internal.widget.remotecompose.core.operations.layout.DecoratorComponent;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+/**
+ * Support clip with a rectangle
+ */
+public class RoundedClipRectModifierOperation extends DrawBase4
+        implements ModifierOperation, DecoratorComponent {
+
+    public static final Companion COMPANION =
+            new Companion(Operations.MODIFIER_ROUNDED_CLIP_RECT) {
+                @Override
+                public Operation construct(float x1,
+                                           float y1,
+                                           float x2,
+                                           float y2) {
+                    return new RoundedClipRectModifierOperation(x1, y1, x2, y2);
+                }
+            };
+    float mWidth;
+    float mHeight;
+
+
+    public RoundedClipRectModifierOperation(
+            float topStart,
+            float topEnd,
+            float bottomStart,
+            float bottomEnd) {
+        super(topStart, topEnd, bottomStart, bottomEnd);
+        mName = "ModifierRoundedClipRect";
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.roundedClipRect(mWidth, mHeight, mX1, mY1, mX2, mY2);
+    }
+
+    @Override
+    public void layout(RemoteContext context, float width, float height) {
+        this.mWidth = width;
+        this.mHeight = height;
+    }
+
+    @Override
+    public void onClick(float x, float y) {
+        // nothing
+    }
+
+    @Override
+    public void serializeToString(int indent, StringSerializer serializer) {
+        serializer.append(
+                indent, "ROUND_CLIP = [" + mWidth + ", " + mHeight
+                        + ", " + mX1 + ", " + mY1
+                        + ", " + mX2 + ", " + mY2 + "]");
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ShapeType.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ShapeType.java
new file mode 100644
index 0000000..e425b4e
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ShapeType.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+/**
+ * Known shapes, used for modifiers (clip/background/border)
+ */
+public class ShapeType {
+    public static int RECTANGLE = 0;
+    public static int CIRCLE = 1;
+    public static int ROUNDED_RECTANGLE = 2;
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java
new file mode 100644
index 0000000..c46c8d7
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+
+/**
+ * Set the width dimension on a component
+ */
+public class WidthModifierOperation extends DimensionModifierOperation {
+
+    public static final DimensionModifierOperation.Companion COMPANION =
+            new DimensionModifierOperation.Companion(Operations.MODIFIER_WIDTH, "WIDTH") {
+                @Override
+                public Operation construct(DimensionModifierOperation.Type type, float value) {
+                    return new WidthModifierOperation(type, value);
+                }
+            };
+
+    public WidthModifierOperation(Type type, float value) {
+        super(type, value);
+    }
+
+    public WidthModifierOperation(Type type) {
+        super(type);
+    }
+
+    public WidthModifierOperation(float value) {
+        super(value);
+    }
+
+    @Override
+    public String toString() {
+        return "Width(" + mValue + ")";
+    }
+
+    @Override
+    public String serializedName() {
+        return "WIDTH";
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/DebugLog.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/DebugLog.java
new file mode 100644
index 0000000..7ccf7f4
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/DebugLog.java
@@ -0,0 +1,127 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.utils;
+
+import java.util.ArrayList;
+
+/**
+ * Internal utility debug class
+ */
+public class DebugLog {
+
+    public static final boolean DEBUG_LAYOUT_ON = false;
+
+    public static class Node {
+        public Node parent;
+        public String name;
+        public String endString;
+        public ArrayList<Node> list = new ArrayList<>();
+
+        public Node(Node parent, String name) {
+            this.parent = parent;
+            this.name = name;
+            this.endString = name + " DONE";
+            if (parent != null) {
+                parent.add(this);
+            }
+        }
+
+        public void add(Node node) {
+            list.add(node);
+        }
+    }
+
+    public static class LogNode extends Node {
+        public LogNode(Node parent, String name) {
+            super(parent, name);
+        }
+    }
+
+    public static Node node = new Node(null, "Root");
+    public static Node currentNode = node;
+
+    public static void clear() {
+        node = new Node(null, "Root");
+        currentNode = node;
+    }
+
+    public static void s(StringValueSupplier valueSupplier) {
+        if (DEBUG_LAYOUT_ON) {
+            currentNode = new Node(currentNode, valueSupplier.getString());
+        }
+    }
+
+    public static void log(StringValueSupplier valueSupplier) {
+        if (DEBUG_LAYOUT_ON) {
+            new LogNode(currentNode, valueSupplier.getString());
+        }
+    }
+
+    public static void e() {
+        if (DEBUG_LAYOUT_ON) {
+            if (currentNode.parent != null) {
+                currentNode = currentNode.parent;
+            } else {
+                currentNode = node;
+            }
+        }
+    }
+
+    public static void e(StringValueSupplier valueSupplier) {
+        if (DEBUG_LAYOUT_ON) {
+            currentNode.endString = valueSupplier.getString();
+            if (currentNode.parent != null) {
+                currentNode = currentNode.parent;
+            } else {
+                currentNode = node;
+            }
+        }
+    }
+
+    public static void printNode(int indent, Node node, StringBuilder builder) {
+        if (DEBUG_LAYOUT_ON) {
+            StringBuilder indentationBuilder = new StringBuilder();
+            for (int i = 0; i < indent; i++) {
+                indentationBuilder.append("| ");
+            }
+            String indentation = indentationBuilder.toString();
+
+            if (node.list.size() > 0) {
+                builder.append(indentation).append(node.name).append("\n");
+                for (Node c : node.list) {
+                    printNode(indent + 1, c, builder);
+                }
+                builder.append(indentation).append(node.endString).append("\n");
+            } else {
+                if (node instanceof LogNode) {
+                    builder.append(indentation).append("     ").append(node.name).append("\n");
+                } else {
+                    builder.append(indentation).append("-- ").append(node.name)
+                            .append(" : ").append(node.endString).append("\n");
+                }
+            }
+        }
+    }
+
+    public static void display() {
+        if (DEBUG_LAYOUT_ON) {
+            StringBuilder builder = new StringBuilder();
+            printNode(0, node, builder);
+            System.out.println("\n" + builder.toString());
+        }
+    }
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/StringValueSupplier.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/StringValueSupplier.java
new file mode 100644
index 0000000..79ef16b
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/utils/StringValueSupplier.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.utils;
+
+/**
+ * Basic interface for a lambda (used for logging)
+ */
+public interface StringValueSupplier {
+    /**
+     * returns a string value
+     * @return a string
+     */
+    String getString();
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java
index a7d0ac6..8186192 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java
@@ -695,6 +695,29 @@
     }
 
     /**
+     * Set the color based the R,G,B,A values
+     * @param r red (0 to 255)
+     * @param g green (0 to 255)
+     * @param b blue (0 to 255)
+     * @param a alpha (0 to 255)
+     */
+    public void setColor(int r, int g, int b, int a) {
+        int color = (a << 24) | (r << 16) | (g << 8) | b;
+        setColor(color);
+    }
+
+    /**
+     * Set the color based the R,G,B,A values
+     * @param r red (0.0 to 1.0)
+     * @param g green (0.0 to 1.0)
+     * @param b blue (0.0 to 1.0)
+     * @param a alpha (0.0 to 1.0)
+     */
+    public void setColor(float r, float g, float b, float a) {
+        setColor((int) r * 255, (int) g * 255, (int) b * 255, (int) a * 255);
+    }
+
+    /**
      * Set the Color based on ID
      * @param color
      */
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntFloatMap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntFloatMap.java
index 23c3ec5..b2d714e 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntFloatMap.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntFloatMap.java
@@ -54,7 +54,7 @@
     /**
      * Put a item in the map
      *
-     * @param key   item'values key
+     * @param key item's key
      * @param value item's value
      * @return old value if exist
      */
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntIntMap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntIntMap.java
index 221014c..606dc78 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntIntMap.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntIntMap.java
@@ -53,7 +53,7 @@
     /**
      * Put a item in the map
      *
-     * @param key   item'values key
+     * @param key item's key
      * @param value item's value
      * @return old value if exist
      */
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java
index 4c1389c..a4fce80 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntegerExpressionEvaluator.java
@@ -400,8 +400,7 @@
             -1, // no op
             2, 2, 2, 2, 2, // + - * / %
             2, 2, 2, 2, 2, 2, 2, 2, 2, //<<, >> , >>> , | , &, ^, min max
-            1, 1, 1, 1, 1, 1,   // neg, abs, ++, -- , not , sign
-
+            1, 1, 1, 1, 1, 1,  // neg, abs, ++, -- , not , sign
             3, 3, 3, // clamp, ifElse, mad,
             0, 0, 0 // mad, ?:,
             // a[0],a[1],a[2]
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/StringSerializer.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/StringSerializer.java
new file mode 100644
index 0000000..fb90781
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/StringSerializer.java
@@ -0,0 +1,52 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.utilities;
+
+/**
+ * Utility serializer maintaining an indent buffer
+ */
+public class StringSerializer {
+    StringBuffer mBuffer = new StringBuffer();
+    String mIndentBuffer = "                                                                      ";
+
+    /**
+     * Append some content to the current buffer
+     * @param indent the indentation level to use
+     * @param content content to append
+     */
+    public void append(int indent, String content) {
+        String indentation = mIndentBuffer.substring(0, indent);
+        mBuffer.append(indentation);
+        mBuffer.append(indentation);
+        mBuffer.append(content);
+        mBuffer.append("\n");
+    }
+
+    /**
+     * Reset the buffer
+     */
+    public void reset() {
+        mBuffer = new StringBuffer();
+    }
+
+    /**
+     * Return a string representation of the buffer
+     * @return string representation
+     */
+    public String toString() {
+        return mBuffer.toString();
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/GeneralEasing.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/GeneralEasing.java
index 693deaf..50a7d59 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/GeneralEasing.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/GeneralEasing.java
@@ -18,7 +18,7 @@
 /**
  * Provides and interface to create easing functions
  */
-public class GeneralEasing extends  Easing{
+public class GeneralEasing extends Easing{
     float[] mEasingData = new float[0];
     Easing mEasingCurve = new CubicEasing(CUBIC_STANDARD);
 
diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java
index a42c584..65a337e 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposeDocument.java
@@ -18,6 +18,7 @@
 import com.android.internal.widget.remotecompose.core.CoreDocument;
 import com.android.internal.widget.remotecompose.core.RemoteComposeBuffer;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
 
 import java.io.InputStream;
 
@@ -113,5 +114,13 @@
         return mDocument.getNamedColors();
     }
 
+    /**
+     * Return a component associated with id
+     * @param id the component id
+     * @return the corresponding component or null if not found
+     */
+    public Component getComponent(int id) {
+        return mDocument.getComponent(id);
+    }
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
index 39a770a..e01dd17 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
@@ -65,6 +65,21 @@
         this.mCanvas = canvas;
     }
 
+    @Override
+    public void save() {
+        mCanvas.save();
+    }
+
+    @Override
+    public void saveLayer(float x, float y, float width, float height) {
+        mCanvas.saveLayer(x, y, x + width, y + height, mPaint);
+    }
+
+    @Override
+    public void restore() {
+        mCanvas.restore();
+    }
+
     /**
      * Draw an image onto the canvas
      *
@@ -613,6 +628,19 @@
     }
 
     @Override
+    public void roundedClipRect(float width, float height,
+                                float topStart, float topEnd,
+                                float bottomStart, float bottomEnd) {
+        Path roundedPath = new Path();
+        float[] radii = new float[] { topStart, topStart,
+                topEnd, topEnd, bottomEnd, bottomEnd, bottomStart, bottomStart};
+
+        roundedPath.addRoundRect(0f, 0f, width, height,
+                radii, android.graphics.Path.Direction.CW);
+        mCanvas.clipPath(roundedPath);
+    }
+
+    @Override
     public void clipPath(int pathId, int regionOp) {
         Path path = getPath(pathId, 0, 1);
         if (regionOp == ClipPath.DIFFERENCE) {
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
index a2f79cc..0d7f97a 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
@@ -215,6 +215,7 @@
         }
         int w = measureDimension(widthMeasureSpec, mDocument.getWidth());
         int h = measureDimension(heightMeasureSpec, mDocument.getHeight());
+        mDocument.getDocument().invalidateMeasure();
 
         if (!USE_VIEW_AREA_CLICK) {
             if (mDocument.getDocument().getContentSizing() == RootContentBehavior.SIZING_SCALE) {
@@ -235,6 +236,8 @@
         if (mDocument == null) {
             return;
         }
+        mARContext.setAnimationEnabled(true);
+        mARContext.currentTime = System.currentTimeMillis();
         mARContext.setDebug(mDebug);
         mARContext.useCanvas(canvas);
         mARContext.mWidth = getWidth();
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index ca984c0..2abdd57 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -258,6 +258,7 @@
                 "com_android_internal_content_om_OverlayManagerImpl.cpp",
                 "com_android_internal_net_NetworkUtilsInternal.cpp",
                 "com_android_internal_os_ClassLoaderFactory.cpp",
+                "com_android_internal_os_DebugStore.cpp",
                 "com_android_internal_os_FuseAppLoop.cpp",
                 "com_android_internal_os_KernelAllocationStats.cpp",
                 "com_android_internal_os_KernelCpuBpfTracking.cpp",
@@ -315,6 +316,7 @@
                 "libcrypto",
                 "libcutils",
                 "libdebuggerd_client",
+                "libdebugstore_cxx",
                 "libutils",
                 "libbinder",
                 "libbinderdebug",
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index ed59327..03b5143a 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -202,6 +202,7 @@
 extern int register_com_android_internal_content_om_OverlayManagerImpl(JNIEnv* env);
 extern int register_com_android_internal_net_NetworkUtilsInternal(JNIEnv* env);
 extern int register_com_android_internal_os_ClassLoaderFactory(JNIEnv* env);
+extern int register_com_android_internal_os_DebugStore(JNIEnv* env);
 extern int register_com_android_internal_os_FuseAppLoop(JNIEnv* env);
 extern int register_com_android_internal_os_KernelAllocationStats(JNIEnv* env);
 extern int register_com_android_internal_os_KernelCpuBpfTracking(JNIEnv* env);
@@ -1599,6 +1600,7 @@
         REG_JNI(register_com_android_internal_content_om_OverlayManagerImpl),
         REG_JNI(register_com_android_internal_net_NetworkUtilsInternal),
         REG_JNI(register_com_android_internal_os_ClassLoaderFactory),
+        REG_JNI(register_com_android_internal_os_DebugStore),
         REG_JNI(register_com_android_internal_os_LongArrayMultiStateCounter),
         REG_JNI(register_com_android_internal_os_LongMultiStateCounter),
         REG_JNI(register_com_android_internal_os_Zygote),
diff --git a/core/jni/com_android_internal_os_DebugStore.cpp b/core/jni/com_android_internal_os_DebugStore.cpp
new file mode 100644
index 0000000..874d6ea
--- /dev/null
+++ b/core/jni/com_android_internal_os_DebugStore.cpp
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+#include <debugstore/debugstore_cxx_bridge.rs.h>
+#include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <nativehelper/ScopedUtfChars.h>
+
+#include <iterator>
+#include <sstream>
+#include <vector>
+
+#include "core_jni_helpers.h"
+
+namespace android {
+
+static struct {
+    jmethodID mGet;
+    jmethodID mSize;
+} gListClassInfo;
+
+static std::vector<std::string> list_to_vector(JNIEnv* env, jobject jList) {
+    std::vector<std::string> vec;
+    jint size = env->CallIntMethod(jList, gListClassInfo.mSize);
+    if (size % 2 != 0) {
+        std::ostringstream oss;
+
+        std::copy(vec.begin(), vec.end(), std::ostream_iterator<std::string>(oss, ", "));
+        ALOGW("DebugStore list size is odd: %d, elements: %s", size, oss.str().c_str());
+
+        return vec;
+    }
+
+    vec.reserve(size);
+
+    for (jint i = 0; i < size; i++) {
+        ScopedLocalRef<jstring> jEntry(env,
+                                       reinterpret_cast<jstring>(
+                                               env->CallObjectMethod(jList, gListClassInfo.mGet,
+                                                                     i)));
+        ScopedUtfChars cEntry(env, jEntry.get());
+        vec.emplace_back(cEntry.c_str());
+    }
+    return vec;
+}
+
+static void com_android_internal_os_DebugStore_endEvent(JNIEnv* env, jclass clazz, jlong eventId,
+                                                        jobject jAttributeList) {
+    auto attributes = list_to_vector(env, jAttributeList);
+    debugstore::debug_store_end(static_cast<uint64_t>(eventId), attributes);
+}
+
+static jlong com_android_internal_os_DebugStore_beginEvent(JNIEnv* env, jclass clazz,
+                                                           jstring jeventName,
+                                                           jobject jAttributeList) {
+    ScopedUtfChars eventName(env, jeventName);
+    auto attributes = list_to_vector(env, jAttributeList);
+    jlong eventId =
+            static_cast<jlong>(debugstore::debug_store_begin(eventName.c_str(), attributes));
+    return eventId;
+}
+
+static void com_android_internal_os_DebugStore_recordEvent(JNIEnv* env, jclass clazz,
+                                                           jstring jeventName,
+                                                           jobject jAttributeList) {
+    ScopedUtfChars eventName(env, jeventName);
+    auto attributes = list_to_vector(env, jAttributeList);
+    debugstore::debug_store_record(eventName.c_str(), attributes);
+}
+
+static const JNINativeMethod gDebugStoreMethods[] = {
+        /* name, signature, funcPtr */
+        {"beginEventNative", "(Ljava/lang/String;Ljava/util/List;)J",
+         (void*)com_android_internal_os_DebugStore_beginEvent},
+        {"endEventNative", "(JLjava/util/List;)V",
+         (void*)com_android_internal_os_DebugStore_endEvent},
+        {"recordEventNative", "(Ljava/lang/String;Ljava/util/List;)V",
+         (void*)com_android_internal_os_DebugStore_recordEvent},
+};
+
+int register_com_android_internal_os_DebugStore(JNIEnv* env) {
+    int res = RegisterMethodsOrDie(env, "com/android/internal/os/DebugStore", gDebugStoreMethods,
+                                   NELEM(gDebugStoreMethods));
+    jclass listClass = FindClassOrDie(env, "java/util/List");
+    gListClassInfo.mGet = GetMethodIDOrDie(env, listClass, "get", "(I)Ljava/lang/Object;");
+    gListClassInfo.mSize = GetMethodIDOrDie(env, listClass, "size", "()I");
+
+    return res;
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/core/res/Android.bp b/core/res/Android.bp
index 9207aa8..e900eb2 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -164,6 +164,7 @@
         "com.android.window.flags.window-aconfig",
         "android.permission.flags-aconfig",
         "android.os.flags-aconfig",
+        "android.media.tv.flags-aconfig",
     ],
 }
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index a00cc8b..50727a2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5609,7 +5609,8 @@
          @hide
     -->
     <permission android:name="android.permission.ALWAYS_BOUND_TV_INPUT"
-        android:protectionLevel="signature|privileged|vendorPrivileged" />
+        android:protectionLevel="signature|privileged|vendorPrivileged"
+        android:featureFlag="android.media.tv.flags.tis_always_bound_permission"/>
 
     <!-- Must be required by a {@link android.media.tv.interactive.TvInteractiveAppService}
          to ensure that only the system can bind to it.
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 495af5b..af0272e 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3961,6 +3961,9 @@
          flag does not exist -->
     <bool name="config_magnification_always_on_enabled">true</bool>
 
+    <!-- Whether to keep fullscreen magnification zoom level when context changes. -->
+    <bool name="config_magnification_keep_zoom_level_when_context_changed">false</bool>
+
     <!-- If true, the display will be shifted around in ambient mode. -->
     <bool name="config_enableBurnInProtection">false</bool>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b158e0f..8f4018f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4779,6 +4779,7 @@
 
   <java-symbol type="bool" name="config_magnification_area" />
   <java-symbol type="bool" name="config_magnification_always_on_enabled" />
+  <java-symbol type="bool" name="config_magnification_keep_zoom_level_when_context_changed" />
 
   <java-symbol type="bool" name="config_trackerAppNeedsPermissions"/>
   <!-- FullScreenMagnification thumbnail -->
diff --git a/core/tests/coretests/src/android/os/FileUtilsTest.java b/core/tests/coretests/src/android/os/FileUtilsTest.java
index 66b22a8..774878a 100644
--- a/core/tests/coretests/src/android/os/FileUtilsTest.java
+++ b/core/tests/coretests/src/android/os/FileUtilsTest.java
@@ -54,7 +54,7 @@
 import static org.junit.Assert.fail;
 
 import android.os.FileUtils.MemoryPipe;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
+import android.platform.test.annotations.DisabledOnRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 import android.provider.DocumentsContract.Document;
 import android.system.Os;
@@ -156,7 +156,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = MemoryPipe.class)
+    @DisabledOnRavenwood(blockedBy = MemoryPipe.class)
     public void testCopy_FileToPipe() throws Exception {
         for (int size : DATA_SIZES) {
             final File src = new File(mTarget, "src");
@@ -177,7 +177,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = MemoryPipe.class)
+    @DisabledOnRavenwood(blockedBy = MemoryPipe.class)
     public void testCopy_PipeToFile() throws Exception {
         for (int size : DATA_SIZES) {
             final File dest = new File(mTarget, "dest");
@@ -197,7 +197,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = MemoryPipe.class)
+    @DisabledOnRavenwood(blockedBy = MemoryPipe.class)
     public void testCopy_PipeToPipe() throws Exception {
         for (int size : DATA_SIZES) {
             byte[] expected = new byte[size];
@@ -215,7 +215,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = MemoryPipe.class)
+    @DisabledOnRavenwood(blockedBy = MemoryPipe.class)
     public void testCopy_ShortPipeToFile() throws Exception {
         byte[] source = new byte[33_000_000];
         new Random().nextBytes(source);
@@ -257,9 +257,9 @@
         assertArrayEquals(expected, actual);
     }
 
-    //TODO(ravenwood) Remove the _$noRavenwood suffix and add @RavenwoodIgnore instead
     @Test
-    public void testCopy_SocketToFile_FileToSocket$noRavenwood() throws Exception {
+    @DisabledOnRavenwood(reason = "Missing Os methods in Ravenwood")
+    public void testCopy_SocketToFile_FileToSocket() throws Exception {
         for (int size : DATA_SIZES ) {
             final File src = new File(mTarget, "src");
             final File dest = new File(mTarget, "dest");
@@ -510,7 +510,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
+    @DisabledOnRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
     public void testBuildUniqueFile_normal() throws Exception {
         assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test"));
         assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.jpg"));
@@ -530,7 +530,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
+    @DisabledOnRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
     public void testBuildUniqueFile_unknown() throws Exception {
         assertNameEquals("test",
                 FileUtils.buildUniqueFile(mTarget, "application/octet-stream", "test"));
@@ -544,7 +544,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
+    @DisabledOnRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
     public void testBuildUniqueFile_dir() throws Exception {
         assertNameEquals("test", FileUtils.buildUniqueFile(mTarget, Document.MIME_TYPE_DIR, "test"));
         new File(mTarget, "test").mkdir();
@@ -559,7 +559,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
+    @DisabledOnRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
     public void testBuildUniqueFile_increment() throws Exception {
         assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.jpg"));
         new File(mTarget, "test.jpg").createNewFile();
@@ -579,7 +579,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
+    @DisabledOnRavenwood(blockedBy = android.webkit.MimeTypeMap.class)
     public void testBuildUniqueFile_mimeless() throws Exception {
         assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "test.jpg"));
         new File(mTarget, "test.jpg").createNewFile();
@@ -675,8 +675,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Requires kernel support")
-    public void testTranslateMode() throws Exception {
+    public void testTranslateMode() {
         assertTranslate("r", O_RDONLY, MODE_READ_ONLY);
 
         assertTranslate("rw", O_RDWR | O_CREAT,
@@ -695,8 +694,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Requires kernel support")
-    public void testMalformedTransate_int() throws Exception {
+    public void testMalformedTransate_int() {
         try {
             // The non-standard Linux access mode 3 should throw
             // an IllegalArgumentException.
@@ -707,8 +705,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Requires kernel support")
-    public void testMalformedTransate_string() throws Exception {
+    public void testMalformedTransate_string() {
         try {
             // The non-standard Linux access mode 3 should throw
             // an IllegalArgumentException.
@@ -719,8 +716,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Requires kernel support")
-    public void testTranslateMode_Invalid() throws Exception {
+    public void testTranslateMode_Invalid() {
         try {
             translateModeStringToPosix("rwx");
             fail();
@@ -734,8 +730,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Requires kernel support")
-    public void testTranslateMode_Access() throws Exception {
+    public void testTranslateMode_Access() {
         assertEquals(O_RDONLY, translateModeAccessToPosix(F_OK));
         assertEquals(O_RDONLY, translateModeAccessToPosix(R_OK));
         assertEquals(O_WRONLY, translateModeAccessToPosix(W_OK));
@@ -744,7 +739,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Requires kernel support")
+    @DisabledOnRavenwood(reason = "Requires kernel support")
     public void testConvertToModernFd() throws Exception {
         final String nonce = String.valueOf(System.nanoTime());
 
diff --git a/core/tests/coretests/src/com/android/internal/os/DebugStoreTest.java b/core/tests/coretests/src/com/android/internal/os/DebugStoreTest.java
new file mode 100644
index 0000000..786c2fc
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/DebugStoreTest.java
@@ -0,0 +1,311 @@
+/*
+ * 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.internal.os;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ServiceInfo;
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+/**
+ * Test class for {@link DebugStore}.
+ *
+ * To run it:
+ * atest FrameworksCoreTests:com.android.internal.os.DebugStoreTest
+ */
+@RunWith(AndroidJUnit4.class)
+@DisabledOnRavenwood(blockedBy = DebugStore.class)
+@SmallTest
+public class DebugStoreTest {
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Mock
+    private DebugStore.DebugStoreNative mDebugStoreNativeMock;
+
+    @Captor
+    private ArgumentCaptor<List<String>> mListCaptor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        DebugStore.setDebugStoreNative(mDebugStoreNativeMock);
+    }
+
+    @Test
+    public void testRecordServiceOnStart() {
+        Intent intent = new Intent();
+        intent.setAction("com.android.ACTION");
+        intent.setComponent(new ComponentName("com.android", "androidService"));
+        intent.setPackage("com.android");
+
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(1L);
+
+        long eventId = DebugStore.recordServiceOnStart(1, 0, intent);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("SvcStart"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "stId", "1",
+                "flg", "0",
+                "act", "com.android.ACTION",
+                "comp", "ComponentInfo{com.android/androidService}",
+                "pkg", "com.android"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(1L);
+    }
+
+    @Test
+    public void testRecordServiceCreate() {
+        ServiceInfo serviceInfo = new ServiceInfo();
+        serviceInfo.name = "androidService";
+        serviceInfo.packageName = "com.android";
+
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(2L);
+
+        long eventId = DebugStore.recordServiceCreate(serviceInfo);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("SvcCreate"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "name", "androidService",
+                "pkg", "com.android"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(2L);
+    }
+
+    @Test
+    public void testRecordServiceBind() {
+        Intent intent = new Intent();
+        intent.setAction("com.android.ACTION");
+        intent.setComponent(new ComponentName("com.android", "androidService"));
+        intent.setPackage("com.android");
+
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(3L);
+
+        long eventId = DebugStore.recordServiceBind(true, intent);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("SvcBind"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "rebind", "true",
+                "act", "com.android.ACTION",
+                "cmp", "ComponentInfo{com.android/androidService}",
+                "pkg", "com.android"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(3L);
+    }
+
+    @Test
+    public void testRecordGoAsync() {
+        DebugStore.recordGoAsync("androidReceiver");
+
+        verify(mDebugStoreNativeMock).recordEvent(eq("GoAsync"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "tname", Thread.currentThread().getName(),
+                "tid", String.valueOf(Thread.currentThread().getId()),
+                "rcv", "androidReceiver"
+        ).inOrder();
+    }
+
+    @Test
+    public void testRecordFinish() {
+        DebugStore.recordFinish("androidReceiver");
+
+        verify(mDebugStoreNativeMock).recordEvent(eq("Finish"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "tname", Thread.currentThread().getName(),
+                "tid", String.valueOf(Thread.currentThread().getId()),
+                "rcv", "androidReceiver"
+        ).inOrder();
+    }
+
+    @Test
+    public void testRecordLongLooperMessage() {
+        DebugStore.recordLongLooperMessage(100, "androidHandler", 500L);
+
+        verify(mDebugStoreNativeMock).recordEvent(eq("LooperMsg"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "code", "100",
+                "trgt", "androidHandler",
+                "elapsed", "500"
+        ).inOrder();
+    }
+
+    @Test
+    public void testRecordBroadcastHandleReceiver() {
+        Intent intent = new Intent();
+        intent.setAction("com.android.ACTION");
+        intent.setComponent(new ComponentName("com.android", "androidReceiver"));
+        intent.setPackage("com.android");
+
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(4L);
+
+        long eventId = DebugStore.recordBroadcastHandleReceiver(intent);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("HandleReceiver"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "tname", Thread.currentThread().getName(),
+                "tid", String.valueOf(Thread.currentThread().getId()),
+                "act", "com.android.ACTION",
+                "cmp", "ComponentInfo{com.android/androidReceiver}",
+                "pkg", "com.android"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(4L);
+    }
+
+    @Test
+    public void testRecordEventEnd() {
+        DebugStore.recordEventEnd(1L);
+
+        verify(mDebugStoreNativeMock).endEvent(eq(1L), anyList());
+    }
+
+    @Test
+    public void testRecordServiceOnStartWithNullIntent() {
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(5L);
+
+        long eventId = DebugStore.recordServiceOnStart(1, 0, null);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("SvcStart"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "stId", "1",
+                "flg", "0",
+                "act", "null",
+                "comp", "null",
+                "pkg", "null"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(5L);
+    }
+
+    @Test
+    public void testRecordServiceCreateWithNullServiceInfo() {
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(6L);
+
+        long eventId = DebugStore.recordServiceCreate(null);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("SvcCreate"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "name", "null",
+                "pkg", "null"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(6L);
+    }
+
+    @Test
+    public void testRecordServiceBindWithNullIntent() {
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(7L);
+
+        long eventId = DebugStore.recordServiceBind(false, null);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("SvcBind"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "rebind", "false",
+                "act", "null",
+                "cmp", "null",
+                "pkg", "null"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(7L);
+    }
+
+    @Test
+    public void testRecordBroadcastHandleReceiverWithNullIntent() {
+        when(mDebugStoreNativeMock.beginEvent(anyString(), anyList())).thenReturn(8L);
+
+        long eventId = DebugStore.recordBroadcastHandleReceiver(null);
+
+        verify(mDebugStoreNativeMock).beginEvent(eq("HandleReceiver"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "tname", Thread.currentThread().getName(),
+                "tid", String.valueOf(Thread.currentThread().getId()),
+                "act", "null",
+                "cmp", "null",
+                "pkg", "null"
+        ).inOrder();
+        assertThat(eventId).isEqualTo(8L);
+    }
+
+    @Test
+    public void testRecordGoAsyncWithNullReceiverClassName() {
+        DebugStore.recordGoAsync(null);
+
+        verify(mDebugStoreNativeMock).recordEvent(eq("GoAsync"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "tname", Thread.currentThread().getName(),
+                "tid", String.valueOf(Thread.currentThread().getId()),
+                "rcv", "null"
+        ).inOrder();
+    }
+
+    @Test
+    public void testRecordFinishWithNullReceiverClassName() {
+        DebugStore.recordFinish(null);
+
+        verify(mDebugStoreNativeMock).recordEvent(eq("Finish"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "tname", Thread.currentThread().getName(),
+                "tid", String.valueOf(Thread.currentThread().getId()),
+                "rcv", "null"
+        ).inOrder();
+    }
+
+    @Test
+    public void testRecordLongLooperMessageWithNullTargetClass() {
+        DebugStore.recordLongLooperMessage(200, null, 1000L);
+
+        verify(mDebugStoreNativeMock).recordEvent(eq("LooperMsg"), mListCaptor.capture());
+        List<String> capturedList = mListCaptor.getValue();
+        assertThat(capturedList).containsExactly(
+                "code", "200",
+                "trgt", "null",
+                "elapsed", "1000"
+        ).inOrder();
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
index b2bc3de..37f0067 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
@@ -18,8 +18,8 @@
 
 import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
 
-import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
-import static androidx.window.common.CommonFoldingFeature.parseListFromString;
+import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
+import static androidx.window.common.layout.CommonFoldingFeature.parseListFromString;
 
 import android.annotation.NonNull;
 import android.content.Context;
@@ -31,6 +31,9 @@
 import android.util.Log;
 import android.util.SparseIntArray;
 
+import androidx.window.common.layout.CommonFoldingFeature;
+import androidx.window.common.layout.DisplayFoldFeatureCommon;
+
 import com.android.internal.R;
 
 import java.util.ArrayList;
@@ -200,6 +203,23 @@
 
 
     /**
+     * Returns the list of supported {@link DisplayFoldFeatureCommon} calculated from the
+     * {@link DeviceStateManagerFoldingFeatureProducer}.
+     */
+    @NonNull
+    public List<DisplayFoldFeatureCommon> getDisplayFeatures() {
+        final List<DisplayFoldFeatureCommon> foldFeatures = new ArrayList<>();
+        final List<CommonFoldingFeature> folds = getFoldsWithUnknownState();
+
+        final boolean isHalfOpenedSupported = isHalfOpenedSupported();
+        for (CommonFoldingFeature fold : folds) {
+            foldFeatures.add(DisplayFoldFeatureCommon.create(fold, isHalfOpenedSupported));
+        }
+        return foldFeatures;
+    }
+
+
+    /**
      * Returns {@code true} if the device supports half-opened mode, {@code false} otherwise.
      */
     public boolean isHalfOpenedSupported() {
@@ -211,7 +231,7 @@
      * @param storeFeaturesConsumer a consumer to collect the data when it is first available.
      */
     @Override
-    public void getData(Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer) {
+    public void getData(@NonNull Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer) {
         mRawFoldSupplier.getData((String displayFeaturesString) -> {
             if (TextUtils.isEmpty(displayFeaturesString)) {
                 storeFeaturesConsumer.accept(new ArrayList<>());
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
index 6d758f1..9651918 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
@@ -26,6 +26,8 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 
+import androidx.window.common.layout.CommonFoldingFeature;
+
 import com.android.internal.R;
 
 import java.util.Optional;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/collections/ListUtil.java b/libs/WindowManager/Jetpack/src/androidx/window/common/collections/ListUtil.java
new file mode 100644
index 0000000..e72459f
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/collections/ListUtil.java
@@ -0,0 +1,41 @@
+/*
+ * 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 androidx.window.common.collections;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * A class to contain utility methods for {@link List}.
+ */
+public final class ListUtil {
+
+    private ListUtil() {}
+
+    /**
+     * Returns a new {@link List} that is created by applying the {@code transformer} to the
+     * {@code source} list.
+     */
+    public static <T, U> List<U> map(List<T> source, Function<T, U> transformer) {
+        final List<U> target = new ArrayList<>();
+        for (int i = 0; i < source.size(); i++) {
+            target.add(transformer.apply(source.get(i)));
+        }
+        return target;
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/common/layout/CommonFoldingFeature.java
similarity index 99%
rename from libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
rename to libs/WindowManager/Jetpack/src/androidx/window/common/layout/CommonFoldingFeature.java
index b95bca1..85c4fe1 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/layout/CommonFoldingFeature.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.common;
+package androidx.window.common.layout;
 
 import static androidx.window.common.ExtensionHelper.isZero;
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/layout/DisplayFoldFeatureCommon.java b/libs/WindowManager/Jetpack/src/androidx/window/common/layout/DisplayFoldFeatureCommon.java
new file mode 100644
index 0000000..594bd9c
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/layout/DisplayFoldFeatureCommon.java
@@ -0,0 +1,171 @@
+/*
+ * 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 androidx.window.common.layout;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.util.ArraySet;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A class that represents if a fold is part of the device.
+ */
+public final class DisplayFoldFeatureCommon {
+
+    /**
+     * Returns a new instance of {@link DisplayFoldFeatureCommon} based off of
+     * {@link CommonFoldingFeature} and whether or not half opened is supported.
+     */
+    public static DisplayFoldFeatureCommon create(CommonFoldingFeature foldingFeature,
+            boolean isHalfOpenedSupported) {
+        @FoldType
+        final int foldType;
+        if (foldingFeature.getType() == CommonFoldingFeature.COMMON_TYPE_HINGE) {
+            foldType = DISPLAY_FOLD_FEATURE_TYPE_HINGE;
+        } else {
+            foldType = DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN;
+        }
+
+        final Set<Integer> properties = new ArraySet<>();
+
+        if (isHalfOpenedSupported) {
+            properties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED);
+        }
+        return new DisplayFoldFeatureCommon(foldType, properties);
+    }
+
+    /**
+     * The type of fold is unknown. This is here for compatibility reasons if a new type is added,
+     * and cannot be reported to an incompatible application.
+     */
+    public static final int DISPLAY_FOLD_FEATURE_TYPE_UNKNOWN = 0;
+
+    /**
+     * The type of fold is a physical hinge separating two display panels.
+     */
+    public static final int DISPLAY_FOLD_FEATURE_TYPE_HINGE = 1;
+
+    /**
+     * The type of fold is a screen that folds from 0-180.
+     */
+    public static final int DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN = 2;
+
+    /**
+     * @hide
+     */
+    @IntDef(value = {DISPLAY_FOLD_FEATURE_TYPE_UNKNOWN, DISPLAY_FOLD_FEATURE_TYPE_HINGE,
+            DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN})
+    public @interface FoldType {
+    }
+
+    /**
+     * The fold supports the half opened state.
+     */
+    public static final int DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED = 1;
+
+    @IntDef(value = {DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED})
+    public @interface FoldProperty {
+    }
+
+    @FoldType
+    private final int mType;
+
+    private final Set<Integer> mProperties;
+
+    /**
+     * Creates an instance of [FoldDisplayFeature].
+     *
+     * @param type                  the type of fold, either [FoldDisplayFeature.TYPE_HINGE] or
+     *                              [FoldDisplayFeature.TYPE_FOLDABLE_SCREEN]
+     * @hide
+     */
+    public DisplayFoldFeatureCommon(@FoldType int type, @NonNull Set<Integer> properties) {
+        mType = type;
+        mProperties = new ArraySet<>();
+        assertPropertiesAreValid(properties);
+        mProperties.addAll(properties);
+    }
+
+    /**
+     * Returns the type of fold that is either a hinge or a fold.
+     */
+    @FoldType
+    public int getType() {
+        return mType;
+    }
+
+    /**
+     * Returns {@code true} if the fold has the given property, {@code false} otherwise.
+     */
+    public boolean hasProperty(@FoldProperty int property) {
+        return mProperties.contains(property);
+    }
+    /**
+     * Returns {@code true} if the fold has all the given properties, {@code false} otherwise.
+     */
+    public boolean hasProperties(@NonNull @FoldProperty int... properties) {
+        for (int i = 0; i < properties.length; i++) {
+            if (!mProperties.contains(properties[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns a copy of the set of properties.
+     * @hide
+     */
+    public Set<Integer> getProperties() {
+        return new ArraySet<>(mProperties);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        DisplayFoldFeatureCommon that = (DisplayFoldFeatureCommon) o;
+        return mType == that.mType && Objects.equals(mProperties, that.mProperties);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mType, mProperties);
+    }
+
+    @Override
+    public String toString() {
+        return "DisplayFoldFeatureCommon{mType=" + mType + ", mProperties=" + mProperties + '}';
+    }
+
+    private static void assertPropertiesAreValid(@NonNull Set<Integer> properties) {
+        for (int property : properties) {
+            if (!isProperty(property)) {
+                throw new IllegalArgumentException("Property is not a valid type: " + property);
+            }
+        }
+    }
+
+    private static boolean isProperty(int property) {
+        if (property == DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED) {
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index e555176..7be14724 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -89,9 +89,9 @@
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.window.common.CommonFoldingFeature;
 import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
 import androidx.window.common.EmptyLifecycleCallbacksAdapter;
+import androidx.window.common.layout.CommonFoldingFeature;
 import androidx.window.extensions.WindowExtensions;
 import androidx.window.extensions.core.util.function.Consumer;
 import androidx.window.extensions.core.util.function.Function;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/DisplayFoldFeatureUtil.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/DisplayFoldFeatureUtil.java
index a0f481a..870c92e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/DisplayFoldFeatureUtil.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/DisplayFoldFeatureUtil.java
@@ -16,48 +16,24 @@
 
 package androidx.window.extensions.layout;
 
-import androidx.window.common.CommonFoldingFeature;
-import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
-
-import java.util.ArrayList;
-import java.util.List;
+import androidx.window.common.layout.DisplayFoldFeatureCommon;
 
 /**
  * Util functions for working with {@link androidx.window.extensions.layout.DisplayFoldFeature}.
  */
-public class DisplayFoldFeatureUtil {
+public final class DisplayFoldFeatureUtil {
 
     private DisplayFoldFeatureUtil() {}
 
-    private static DisplayFoldFeature create(CommonFoldingFeature foldingFeature,
-            boolean isHalfOpenedSupported) {
-        final int foldType;
-        if (foldingFeature.getType() == CommonFoldingFeature.COMMON_TYPE_HINGE) {
-            foldType = DisplayFoldFeature.TYPE_HINGE;
-        } else {
-            foldType = DisplayFoldFeature.TYPE_SCREEN_FOLD_IN;
-        }
-        DisplayFoldFeature.Builder featureBuilder = new DisplayFoldFeature.Builder(foldType);
-
-        if (isHalfOpenedSupported) {
-            featureBuilder.addProperty(DisplayFoldFeature.FOLD_PROPERTY_SUPPORTS_HALF_OPENED);
-        }
-        return featureBuilder.build();
-    }
-
     /**
-     * Returns the list of supported {@link DisplayFeature} calculated from the
-     * {@link DeviceStateManagerFoldingFeatureProducer}.
+     * Returns a {@link DisplayFoldFeature} that matches the given {@link DisplayFoldFeatureCommon}.
      */
-    public static List<DisplayFoldFeature> extractDisplayFoldFeatures(
-            DeviceStateManagerFoldingFeatureProducer producer) {
-        List<DisplayFoldFeature> foldFeatures = new ArrayList<>();
-        List<CommonFoldingFeature> folds = producer.getFoldsWithUnknownState();
-
-        final boolean isHalfOpenedSupported = producer.isHalfOpenedSupported();
-        for (CommonFoldingFeature fold : folds) {
-            foldFeatures.add(DisplayFoldFeatureUtil.create(fold, isHalfOpenedSupported));
+    public static DisplayFoldFeature translate(DisplayFoldFeatureCommon foldFeatureCommon) {
+        final DisplayFoldFeature.Builder builder =
+                new DisplayFoldFeature.Builder(foldFeatureCommon.getType());
+        for (int property: foldFeatureCommon.getProperties()) {
+            builder.addProperty(property);
         }
-        return foldFeatures;
+        return builder.build();
     }
 }
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 a3ef68a..f1ea19a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -19,11 +19,11 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 
-import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT;
-import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
 import static androidx.window.common.ExtensionHelper.isZero;
 import static androidx.window.common.ExtensionHelper.rotateRectToDisplayRotation;
 import static androidx.window.common.ExtensionHelper.transformToWindowSpaceRect;
+import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_FLAT;
+import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
 
 import android.app.Activity;
 import android.app.ActivityThread;
@@ -45,9 +45,10 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.UiContext;
 import androidx.annotation.VisibleForTesting;
-import androidx.window.common.CommonFoldingFeature;
 import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
 import androidx.window.common.EmptyLifecycleCallbacksAdapter;
+import androidx.window.common.collections.ListUtil;
+import androidx.window.common.layout.CommonFoldingFeature;
 import androidx.window.extensions.core.util.function.Consumer;
 import androidx.window.extensions.util.DeduplicateConsumer;
 
@@ -95,8 +96,8 @@
                 .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged());
         mFoldingFeatureProducer = foldingFeatureProducer;
         mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged);
-        final List<DisplayFoldFeature> displayFoldFeatures =
-                DisplayFoldFeatureUtil.extractDisplayFoldFeatures(mFoldingFeatureProducer);
+        final List<DisplayFoldFeature> displayFoldFeatures = ListUtil.map(
+                mFoldingFeatureProducer.getDisplayFeatures(), DisplayFoldFeatureUtil::translate);
         mSupportedWindowFeatures = new SupportedWindowFeatures.Builder(displayFoldFeatures).build();
     }
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
index b63fd08..60bc7be 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
@@ -26,10 +26,10 @@
 
 import androidx.annotation.NonNull;
 import androidx.window.common.BaseDataProducer;
-import androidx.window.common.CommonFoldingFeature;
 import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
 import androidx.window.common.EmptyLifecycleCallbacksAdapter;
 import androidx.window.common.RawFoldingFeatureProducer;
+import androidx.window.common.layout.CommonFoldingFeature;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
index 4fd03e4..6e0e711 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
@@ -26,7 +26,7 @@
 import android.graphics.Rect;
 import android.os.IBinder;
 
-import androidx.window.common.CommonFoldingFeature;
+import androidx.window.common.layout.CommonFoldingFeature;
 
 import java.util.ArrayList;
 import java.util.Collections;
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/collections/ListUtilTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/collections/ListUtilTest.java
new file mode 100644
index 0000000..a077bdf
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/collections/ListUtilTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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 androidx.window.common.collections;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test class for {@link ListUtil}.
+ *
+ * Build/Install/Run:
+ *  atest WMJetpackUnitTests:ListUtil
+ */
+public class ListUtilTest {
+
+    @Test
+    public void test_map_empty_returns_empty() {
+        final List<String> emptyList = new ArrayList<>();
+        final List<Integer> result = ListUtil.map(emptyList, String::length);
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    public void test_map_maintains_order() {
+        final List<String> source = new ArrayList<>();
+        source.add("a");
+        source.add("aa");
+
+        final List<Integer> result = ListUtil.map(source, String::length);
+
+        assertThat(result).containsExactly(1, 2).inOrder();
+    }
+}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/layout/DisplayFoldFeatureCommonTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/layout/DisplayFoldFeatureCommonTest.java
new file mode 100644
index 0000000..6c17851
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/layout/DisplayFoldFeatureCommonTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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 androidx.window.common.layout;
+
+import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
+import static androidx.window.common.layout.CommonFoldingFeature.COMMON_TYPE_FOLD;
+import static androidx.window.common.layout.CommonFoldingFeature.COMMON_TYPE_HINGE;
+import static androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED;
+import static androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_HINGE;
+import static androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Rect;
+import android.util.ArraySet;
+
+import org.junit.Test;
+
+import java.util.Set;
+
+/**
+ * Test class for {@link DisplayFoldFeatureCommon}.
+ *
+ * Build/Install/Run:
+ *  atest WMJetpackUnitTests:DisplayFoldFeatureCommonTest
+ */
+public class DisplayFoldFeatureCommonTest {
+
+    @Test
+    public void test_different_type_not_equals() {
+        final Set<Integer> properties = new ArraySet<>();
+        final DisplayFoldFeatureCommon first =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties);
+        final DisplayFoldFeatureCommon second =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN, properties);
+
+        assertThat(first).isEqualTo(second);
+    }
+
+    @Test
+    public void test_different_property_set_not_equals() {
+        final Set<Integer> firstProperties = new ArraySet<>();
+        final Set<Integer> secondProperties = new ArraySet<>();
+        secondProperties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED);
+        final DisplayFoldFeatureCommon first =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, firstProperties);
+        final DisplayFoldFeatureCommon second =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, secondProperties);
+
+        assertThat(first).isNotEqualTo(second);
+    }
+
+    @Test
+    public void test_check_single_property_exists() {
+        final Set<Integer> properties = new ArraySet<>();
+        properties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED);
+        final DisplayFoldFeatureCommon foldFeatureCommon =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties);
+
+        assertThat(
+                foldFeatureCommon.hasProperty(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED))
+                .isTrue();
+    }
+
+    @Test
+    public void test_check_multiple_properties_exists() {
+        final Set<Integer> properties = new ArraySet<>();
+        properties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED);
+        final DisplayFoldFeatureCommon foldFeatureCommon =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties);
+
+        assertThat(foldFeatureCommon.hasProperties(
+                DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED))
+                .isTrue();
+    }
+
+    @Test
+    public void test_properties_matches_getter() {
+        final Set<Integer> properties = new ArraySet<>();
+        properties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED);
+        final DisplayFoldFeatureCommon foldFeatureCommon =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties);
+
+        assertThat(foldFeatureCommon.getProperties()).isEqualTo(properties);
+    }
+
+    @Test
+    public void test_type_matches_getter() {
+        final Set<Integer> properties = new ArraySet<>();
+        final DisplayFoldFeatureCommon foldFeatureCommon =
+                new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties);
+
+        assertThat(foldFeatureCommon.getType()).isEqualTo(DISPLAY_FOLD_FEATURE_TYPE_HINGE);
+    }
+
+    @Test
+    public void test_create_half_opened_feature() {
+        final CommonFoldingFeature foldingFeature =
+                new CommonFoldingFeature(COMMON_TYPE_HINGE, COMMON_STATE_UNKNOWN, new Rect());
+        final DisplayFoldFeatureCommon foldFeatureCommon = DisplayFoldFeatureCommon.create(
+                foldingFeature, true);
+
+        assertThat(foldFeatureCommon.getType()).isEqualTo(DISPLAY_FOLD_FEATURE_TYPE_HINGE);
+        assertThat(
+                foldFeatureCommon.hasProperty(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED))
+                .isTrue();
+    }
+
+    @Test
+    public void test_create_fold_feature_no_half_opened() {
+        final CommonFoldingFeature foldingFeature =
+                new CommonFoldingFeature(COMMON_TYPE_FOLD, COMMON_STATE_UNKNOWN, new Rect());
+        final DisplayFoldFeatureCommon foldFeatureCommon = DisplayFoldFeatureCommon.create(
+                foldingFeature, true);
+
+        assertThat(foldFeatureCommon.getType()).isEqualTo(DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN);
+        assertThat(
+                foldFeatureCommon.hasProperty(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED))
+                .isTrue();
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 81942e8..d68c018 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -89,6 +89,7 @@
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
+import com.android.wm.shell.common.DisplayChangeController;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayLayout;
@@ -175,6 +176,7 @@
     private boolean mInImmersiveMode;
     private final String mSysUIPackageName;
 
+    private final DisplayChangeController.OnDisplayChangingListener mOnDisplayChangingListener;
     private final ISystemGestureExclusionListener mGestureExclusionListener =
             new ISystemGestureExclusionListener.Stub() {
                 @Override
@@ -287,6 +289,31 @@
         mSysUIPackageName = mContext.getResources().getString(
                 com.android.internal.R.string.config_systemUi);
         mInteractionJankMonitor = interactionJankMonitor;
+        mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> {
+            DesktopModeWindowDecoration decoration;
+            RunningTaskInfo taskInfo;
+            for (int i = 0; i < mWindowDecorByTaskId.size(); i++) {
+                decoration = mWindowDecorByTaskId.valueAt(i);
+                if (decoration == null) {
+                    continue;
+                } else {
+                    taskInfo = decoration.mTaskInfo;
+                }
+
+                // Check if display has been rotated between portrait & landscape
+                if (displayId == taskInfo.displayId && taskInfo.isFreeform()
+                        && (fromRotation % 2 != toRotation % 2)) {
+                    // Check if the task bounds on the rotated display will be out of bounds.
+                    // If so, then update task bounds to be within reachable area.
+                    final Rect taskBounds = new Rect(
+                            taskInfo.configuration.windowConfiguration.getBounds());
+                    if (DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(
+                            taskBounds, decoration.calculateValidDragArea())) {
+                        t.setBounds(taskInfo.token, taskBounds);
+                    }
+                }
+            }
+        };
 
         shellInit.addInitCallback(this::onInit, this);
     }
@@ -298,6 +325,7 @@
                 new DesktopModeOnInsetsChangedListener());
         mDesktopTasksController.setOnTaskResizeAnimationListener(
                 new DesktopModeOnTaskResizeAnimationListener());
+        mDisplayController.addDisplayChangingController(mOnDisplayChangingListener);
         try {
             mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener,
                     mContext.getDisplayId());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 24fb971..d70e225 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -312,8 +312,7 @@
         // transaction (that applies task crop) is synced with the buffer transaction (that draws
         // the View). Both will be shown on screen at the same, whereas applying them independently
         // causes flickering. See b/270202228.
-        final boolean applyTransactionOnDraw =
-                taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM;
+        final boolean applyTransactionOnDraw = taskInfo.isFreeform();
         relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskPositionAndCrop);
         if (!applyTransactionOnDraw) {
             t.apply();
@@ -324,7 +323,7 @@
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
         Trace.beginSection("DesktopModeWindowDecoration#relayout");
-        if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+        if (taskInfo.isFreeform()) {
             // The Task is in Freeform mode -> show its header in sync since it's an integral part
             // of the window itself - a delayed header might cause bad UX.
             relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw,
@@ -524,9 +523,7 @@
     }
 
     private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
-        final boolean isFreeform =
-                taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM;
-        return isFreeform && taskInfo.isResizeable;
+        return taskInfo.isFreeform() && taskInfo.isResizeable;
     }
 
     private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/CloseAllAppsWithAppHeaderExitTest.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/CloseAllAppsWithAppHeaderExitTest.kt
new file mode 100644
index 0000000..9ba3a45
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/CloseAllAppsWithAppHeaderExitTest.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/** Functional test for [CloseAllAppsWithAppHeaderExit]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class CloseAllAppsWithAppHeaderExitTest : CloseAllAppsWithAppHeaderExit() {
+
+    @Test
+    override fun closeAllAppsInDesktop() {
+        super.closeAllAppsInDesktop()
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/DragAppWindowMultiWindowTest.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/DragAppWindowMultiWindowTest.kt
new file mode 100644
index 0000000..ed1d488
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/DragAppWindowMultiWindowTest.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.DragAppWindowMultiWindow
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/** Functional test for [DragAppWindowMultiWindow]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class DragAppWindowMultiWindowTest : DragAppWindowMultiWindow()
+{
+    @Test
+    override fun dragAppWindow() {
+        super.dragAppWindow()
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/DragAppWindowSingleWindowTest.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/DragAppWindowSingleWindowTest.kt
new file mode 100644
index 0000000..d8b9348
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/DragAppWindowSingleWindowTest.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.DragAppWindowSingleWindow
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/** Functional test for [DragAppWindowSingleWindow]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class DragAppWindowSingleWindowTest : DragAppWindowSingleWindow()
+{
+    @Test
+    override fun dragAppWindow() {
+        super.dragAppWindow()
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/EnterDesktopWithAppHandleMenuTest.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/EnterDesktopWithAppHandleMenuTest.kt
new file mode 100644
index 0000000..546ce2d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/EnterDesktopWithAppHandleMenuTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithAppHandleMenu
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/** Functional test for [EnterDesktopWithAppHandleMenu]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class EnterDesktopWithAppHandleMenuTest : EnterDesktopWithAppHandleMenu() {
+    @Test
+    override fun enterDesktopWithAppHandleMenu() {
+        super.enterDesktopWithAppHandleMenu()
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/EnterDesktopWithDragTest.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/EnterDesktopWithDragTest.kt
new file mode 100644
index 0000000..b5fdb16
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/EnterDesktopWithDragTest.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.functional
+
+import android.platform.test.annotations.Postsubmit
+import android.tools.Rotation
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/** Functional test for [EnterDesktopWithDrag]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class EnterDesktopWithDragTest : EnterDesktopWithDrag(Rotation.ROTATION_0) {
+
+    @Test
+    override fun enterDesktopWithDrag() {
+        super.enterDesktopWithDrag()
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/ExitDesktopWithDragToTopDragZoneTest.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/ExitDesktopWithDragToTopDragZoneTest.kt
new file mode 100644
index 0000000..8f802d2
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/ExitDesktopWithDragToTopDragZoneTest.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.functional
+
+import android.platform.test.annotations.Postsubmit
+import android.tools.Rotation
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.ExitDesktopWithDragToTopDragZone
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/** Functional test for [ExitDesktopWithDragToTopDragZone]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class ExitDesktopWithDragToTopDragZoneTest :
+    ExitDesktopWithDragToTopDragZone(Rotation.ROTATION_0) {
+    @Test
+    override fun exitDesktopWithDragToTopDragZone() {
+        super.exitDesktopWithDragToTopDragZone()
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/MaximizeAppWindowTest.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/MaximizeAppWindowTest.kt
new file mode 100644
index 0000000..f899082
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/MaximizeAppWindowTest.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.MaximizeAppWindow
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/** Functional test for [MaximizeAppWindow]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class MaximizeAppWindowTest : MaximizeAppWindow()
+{
+    @Test
+    override fun maximizeAppWindow() {
+        super.maximizeAppWindow()
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/MinimizeWindowOnAppOpenTest.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/MinimizeWindowOnAppOpenTest.kt
new file mode 100644
index 0000000..63c428a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/MinimizeWindowOnAppOpenTest.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.MinimizeWindowOnAppOpen
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/** Functional test for [MinimizeWindowOnAppOpen]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class MinimizeWindowOnAppOpenTest : MinimizeWindowOnAppOpen()
+{
+    @Test
+    override fun openAppToMinimizeWindow() {
+        // Launch a new app while 4 apps are already open on desktop. This should result in the
+        // first app we opened to be minimized.
+        super.openAppToMinimizeWindow()
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/ResizeAppWithCornerResizeTest.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/ResizeAppWithCornerResizeTest.kt
new file mode 100644
index 0000000..4797aaf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/ResizeAppWithCornerResizeTest.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.functional
+
+import android.platform.test.annotations.Postsubmit
+import android.tools.Rotation
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.ResizeAppWithCornerResize
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/** Functional test for [ResizeAppWithCornerResize]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class ResizeAppWithCornerResizeTest : ResizeAppWithCornerResize(Rotation.ROTATION_0) {
+    @Test
+    override fun resizeAppWithCornerResize() {
+        super.resizeAppWithCornerResize()
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/SwitchToOverviewFromDesktopTest.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/SwitchToOverviewFromDesktopTest.kt
new file mode 100644
index 0000000..9a71361
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/functional/SwitchToOverviewFromDesktopTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.SwitchToOverviewFromDesktop
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/** Functional test for [SwitchToOverviewFromDesktop]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+open class SwitchToOverviewFromDesktopTest : SwitchToOverviewFromDesktop() {
+    @Test
+    override fun switchToOverview() {
+        super.switchToOverview()
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 61c7080..bbf42b5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -21,6 +21,7 @@
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
 import android.app.WindowConfiguration.WindowingMode
 import android.content.ComponentName
@@ -51,11 +52,13 @@
 import android.view.InsetsSource
 import android.view.InsetsState
 import android.view.KeyEvent
+import android.view.Surface
 import android.view.SurfaceControl
 import android.view.SurfaceView
 import android.view.View
 import android.view.WindowInsets.Type.navigationBars
 import android.view.WindowInsets.Type.statusBars
+import android.window.WindowContainerTransaction
 import androidx.test.filters.SmallTest
 import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
@@ -69,6 +72,7 @@
 import com.android.wm.shell.TestRunningTaskInfoBuilder
 import com.android.wm.shell.TestShellExecutor
 import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser
+import com.android.wm.shell.common.DisplayChangeController
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.DisplayInsetsController
 import com.android.wm.shell.common.DisplayLayout
@@ -110,6 +114,7 @@
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.doNothing
 import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
@@ -166,6 +171,7 @@
     private lateinit var mockitoSession: StaticMockitoSession
     private lateinit var shellInit: ShellInit
     private lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener
+    private lateinit var displayChangingListener: DisplayChangeController.OnDisplayChangingListener
     private lateinit var desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel
 
     @Before
@@ -174,6 +180,7 @@
             mockitoSession()
                 .strictness(Strictness.LENIENT)
                 .spyStatic(DesktopModeStatus::class.java)
+                .spyStatic(DragPositioningCallbackUtility::class.java)
                 .startMocking()
         doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(Mockito.any()) }
 
@@ -218,10 +225,17 @@
 
         shellInit.init()
 
-        val listenerCaptor =
-                argumentCaptor<DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener>()
-        verify(displayInsetsController).addInsetsChangedListener(anyInt(), listenerCaptor.capture())
-        desktopModeOnInsetsChangedListener = listenerCaptor.firstValue
+        val insetListenerCaptor =
+            argumentCaptor<DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener>()
+        verify(displayInsetsController)
+            .addInsetsChangedListener(anyInt(), insetListenerCaptor.capture())
+        desktopModeOnInsetsChangedListener = insetListenerCaptor.firstValue
+
+        val displayChangingListenerCaptor =
+            argumentCaptor<DisplayChangeController.OnDisplayChangingListener>()
+        verify(mockDisplayController)
+            .addDisplayChangingController(displayChangingListenerCaptor.capture())
+        displayChangingListener = displayChangingListenerCaptor.firstValue
     }
 
     @After
@@ -786,6 +800,135 @@
         })
     }
 
+    @Test
+    fun testOnDisplayRotation_tasksOutOfValidArea_taskBoundsUpdated() {
+        val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+        val secondTask =
+            createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
+        val thirdTask =
+            createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
+
+        doReturn(true).`when` {
+            DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any())
+        }
+        setUpMockDecorationsForTasks(task, secondTask, thirdTask)
+
+        onTaskOpening(task)
+        onTaskOpening(secondTask)
+        onTaskOpening(thirdTask)
+
+        val wct = mock<WindowContainerTransaction>()
+
+        displayChangingListener.onDisplayChange(
+            task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct
+        )
+
+        verify(wct).setBounds(eq(task.token), any())
+        verify(wct).setBounds(eq(secondTask.token), any())
+        verify(wct).setBounds(eq(thirdTask.token), any())
+    }
+
+    @Test
+    fun testOnDisplayRotation_taskInValidArea_taskBoundsNotUpdated() {
+        val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+        val secondTask =
+            createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
+        val thirdTask =
+            createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
+
+        doReturn(false).`when` {
+            DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any())
+        }
+        setUpMockDecorationsForTasks(task, secondTask, thirdTask)
+
+        onTaskOpening(task)
+        onTaskOpening(secondTask)
+        onTaskOpening(thirdTask)
+
+        val wct = mock<WindowContainerTransaction>()
+        displayChangingListener.onDisplayChange(
+            task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct
+        )
+
+        verify(wct, never()).setBounds(eq(task.token), any())
+        verify(wct, never()).setBounds(eq(secondTask.token), any())
+        verify(wct, never()).setBounds(eq(thirdTask.token), any())
+    }
+
+    @Test
+    fun testOnDisplayRotation_sameOrientationRotation_taskBoundsNotUpdated() {
+        val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+        val secondTask =
+            createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
+        val thirdTask =
+            createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
+
+        setUpMockDecorationsForTasks(task, secondTask, thirdTask)
+
+        onTaskOpening(task)
+        onTaskOpening(secondTask)
+        onTaskOpening(thirdTask)
+
+        val wct = mock<WindowContainerTransaction>()
+        displayChangingListener.onDisplayChange(
+            task.displayId, Surface.ROTATION_0, Surface.ROTATION_180, null, wct
+        )
+
+        verify(wct, never()).setBounds(eq(task.token), any())
+        verify(wct, never()).setBounds(eq(secondTask.token), any())
+        verify(wct, never()).setBounds(eq(thirdTask.token), any())
+    }
+
+    @Test
+    fun testOnDisplayRotation_differentDisplayId_taskBoundsNotUpdated() {
+        val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+        val secondTask = createTask(displayId = -2, windowingMode = WINDOWING_MODE_FREEFORM)
+        val thirdTask = createTask(displayId = -3, windowingMode = WINDOWING_MODE_FREEFORM)
+
+        doReturn(true).`when` {
+            DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any())
+        }
+        setUpMockDecorationsForTasks(task, secondTask, thirdTask)
+
+        onTaskOpening(task)
+        onTaskOpening(secondTask)
+        onTaskOpening(thirdTask)
+
+        val wct = mock<WindowContainerTransaction>()
+        displayChangingListener.onDisplayChange(
+            task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct
+        )
+
+        verify(wct).setBounds(eq(task.token), any())
+        verify(wct, never()).setBounds(eq(secondTask.token), any())
+        verify(wct, never()).setBounds(eq(thirdTask.token), any())
+    }
+
+    @Test
+    fun testOnDisplayRotation_nonFreeformTask_taskBoundsNotUpdated() {
+        val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+        val secondTask = createTask(displayId = -2, windowingMode = WINDOWING_MODE_FULLSCREEN)
+        val thirdTask = createTask(displayId = -3, windowingMode = WINDOWING_MODE_PINNED)
+
+        doReturn(true).`when` {
+            DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any())
+        }
+        setUpMockDecorationsForTasks(task, secondTask, thirdTask)
+
+        onTaskOpening(task)
+        onTaskOpening(secondTask)
+        onTaskOpening(thirdTask)
+
+        val wct = mock<WindowContainerTransaction>()
+        displayChangingListener.onDisplayChange(
+            task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct
+        )
+
+        verify(wct).setBounds(eq(task.token), any())
+        verify(wct, never()).setBounds(eq(secondTask.token), any())
+        verify(wct, never()).setBounds(eq(thirdTask.token), any())
+    }
+
     private fun createOpenTaskDecoration(
         @WindowingMode windowingMode: Int,
         onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> =
@@ -864,6 +1007,7 @@
             whenever(mockSplitScreenController.isTaskInSplitScreen(task.taskId))
                 .thenReturn(true)
         }
+        whenever(decoration.calculateValidDragArea()).thenReturn(Rect(0, 60, 2560, 1600))
         return decoration
     }
 
diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig
index 97971e1..3196ba1 100644
--- a/media/java/android/media/tv/flags/media_tv.aconfig
+++ b/media/java/android/media/tv/flags/media_tv.aconfig
@@ -23,4 +23,12 @@
     namespace: "media_tv"
     description: "TIAF V3.0 APIs for Android V"
     bug: "303323657"
+}
+
+flag {
+    name: "tis_always_bound_permission"
+    is_exported: true
+    namespace: "media_tv"
+    description: "Introduce ALWAYS_BOUND_TV_INPUT for TIS."
+    bug: "332201346"
 }
\ No newline at end of file
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index e8ef620..ba59ce8 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -3264,6 +3264,24 @@
 
             if (forceNotify || success) {
                 notifyForSettingsChange(key, name);
+
+                // If this is an aconfig flag, it will be written as a staged flag.
+                // Notify that its staged flag value will be updated.
+                if (Flags.notifyIndividualAconfigSyspropChanged() && type == SETTINGS_TYPE_CONFIG) {
+                    int slashIndex = name.indexOf('/');
+                    boolean validSlashIndex = slashIndex != -1
+                            && slashIndex != 0
+                            && slashIndex != name.length();
+                    if (validSlashIndex) {
+                        String namespace = name.substring(0, slashIndex);
+                        String flagName = name.substring(slashIndex + 1);
+                        if (settingsState.getAconfigDefaultFlags().containsKey(flagName)) {
+                            String stagedName = "staged/" + namespace + "*" + flagName;
+                            notifyForSettingsChange(key, stagedName);
+                        }
+                    }
+                }
+
                 if (wasUnsetNonPredefinedSetting) {
                     // Increment the generation number for all non-predefined, unset settings,
                     // because a new non-predefined setting has been inserted
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
index 4f5955b..f53dec6 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
+++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
@@ -52,3 +52,14 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "notify_individual_aconfig_sysprop_changed"
+    namespace: "core_experiments_team_internal"
+    description: "When enabled, propagate individual aconfig sys props on flag stage."
+    bug: "331963764"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 49098f3..201aaed 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -589,16 +589,6 @@
 }
 
 flag {
-    name: "screenshot_private_profile_accessibility_announcement_fix"
-    namespace: "systemui"
-    description: "Modified a11y announcement for private space screenshots"
-    bug: "326941376"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
     name: "screenshot_private_profile_behavior_fix"
     namespace: "systemui"
     description: "Private profile support for screenshots"
@@ -1308,3 +1298,10 @@
         purpose: PURPOSE_BUGFIX
    }
 }
+
+flag {
+   name: "compose_haptic_sliders"
+   namespace: "systemui"
+   description: "Adding haptic component infrastructure to sliders in Compose."
+   bug: "341968766"
+}
\ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 7a41bc6..1255248 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -99,7 +99,9 @@
 import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
 import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
 import org.mockito.kotlin.whenever
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
@@ -741,6 +743,18 @@
         }
 
     @Test
+    fun communalContent_readTriggersUmoVisibilityUpdate() =
+        testScope.runTest {
+            verify(mediaHost, never()).updateViewVisibility()
+
+            val communalContent by collectLastValue(underTest.communalContent)
+
+            // updateViewVisibility is called when the flow is collected.
+            assertThat(communalContent).isNotNull()
+            verify(mediaHost).updateViewVisibility()
+        }
+
+    @Test
     fun scrollPosition_persistedOnEditEntry() {
         val index = 2
         val offset = 30
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
index 86c680a..023de52 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
@@ -82,7 +82,7 @@
                 assertFalse(launching!!)
 
                 val parent = FrameLayout(context)
-                val view = CommunalAppWidgetHostView(context)
+                val view = CommunalAppWidgetHostView(context, underTest)
                 parent.addView(view)
                 val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImplTest.kt
index aef9163..b917014 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImplTest.kt
@@ -124,4 +124,36 @@
             underTest.setIsLaunchingActivity(true)
             Truth.assertThat(underTest.isLaunchingActivity.value).isEqualTo(true)
         }
+
+    @Test
+    fun isAnyFlingAnimationRunning() =
+        testScope.runTest() {
+            val actual by collectLastValue(underTest.isAnyFlingAnimationRunning)
+
+            // WHEN transitioning from QS to Gone with user input ongoing
+            val userInputOngoing = MutableStateFlow(true)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Transition(
+                        fromScene = Scenes.QuickSettings,
+                        toScene = Scenes.Gone,
+                        currentScene = flowOf(Scenes.QuickSettings),
+                        progress = MutableStateFlow(.1f),
+                        isInitiatedByUserInput = true,
+                        isUserInputOngoing = userInputOngoing,
+                    )
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // THEN qs is not flinging
+            Truth.assertThat(actual).isFalse()
+
+            // WHEN user input ends
+            userInputOngoing.value = false
+            runCurrent()
+
+            // THEN qs is flinging
+            Truth.assertThat(actual).isTrue()
+        }
 }
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 0318458..fe49f3a 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1379,11 +1379,15 @@
 
     <!-- Casting that launched by SysUI (i.e. when there is no app name) -->
     <!-- System casting media projection permission dialog title. [CHAR LIMIT=100] -->
-    <string name="media_projection_entry_cast_permission_dialog_title">Start casting?</string>
+    <string name="media_projection_entry_cast_permission_dialog_title">Cast your screen?</string>
+    <!-- System casting media projection permission option for capturing just a single app [CHAR LIMIT=50] -->
+    <string name="media_projection_entry_cast_permission_dialog_option_text_single_app">Cast one app</string>
+    <!-- System casting media projection permission option for capturing the whole screen [CHAR LIMIT=50] -->
+    <string name="media_projection_entry_cast_permission_dialog_option_text_entire_screen">Cast entire screen</string>
     <!-- System casting media projection permission warning for capturing the whole screen when SysUI casting requests it. [CHAR LIMIT=350] -->
-    <string name="media_projection_entry_cast_permission_dialog_warning_entire_screen">When you’re casting, Android has access to anything visible on your screen or played on your device. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
+    <string name="media_projection_entry_cast_permission_dialog_warning_entire_screen">When you’re casting your entire screen, anything on your screen is visible. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
     <!-- System casting media projection permission warning for capturing a single app when SysUI casting requests it. [CHAR LIMIT=350] -->
-    <string name="media_projection_entry_cast_permission_dialog_warning_single_app">When you’re casting an app, Android has access to anything shown or played on that app. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
+    <string name="media_projection_entry_cast_permission_dialog_warning_single_app">When you’re casting an app, anything shown or played in that app is visible. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
     <!-- System casting media projection permission button to continue for SysUI casting. [CHAR LIMIT=60] -->
     <string name="media_projection_entry_cast_permission_dialog_continue">Start casting</string>
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
index ca03a00..da270c0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
@@ -39,7 +39,6 @@
 import com.android.systemui.biometrics.shared.model.AuthenticationReason.SettingsOperations
 import com.android.systemui.biometrics.shared.model.AuthenticationState
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
@@ -49,6 +48,7 @@
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.filterIsInstance
@@ -85,7 +85,7 @@
      *   onAcquired in [FingerprintManager.EnrollmentCallback] and [FaceManager.EnrollmentCallback]
      */
     private val authenticationState: Flow<AuthenticationState> =
-        conflatedCallbackFlow {
+        callbackFlow {
                 val updateAuthenticationState = { state: AuthenticationState ->
                     Log.d(TAG, "authenticationState updated: $state")
                     trySendWithFailureLogging(state, TAG, "Error sending AuthenticationState state")
@@ -169,7 +169,9 @@
                         }
                     }
 
-                updateAuthenticationState(AuthenticationState.Idle(AuthenticationReason.NotRunning))
+                updateAuthenticationState(
+                    AuthenticationState.Idle(requestReason = AuthenticationReason.NotRunning)
+                )
                 biometricManager?.registerAuthenticationStateListener(authenticationStateListener)
                 awaitClose {
                     biometricManager?.unregisterAuthenticationStateListener(
@@ -180,23 +182,32 @@
             .distinctUntilChanged()
             .shareIn(applicationScope, started = SharingStarted.Eagerly, replay = 1)
 
-    override val fingerprintAuthenticationReason: Flow<AuthenticationReason> =
+    private val fingerprintAuthenticationState: Flow<AuthenticationState> =
         authenticationState
             .filter {
-                it is AuthenticationState.Idle ||
-                    (it is AuthenticationState.Started &&
-                        it.biometricSourceType == BiometricSourceType.FINGERPRINT) ||
-                    (it is AuthenticationState.Stopped &&
-                        it.biometricSourceType == BiometricSourceType.FINGERPRINT)
+                it.biometricSourceType == null ||
+                    it.biometricSourceType == BiometricSourceType.FINGERPRINT
             }
+            .onEach { Log.d(TAG, "fingerprintAuthenticationState updated: $it") }
+
+    private val fingerprintRunningState: Flow<AuthenticationState> =
+        fingerprintAuthenticationState
+            .filter {
+                it is AuthenticationState.Idle ||
+                    it is AuthenticationState.Started ||
+                    it is AuthenticationState.Stopped
+            }
+            .onEach { Log.d(TAG, "fingerprintRunningState updated: $it") }
+
+    override val fingerprintAuthenticationReason: Flow<AuthenticationReason> =
+        fingerprintRunningState
             .map { it.requestReason }
             .onEach { Log.d(TAG, "fingerprintAuthenticationReason updated: $it") }
 
     override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> =
-        authenticationState
-            .filterIsInstance<AuthenticationState.Acquired>()
-            .filter { it.biometricSourceType == BiometricSourceType.FINGERPRINT }
-            .map { AcquiredFingerprintAuthenticationStatus(it.requestReason, it.acquiredInfo) }
+        fingerprintAuthenticationState.filterIsInstance<AuthenticationState.Acquired>().map {
+            AcquiredFingerprintAuthenticationStatus(it.requestReason, it.acquiredInfo)
+        }
 
     companion object {
         private const val TAG = "BiometricStatusRepositoryImpl"
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt
index 5ceae36..81ea6a9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/AuthenticationState.kt
@@ -27,6 +27,9 @@
  * authentication.
  */
 sealed interface AuthenticationState {
+    /** Indicates [BiometricSourceType] of authentication state update, null in idle auth state. */
+    val biometricSourceType: BiometricSourceType?
+
     /**
      * Indicates [AuthenticationReason] from [BiometricRequestConstants.RequestReason] for
      * requesting auth
@@ -43,7 +46,7 @@
      *   message.
      */
     data class Acquired(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         override val requestReason: AuthenticationReason,
         val acquiredInfo: Int
     ) : AuthenticationState
@@ -59,7 +62,7 @@
      * @param requestReason reason from [BiometricRequestConstants.RequestReason] for authentication
      */
     data class Error(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         val errString: String?,
         val errCode: Int,
         override val requestReason: AuthenticationReason,
@@ -73,7 +76,7 @@
      * @param userId The user id for the requested authentication
      */
     data class Failed(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         override val requestReason: AuthenticationReason,
         val userId: Int
     ) : AuthenticationState
@@ -87,7 +90,7 @@
      * @param requestReason reason from [BiometricRequestConstants.RequestReason] for authentication
      */
     data class Help(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         val helpString: String?,
         val helpCode: Int,
         override val requestReason: AuthenticationReason,
@@ -96,9 +99,13 @@
     /**
      * Authentication state when no auth is running
      *
+     * @param biometricSourceType null
      * @param requestReason [AuthenticationReason.NotRunning]
      */
-    data class Idle(override val requestReason: AuthenticationReason) : AuthenticationState
+    data class Idle(
+        override val biometricSourceType: BiometricSourceType? = null,
+        override val requestReason: AuthenticationReason
+    ) : AuthenticationState
 
     /**
      * AuthenticationState when auth is started
@@ -107,7 +114,7 @@
      * @param requestReason reason from [BiometricRequestConstants.RequestReason] for authentication
      */
     data class Started(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         override val requestReason: AuthenticationReason
     ) : AuthenticationState
 
@@ -118,7 +125,7 @@
      * @param requestReason [AuthenticationReason.NotRunning]
      */
     data class Stopped(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         override val requestReason: AuthenticationReason
     ) : AuthenticationState
 
@@ -131,7 +138,7 @@
      * @param userId The user id for the requested authentication
      */
     data class Succeeded(
-        val biometricSourceType: BiometricSourceType,
+        override val biometricSourceType: BiometricSourceType,
         val isStrongBiometric: Boolean,
         override val requestReason: AuthenticationReason,
         val userId: Int
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 3fc8b09..b06cf3f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -97,8 +97,10 @@
     private val metricsLogger: CommunalMetricsLogger,
 ) : BaseCommunalViewModel(communalSceneInteractor, communalInteractor, mediaHost) {
 
+    private val logger = Logger(logBuffer, "CommunalViewModel")
+
     private val _isMediaHostVisible =
-        conflatedCallbackFlow<Boolean> {
+        conflatedCallbackFlow {
                 val callback = { visible: Boolean ->
                     trySend(visible)
                     Unit
@@ -106,11 +108,18 @@
                 mediaHost.addVisibilityChangeListener(callback)
                 awaitClose { mediaHost.removeVisibilityChangeListener(callback) }
             }
-            .onStart { emit(mediaHost.visible) }
+            .onStart {
+                // Ensure the visibility state is correct when the hub is opened and this flow is
+                // started so that the UMO is shown when needed. The visibility state in MediaHost
+                // is not updated once its view has been detached, aka the hub is closed, which can
+                // result in this getting stuck as False and never being updated as the UMO is not
+                // shown.
+                mediaHost.updateViewVisibility()
+                emit(mediaHost.visible)
+            }
+            .onEach { logger.d({ "_isMediaHostVisible: $bool1" }) { bool1 = it } }
             .flowOn(mainDispatcher)
 
-    private val logger = Logger(logBuffer, "CommunalViewModel")
-
     /** Communal content saved from the previous emission when the flow is active (not "frozen"). */
     private var frozenCommunalContent: List<CommunalContentModel>? = null
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
index 058ca4d..10a565f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
@@ -36,7 +36,7 @@
     context: Context,
     private val backgroundScope: CoroutineScope,
     hostId: Int,
-    interactionHandler: RemoteViews.InteractionHandler,
+    private val interactionHandler: RemoteViews.InteractionHandler,
     looper: Looper,
     logBuffer: LogBuffer,
 ) : AppWidgetHost(context, hostId, interactionHandler, looper) {
@@ -55,7 +55,7 @@
         appWidgetId: Int,
         appWidget: AppWidgetProviderInfo?
     ): AppWidgetHostView {
-        return CommunalAppWidgetHostView(context)
+        return CommunalAppWidgetHostView(context, interactionHandler)
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt
index 2559137..d549734 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt
@@ -17,17 +17,25 @@
 package com.android.systemui.communal.widgets
 
 import android.appwidget.AppWidgetHostView
+import android.appwidget.AppWidgetManager
 import android.appwidget.AppWidgetProviderInfo
 import android.content.Context
+import android.content.pm.LauncherActivityInfo
+import android.content.pm.LauncherApps
 import android.graphics.Outline
 import android.graphics.Rect
 import android.view.View
 import android.view.ViewOutlineProvider
+import android.widget.RemoteViews
+import android.widget.RemoteViews.RemoteResponse
 import com.android.systemui.animation.LaunchableView
 import com.android.systemui.animation.LaunchableViewDelegate
 
 /** AppWidgetHostView that displays in communal hub with support for rounded corners. */
-class CommunalAppWidgetHostView(context: Context) : AppWidgetHostView(context), LaunchableView {
+class CommunalAppWidgetHostView(
+    context: Context,
+    private val interactionHandler: RemoteViews.InteractionHandler,
+) : AppWidgetHostView(context, interactionHandler), LaunchableView {
     private val launchableViewDelegate =
         LaunchableViewDelegate(
             this,
@@ -92,4 +100,26 @@
         launchableViewDelegate.setShouldBlockVisibilityChanges(block)
 
     override fun setVisibility(visibility: Int) = launchableViewDelegate.setVisibility(visibility)
+
+    override fun onDefaultViewClicked(view: View) {
+        AppWidgetManager.getInstance(context)?.noteAppWidgetTapped(appWidgetId)
+        if (appWidgetInfo == null) {
+            return
+        }
+        val launcherApps = context.getSystemService(LauncherApps::class.java)
+        val activityInfo: LauncherActivityInfo =
+            launcherApps
+                .getActivityList(appWidgetInfo.provider.packageName, appWidgetInfo.profile)
+                ?.getOrNull(0) ?: return
+
+        val intent =
+            launcherApps.getMainActivityLaunchIntent(
+                activityInfo.componentName,
+                null,
+                activityInfo.user
+            )
+        if (intent != null) {
+            interactionHandler.onInteraction(view, intent, RemoteResponse.fromPendingIntent(intent))
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegate.kt
index 8af46f4..1ac3ccd 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegate.kt
@@ -46,6 +46,7 @@
         appName,
         hostUid,
         mediaProjectionMetricsLogger,
+        dialogIconDrawable = R.drawable.ic_cast_connected,
     ) {
     override fun onCreate(dialog: AlertDialog, savedInstanceState: Bundle?) {
         super.onCreate(dialog, savedInstanceState)
@@ -82,7 +83,9 @@
                 listOf(
                     ScreenShareOption(
                         mode = SINGLE_APP,
-                        spinnerText = R.string.screen_share_permission_dialog_option_single_app,
+                        spinnerText =
+                            R.string
+                                .media_projection_entry_cast_permission_dialog_option_text_single_app,
                         warningText =
                             R.string
                                 .media_projection_entry_cast_permission_dialog_warning_single_app,
@@ -90,7 +93,9 @@
                     ),
                     ScreenShareOption(
                         mode = ENTIRE_SCREEN,
-                        spinnerText = R.string.screen_share_permission_dialog_option_entire_screen,
+                        spinnerText =
+                            R.string
+                                .media_projection_entry_cast_permission_dialog_option_text_entire_screen,
                         warningText =
                             R.string
                                 .media_projection_entry_cast_permission_dialog_warning_entire_screen,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
index bc8642c..a77375c 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
@@ -19,7 +19,6 @@
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
-import static com.android.systemui.Flags.screenshotPrivateProfileAccessibilityAnnouncementFix;
 import static com.android.systemui.Flags.screenshotSaveImageExporter;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
@@ -355,19 +354,9 @@
 
     void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) {
         withWindowAttached(() -> {
-            if (screenshotPrivateProfileAccessibilityAnnouncementFix()) {
-                mAnnouncementResolver.getScreenshotAnnouncement(
-                        screenshot.getUserHandle().getIdentifier(),
-                        mViewProxy::announceForAccessibility);
-            } else {
-                if (mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) {
-                    mViewProxy.announceForAccessibility(mContext.getResources().getString(
-                            R.string.screenshot_saving_work_profile_title));
-                } else {
-                    mViewProxy.announceForAccessibility(
-                            mContext.getResources().getString(R.string.screenshot_saving_title));
-                }
-            }
+            mAnnouncementResolver.getScreenshotAnnouncement(
+                    screenshot.getUserHandle().getIdentifier(),
+                    mViewProxy::announceForAccessibility);
         });
 
         mViewProxy.reset();
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index ec529cd..540d4c4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -19,7 +19,6 @@
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
-import static com.android.systemui.Flags.screenshotPrivateProfileAccessibilityAnnouncementFix;
 import static com.android.systemui.Flags.screenshotSaveImageExporter;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
@@ -355,19 +354,11 @@
 
     void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) {
         withWindowAttached(() -> {
-            if (screenshotPrivateProfileAccessibilityAnnouncementFix()) {
-                mAnnouncementResolver.getScreenshotAnnouncement(
-                        screenshot.getUserHandle().getIdentifier(),
-                        mViewProxy::announceForAccessibility);
-            } else {
-                if (mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) {
-                    mViewProxy.announceForAccessibility(mContext.getResources().getString(
-                            R.string.screenshot_saving_work_profile_title));
-                } else {
-                    mViewProxy.announceForAccessibility(
-                            mContext.getResources().getString(R.string.screenshot_saving_title));
-                }
-            }
+            mAnnouncementResolver.getScreenshotAnnouncement(
+                    screenshot.getUserHandle().getIdentifier(),
+                    announcement -> {
+                        mViewProxy.announceForAccessibility(announcement);
+                    });
         });
 
         mViewProxy.reset();
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractor.kt
index 134c983..d1a0a6d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractor.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.shade.domain.interactor
 
 import com.android.systemui.shade.data.repository.ShadeAnimationRepository
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
@@ -38,4 +39,7 @@
      * that is not considered "closing".
      */
     abstract val isAnyCloseAnimationRunning: StateFlow<Boolean>
+
+    /** Whether a short animation to expand or collapse is running after user input has ended. */
+    abstract val isAnyFlingAnimationRunning: Flow<Boolean>
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorEmptyImpl.kt
index f364d6d..dbc1b3b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorEmptyImpl.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.shade.data.repository.ShadeAnimationRepository
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
 
 /** Implementation of ShadeAnimationInteractor for shadeless SysUI variants. */
 @SysUISingleton
@@ -29,4 +30,5 @@
     shadeAnimationRepository: ShadeAnimationRepository,
 ) : ShadeAnimationInteractor(shadeAnimationRepository) {
     override val isAnyCloseAnimationRunning = MutableStateFlow(false)
+    override val isAnyFlingAnimationRunning = flowOf(false)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorLegacyImpl.kt
index c4f4134..32d8659 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorLegacyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorLegacyImpl.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.shade.data.repository.ShadeAnimationRepository
 import com.android.systemui.shade.data.repository.ShadeRepository
 import javax.inject.Inject
+import kotlinx.coroutines.flow.map
 
 /** Implementation of ShadeAnimationInteractor compatible with NPVC. */
 @SysUISingleton
@@ -30,4 +31,5 @@
     shadeRepository: ShadeRepository,
 ) : ShadeAnimationInteractor(shadeAnimationRepository) {
     override val isAnyCloseAnimationRunning = shadeRepository.legacyIsClosing
+    override val isAnyFlingAnimationRunning = shadeRepository.currentFling.map { it != null }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImpl.kt
index d9982e3..79a94a5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImpl.kt
@@ -36,12 +36,12 @@
 @SysUISingleton
 class ShadeAnimationInteractorSceneContainerImpl
 @Inject
+@OptIn(ExperimentalCoroutinesApi::class)
 constructor(
     @Background scope: CoroutineScope,
     shadeAnimationRepository: ShadeAnimationRepository,
     sceneInteractor: SceneInteractor,
 ) : ShadeAnimationInteractor(shadeAnimationRepository) {
-    @OptIn(ExperimentalCoroutinesApi::class)
     override val isAnyCloseAnimationRunning =
         sceneInteractor.transitionState
             .flatMapLatest { state ->
@@ -62,4 +62,26 @@
             }
             .distinctUntilChanged()
             .stateIn(scope, SharingStarted.Eagerly, false)
+
+    override val isAnyFlingAnimationRunning =
+        sceneInteractor.transitionState
+            .flatMapLatest { state ->
+                when (state) {
+                    is ObservableTransitionState.Idle -> flowOf(false)
+                    is ObservableTransitionState.Transition ->
+                        if (
+                            state.isInitiatedByUserInput &&
+                                (state.fromScene == Scenes.Shade ||
+                                    state.toScene == Scenes.Shade ||
+                                    state.fromScene == Scenes.QuickSettings ||
+                                    state.toScene == Scenes.QuickSettings)
+                        ) {
+                            state.isUserInputOngoing.map { !it }
+                        } else {
+                            flowOf(false)
+                        }
+                }
+            }
+            .distinctUntilChanged()
+            .stateIn(scope, SharingStarted.Eagerly, false)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index a6ca3ab..17f401a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -19,6 +19,9 @@
 import android.Manifest.permission.RECEIVE_EMERGENCY_BROADCAST
 import android.app.Notification
 import android.app.Notification.BubbleMetadata
+import android.app.Notification.CATEGORY_ALARM
+import android.app.Notification.CATEGORY_CAR_EMERGENCY
+import android.app.Notification.CATEGORY_CAR_WARNING
 import android.app.Notification.CATEGORY_EVENT
 import android.app.Notification.CATEGORY_REMINDER
 import android.app.Notification.VISIBILITY_PRIVATE
@@ -42,6 +45,7 @@
 import android.service.notification.Flags
 import com.android.internal.logging.UiEvent
 import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -307,6 +311,9 @@
         ALLOW_CALLSTYLE,
         ALLOW_CATEGORY_REMINDER,
         ALLOW_CATEGORY_EVENT,
+        ALLOW_CATEGORY_ALARM,
+        ALLOW_CATEGORY_CAR_EMERGENCY,
+        ALLOW_CATEGORY_CAR_WARNING,
         ALLOW_FSI_WITH_PERMISSION_ON,
         ALLOW_COLORIZED,
         ALLOW_EMERGENCY,
@@ -333,8 +340,13 @@
         @UiEvent(doc = "HUN allowed during avalanche because it is colorized.")
         AVALANCHE_SUPPRESSOR_HUN_ALLOWED_COLORIZED(1832),
         @UiEvent(doc = "HUN allowed during avalanche because it is an emergency notification.")
-        AVALANCHE_SUPPRESSOR_HUN_ALLOWED_EMERGENCY(1833);
-
+        AVALANCHE_SUPPRESSOR_HUN_ALLOWED_EMERGENCY(1833),
+        @UiEvent(doc = "HUN allowed during avalanche because it is an alarm.")
+        AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_ALARM(1867),
+        @UiEvent(doc = "HUN allowed during avalanche because it is a car emergency.")
+        AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_CAR_EMERGENCY(1868),
+        @UiEvent(doc = "HUN allowed during avalanche because it is a car warning")
+        AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_CAR_WARNING(1869);
         override fun getId(): Int {
             return id
         }
@@ -423,6 +435,22 @@
             return State.ALLOW_CATEGORY_REMINDER
         }
 
+        if (entry.sbn.notification.category == CATEGORY_ALARM) {
+            uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_ALARM)
+            return State.ALLOW_CATEGORY_ALARM
+        }
+
+        if (entry.sbn.notification.category == CATEGORY_CAR_EMERGENCY) {
+            uiEventLogger.log(
+                    AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_CAR_EMERGENCY)
+            return State.ALLOW_CATEGORY_CAR_EMERGENCY
+        }
+
+        if (entry.sbn.notification.category == CATEGORY_CAR_WARNING) {
+            uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_CAR_WARNING)
+            return State.ALLOW_CATEGORY_CAR_WARNING
+        }
+
         if (entry.sbn.notification.category == CATEGORY_EVENT) {
             uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_CATEGORY_EVENT)
             return State.ALLOW_CATEGORY_EVENT
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 37fdaeb..6d3cad5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2995,7 +2995,7 @@
                 @Override
                 public void onFalse() {
                     // Hides quick settings, bouncer, and quick-quick settings.
-                    mStatusBarKeyguardViewManager.reset(true, /* isFalsingReset= */true);
+                    mStatusBarKeyguardViewManager.reset(true);
                 }
             };
 
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 0b8f18e..2d775b7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -708,7 +708,7 @@
      * Shows the notification keyguard or the bouncer depending on
      * {@link #needsFullscreenBouncer()}.
      */
-    protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
+    protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) {
         boolean isDozing = mDozing;
         if (Flags.simPinRaceConditionOnRestart()) {
             KeyguardState toState = mKeyguardTransitionInteractor.getTransitionState().getValue()
@@ -734,12 +734,8 @@
                         mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
                     }
                 }
-            } else if (!isFalsingReset) {
-                // Falsing resets can cause this to flicker, so don't reset in this case
-                Log.i(TAG, "Sim bouncer is already showing, issuing a refresh");
-                mPrimaryBouncerInteractor.hide();
-                mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
-
+            } else {
+                Log.e(TAG, "Attempted to show the sim bouncer when it is already showing.");
             }
         } else {
             mCentralSurfaces.showKeyguard();
@@ -961,10 +957,6 @@
 
     @Override
     public void reset(boolean hideBouncerWhenShowing) {
-        reset(hideBouncerWhenShowing, /* isFalsingReset= */false);
-    }
-
-    public void reset(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
         if (mKeyguardStateController.isShowing() && !bouncerIsAnimatingAway()) {
             final boolean isOccluded = mKeyguardStateController.isOccluded();
             // Hide quick settings.
@@ -976,7 +968,7 @@
                     hideBouncer(false /* destroyView */);
                 }
             } else {
-                showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset);
+                showBouncerOrKeyguard(hideBouncerWhenShowing);
             }
             if (hideBouncerWhenShowing) {
                 hideAlternateBouncer(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt
index bdbe5eb..59602dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt
@@ -44,8 +44,10 @@
 
     private val appName = "Test App"
 
-    private val resIdSingleApp = R.string.screen_share_permission_dialog_option_single_app
-    private val resIdFullScreen = R.string.screen_share_permission_dialog_option_entire_screen
+    private val resIdSingleApp =
+        R.string.media_projection_entry_cast_permission_dialog_option_text_single_app
+    private val resIdFullScreen =
+        R.string.media_projection_entry_cast_permission_dialog_option_text_entire_screen
     private val resIdSingleAppDisabled =
         R.string.media_projection_entry_app_permission_dialog_single_app_disabled
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
index f9509d2..d1b1f46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
@@ -17,6 +17,9 @@
 package com.android.systemui.statusbar.notification.interruption
 
 import android.Manifest.permission
+import android.app.Notification.CATEGORY_ALARM
+import android.app.Notification.CATEGORY_CAR_EMERGENCY
+import android.app.Notification.CATEGORY_CAR_WARNING
 import android.app.Notification.CATEGORY_EVENT
 import android.app.Notification.CATEGORY_REMINDER
 import android.app.NotificationManager
@@ -256,6 +259,61 @@
     }
 
     @Test
+    fun testAvalancheFilter_duringAvalanche_allowCategoryAlarm() {
+        avalancheProvider.startTime = whenAgo(10)
+
+        withFilter(
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
+                uiEventLogger, context, notificationManager)
+        ) {
+            ensurePeekState()
+            assertShouldHeadsUp(
+                buildEntry {
+                    importance = NotificationManager.IMPORTANCE_HIGH
+                    category = CATEGORY_ALARM
+                }
+            )
+        }
+    }
+
+    @Test
+    fun testAvalancheFilter_duringAvalanche_allowCategoryCarEmergency() {
+        avalancheProvider.startTime = whenAgo(10)
+
+        withFilter(
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
+                uiEventLogger, context, notificationManager)
+        ) {
+            ensurePeekState()
+            assertShouldHeadsUp(
+                buildEntry {
+                    importance = NotificationManager.IMPORTANCE_HIGH
+                    category = CATEGORY_CAR_EMERGENCY
+
+                }
+            )
+        }
+    }
+
+    @Test
+    fun testAvalancheFilter_duringAvalanche_allowCategoryCarWarning() {
+        avalancheProvider.startTime = whenAgo(10)
+
+        withFilter(
+            AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager,
+                uiEventLogger, context, notificationManager)
+        ) {
+            ensurePeekState()
+            assertShouldHeadsUp(
+                buildEntry {
+                    importance = NotificationManager.IMPORTANCE_HIGH
+                    category = CATEGORY_CAR_WARNING
+                }
+            )
+        }
+    }
+
+    @Test
     fun testAvalancheFilter_duringAvalanche_allowFsi() {
         avalancheProvider.startTime = whenAgo(10)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 9b61105..af5e60e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1068,7 +1068,7 @@
     public void testShowBouncerOrKeyguard_needsFullScreen() {
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
         verify(mCentralSurfaces).hideKeyguard();
         verify(mPrimaryBouncerInteractor).show(true);
     }
@@ -1084,7 +1084,7 @@
                 .thenReturn(KeyguardState.LOCKSCREEN);
 
         reset(mCentralSurfaces);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
         verify(mPrimaryBouncerInteractor).show(true);
         verify(mCentralSurfaces).showKeyguard();
     }
@@ -1092,26 +1092,11 @@
     @Test
     @DisableSceneContainer
     public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() {
-        boolean isFalsingReset = false;
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
         when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
         verify(mCentralSurfaces, never()).hideKeyguard();
-        verify(mPrimaryBouncerInteractor).show(true);
-    }
-
-    @Test
-    @DisableSceneContainer
-    public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing_onFalsing() {
-        boolean isFalsingReset = true;
-        when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
-                KeyguardSecurityModel.SecurityMode.SimPin);
-        when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
-        verify(mCentralSurfaces, never()).hideKeyguard();
-
-        // Do not refresh the full screen bouncer if the call is from falsing
         verify(mPrimaryBouncerInteractor, never()).show(true);
     }
 
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
index ee28099..0238baa 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
@@ -16,6 +16,7 @@
 package com.android.ravenwood.common;
 
 import java.io.FileDescriptor;
+import java.io.IOException;
 
 /**
  * Collection of methods to workaround limitation in the hostside JVM.
@@ -44,6 +45,11 @@
     public abstract int getFdInt(FileDescriptor fd);
 
     /**
+     * Equivalent to Android's Os.close(fd).
+     */
+    public abstract void closeFd(FileDescriptor fd) throws IOException;
+
+    /**
      * Placeholder implementation for the host side.
      *
      * Even on the host side, we don't want to throw just because the class is loaded,
@@ -64,5 +70,10 @@
         public int getFdInt(FileDescriptor fd) {
             throw calledOnHostside();
         }
+
+        @Override
+        public void closeFd(FileDescriptor fd) {
+            throw calledOnHostside();
+        }
     }
 }
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
index 9aedaab..a260147 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
@@ -16,6 +16,8 @@
 package com.android.ravenwood.common;
 
 import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
 
 class OpenJdkWorkaround extends JvmWorkaround {
     @Override
@@ -43,4 +45,19 @@
                     + " perhaps JRE has changed?", e);
         }
     }
+
+    @Override
+    public void closeFd(FileDescriptor fd) throws IOException {
+        try {
+            final Object obj = Class.forName("jdk.internal.access.SharedSecrets").getMethod(
+                    "getJavaIOFileDescriptorAccess").invoke(null);
+            Class.forName("jdk.internal.access.JavaIOFileDescriptorAccess").getMethod(
+                    "close", FileDescriptor.class).invoke(obj, fd);
+        } catch (InvocationTargetException e) {
+            SneakyThrow.sneakyThrow(e.getTargetException());
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException("Failed to interact with raw FileDescriptor internals;"
+                    + " perhaps JRE has changed?", e);
+        }
+    }
 }
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/SneakyThrow.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/SneakyThrow.java
new file mode 100644
index 0000000..0dbf7df
--- /dev/null
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/SneakyThrow.java
@@ -0,0 +1,33 @@
+/*
+ * 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.ravenwood.common;
+
+public class SneakyThrow {
+
+    private SneakyThrow() {
+    }
+
+    /**
+     * Throw checked exceptions without the need to declare in method signature
+     */
+    public static void sneakyThrow(Throwable t) {
+        SneakyThrow.<RuntimeException>sneakyThrow_(t);
+    }
+
+    private static <T extends Throwable> void sneakyThrow_(Throwable t) throws T {
+        throw (T) t;
+    }
+}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
index 1a15d7a..5a3589d 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
@@ -16,105 +16,16 @@
 
 package com.android.platform.test.ravenwood.nativesubstitution;
 
-import static android.os.ParcelFileDescriptor.MODE_APPEND;
-import static android.os.ParcelFileDescriptor.MODE_CREATE;
-import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
-import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
-import static android.os.ParcelFileDescriptor.MODE_TRUNCATE;
-import static android.os.ParcelFileDescriptor.MODE_WORLD_READABLE;
-import static android.os.ParcelFileDescriptor.MODE_WORLD_WRITEABLE;
-import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY;
-
-import android.system.ErrnoException;
-import android.system.Os;
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
 import com.android.ravenwood.common.JvmWorkaround;
 
-import java.io.File;
 import java.io.FileDescriptor;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.util.HashMap;
-import java.util.Map;
 
 public class ParcelFileDescriptor_host {
-    private static final String TAG = "ParcelFileDescriptor_host";
-
-    /**
-     * Since we don't have a great way to keep an unmanaged {@code FileDescriptor} reference
-     * alive, we keep a strong reference to the {@code RandomAccessFile} we used to open it. This
-     * gives us a way to look up the original parent object when closing later.
-     */
-    @GuardedBy("sActive")
-    private static final Map<FileDescriptor, RandomAccessFile> sActive = new HashMap<>();
-
-    public static void native_setFdInt$ravenwood(FileDescriptor fd, int fdInt) {
+    public static void setFdInt(FileDescriptor fd, int fdInt) {
         JvmWorkaround.getInstance().setFdInt(fd, fdInt);
     }
 
-    public static int native_getFdInt$ravenwood(FileDescriptor fd) {
+    public static int getFdInt(FileDescriptor fd) {
         return JvmWorkaround.getInstance().getFdInt(fd);
     }
-
-    public static FileDescriptor native_open$ravenwood(File file, int pfdMode) throws IOException {
-        if ((pfdMode & MODE_CREATE) != 0 && !file.exists()) {
-            throw new FileNotFoundException();
-        }
-
-        final String modeString;
-        if ((pfdMode & MODE_READ_WRITE) == MODE_READ_WRITE) {
-            modeString = "rw";
-        } else if ((pfdMode & MODE_WRITE_ONLY) == MODE_WRITE_ONLY) {
-            modeString = "rw";
-        } else if ((pfdMode & MODE_READ_ONLY) == MODE_READ_ONLY) {
-            modeString = "r";
-        } else {
-            throw new IllegalArgumentException();
-        }
-
-        final RandomAccessFile raf = new RandomAccessFile(file, modeString);
-
-        // Now that we have a real file on disk, match requested flags
-        if ((pfdMode & MODE_TRUNCATE) != 0) {
-            raf.setLength(0);
-        }
-        if ((pfdMode & MODE_APPEND) != 0) {
-            raf.seek(raf.length());
-        }
-        if ((pfdMode & MODE_WORLD_READABLE) != 0) {
-            file.setReadable(true, false);
-        }
-        if ((pfdMode & MODE_WORLD_WRITEABLE) != 0) {
-            file.setWritable(true, false);
-        }
-
-        final FileDescriptor fd = raf.getFD();
-        synchronized (sActive) {
-            sActive.put(fd, raf);
-        }
-        return fd;
-    }
-
-    public static void native_close$ravenwood(FileDescriptor fd) {
-        final RandomAccessFile raf;
-        synchronized (sActive) {
-            raf = sActive.remove(fd);
-        }
-        int fdInt = JvmWorkaround.getInstance().getFdInt(fd);
-        try {
-            if (raf != null) {
-                raf.close();
-            } else {
-                // This FD wasn't created by native_open$ravenwood().
-                // The FD was passed to the PFD ctor. Just close it.
-                Os.close(fd);
-            }
-        } catch (IOException | ErrnoException e) {
-            Log.w(TAG, "Exception thrown while closing fd " + fdInt, e);
-        }
-    }
 }
-;
\ No newline at end of file
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
index 825ab72..ecaa816 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
@@ -15,9 +15,11 @@
  */
 package android.system;
 
+import com.android.ravenwood.common.JvmWorkaround;
 import com.android.ravenwood.common.RavenwoodRuntimeNative;
 
 import java.io.FileDescriptor;
+import java.io.IOException;
 
 /**
  * OS class replacement used on Ravenwood. For now, we just implement APIs as we need them...
@@ -56,6 +58,15 @@
 
     /** Ravenwood version of the OS API. */
     public static void close(FileDescriptor fd) throws ErrnoException {
-        RavenwoodRuntimeNative.close(fd);
+        try {
+            JvmWorkaround.getInstance().closeFd(fd);
+        } catch (IOException e) {
+            // The only valid error on Linux that can happen is EIO
+            throw new ErrnoException("close", OsConstants.EIO);
+        }
+    }
+
+    public static FileDescriptor open(String path, int flags, int mode) throws ErrnoException {
+        return RavenwoodRuntimeNative.open(path, flags, mode);
     }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
index 2bc8e71..beba833 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
@@ -48,7 +48,7 @@
 
     public static native StructStat stat(String path) throws ErrnoException;
 
-    private static native void nClose(int fd) throws ErrnoException;
+    private static native int nOpen(String path, int flags, int mode) throws ErrnoException;
 
     public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
         return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence);
@@ -69,7 +69,7 @@
     public static FileDescriptor dup(FileDescriptor fd) throws ErrnoException {
         var fdInt = nDup(JvmWorkaround.getInstance().getFdInt(fd));
 
-        var retFd = new java.io.FileDescriptor();
+        var retFd = new FileDescriptor();
         JvmWorkaround.getInstance().setFdInt(retFd, fdInt);
         return retFd;
     }
@@ -86,10 +86,11 @@
         return nFstat(fdInt);
     }
 
-    /** See close(2) */
-    public static void close(FileDescriptor fd) throws ErrnoException {
-        var fdInt = JvmWorkaround.getInstance().getFdInt(fd);
-
-        nClose(fdInt);
+    public static FileDescriptor open(String path, int flags, int mode) throws ErrnoException {
+        int fd = nOpen(path, flags, mode);
+        if (fd < 0) return null;
+        var retFd = new FileDescriptor();
+        JvmWorkaround.getInstance().setFdInt(retFd, fd);
+        return retFd;
     }
 }
diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp
index ee84954..c804928 100644
--- a/ravenwood/runtime-jni/ravenwood_runtime.cpp
+++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp
@@ -18,9 +18,11 @@
 #include <sys/stat.h>
 #include <string.h>
 #include <unistd.h>
+#include <string>
 #include <nativehelper/JNIHelp.h>
 #include <nativehelper/ScopedLocalRef.h>
 #include <nativehelper/ScopedUtfChars.h>
+#include <nativehelper/ScopedPrimitiveArray.h>
 
 #include "jni.h"
 #include "utils/Log.h"
@@ -49,6 +51,43 @@
 static jclass g_StructStat;
 static jclass g_StructTimespecClass;
 
+// We have to explicitly decode the string to real UTF-8, because when using GetStringUTFChars
+// we only get modified UTF-8, which is not the platform string type used in host JVM.
+struct ScopedRealUtf8Chars {
+    ScopedRealUtf8Chars(JNIEnv* env, jstring s) : valid_(false) {
+        if (s == nullptr) {
+            jniThrowNullPointerException(env);
+            return;
+        }
+        jclass clazz = env->GetObjectClass(s);
+        jmethodID getBytes = env->GetMethodID(clazz, "getBytes", "(Ljava/lang/String;)[B");
+
+        ScopedLocalRef<jstring> utf8(env, env->NewStringUTF("UTF-8"));
+        ScopedLocalRef<jbyteArray> jbytes(env,
+            (jbyteArray) env->CallObjectMethod(s, getBytes, utf8.get()));
+
+        ScopedByteArrayRO bytes(env, jbytes.get());
+        string_.append((const char *) bytes.get(), bytes.size());
+        valid_ = true;
+    }
+
+    const char* c_str() const {
+        return valid_ ? string_.c_str() : nullptr;
+    }
+
+    size_t size() const {
+        return string_.size();
+    }
+
+    const char& operator[](size_t n) const {
+        return string_[n];
+    }
+
+private:
+    std::string string_;
+    bool valid_;
+};
+
 static jclass findClass(JNIEnv* env, const char* name) {
     ScopedLocalRef<jclass> localClass(env, env->FindClass(name));
     jclass result = reinterpret_cast<jclass>(env->NewGlobalRef(localClass.get()));
@@ -99,7 +138,7 @@
 }
 
 static jobject doStat(JNIEnv* env, jstring javaPath, bool isLstat) {
-    ScopedUtfChars path(env, javaPath);
+    ScopedRealUtf8Chars path(env, javaPath);
     if (path.c_str() == NULL) {
         return NULL;
     }
@@ -167,9 +206,12 @@
     return doStat(env, javaPath, false);
 }
 
-static void nClose(JNIEnv* env, jclass, jint fd) {
-    // Don't use TEMP_FAILURE_RETRY() on close(): https://lkml.org/lkml/2005/9/10/129
-    throwIfMinusOne(env, "close", close(fd));
+static jint Linux_open(JNIEnv* env, jobject, jstring javaPath, jint flags, jint mode) {
+    ScopedRealUtf8Chars path(env, javaPath);
+    if (path.c_str() == NULL) {
+        return -1;
+    }
+    return throwIfMinusOne(env, "open", TEMP_FAILURE_RETRY(open(path.c_str(), flags, mode)));
 }
 
 // ---- Registration ----
@@ -184,7 +226,7 @@
     { "nFstat", "(I)Landroid/system/StructStat;", (void*)nFstat },
     { "lstat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_lstat },
     { "stat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_stat },
-    { "nClose", "(I)V", (void*)nClose },
+    { "nOpen", "(Ljava/lang/String;II)I", (void*)Linux_open },
 };
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
index b5b998f..6b6b39d 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
@@ -1131,7 +1131,10 @@
             }
 
             if (isAlwaysOnMagnificationEnabled()) {
-                zoomOutFromService(displayId);
+                if (!mControllerCtx.getContext().getResources().getBoolean(
+                        R.bool.config_magnification_keep_zoom_level_when_context_changed)) {
+                    zoomOutFromService(displayId);
+                }
             } else {
                 reset(displayId, true);
             }
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 33cf842..fdf0ba6 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -3535,6 +3535,10 @@
 
         synchronized (mRecords) {
             int phoneId = getPhoneIdFromSubId(subId);
+            if (!validatePhoneId(phoneId)) {
+                loge("Invalid phone ID " + phoneId + " for " + subId);
+                return;
+            }
             mCarrierRoamingNtnMode[phoneId] = active;
             for (Record r : mRecords) {
                 if (r.matchTelephonyCallbackEvent(
@@ -3582,6 +3586,10 @@
 
         synchronized (mRecords) {
             int phoneId = getPhoneIdFromSubId(subId);
+            if (!validatePhoneId(phoneId)) {
+                loge("Invalid phone ID " + phoneId + " for " + subId);
+                return;
+            }
             mCarrierRoamingNtnEligible[phoneId] = eligible;
             for (Record r : mRecords) {
                 if (r.matchTelephonyCallbackEvent(
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index cf0befa..33f33fb 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1716,6 +1716,12 @@
      */
     @Nullable volatile ContentCaptureManagerInternal mContentCaptureService;
 
+    /**
+     * The interface to the freezer.
+     */
+    @NonNull
+    private final Freezer mFreezer;
+
     /*
      * The default duration for the binder heavy hitter auto sampler
      */
@@ -2506,6 +2512,7 @@
             @Nullable UserController userController) {
         mInjector = injector;
         mContext = mInjector.getContext();
+        mFreezer = injector.getFreezer();
         mUiContext = null;
         mAppErrors = injector.getAppErrors();
         mPackageWatchdog = null;
@@ -2555,6 +2562,7 @@
         LockGuard.installLock(this, LockGuard.INDEX_ACTIVITY);
         mInjector = new Injector(systemContext);
         mContext = systemContext;
+        mFreezer = mInjector.getFreezer();
 
         mFactoryTest = FactoryTest.getMode();
         mSystemThread = ActivityThread.currentActivityThread();
@@ -20919,6 +20927,11 @@
         public IntentFirewall getIntentFirewall() {
             return null;
         }
+
+        /** @return the default Freezer. */
+        public Freezer getFreezer() {
+            return new Freezer();
+        }
     }
 
     @Override
@@ -21022,7 +21035,7 @@
         final long token = Binder.clearCallingIdentity();
 
         try {
-            return CachedAppOptimizer.isFreezerSupported();
+            return mFreezer.isFreezerSupported();
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -21177,4 +21190,9 @@
     void clearPendingTopAppLocked() {
         mPendingStartActivityUids.clear();
     }
+
+    @NonNull
+    Freezer getFreezer() {
+        return mFreezer;
+    }
 }
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 1c4ffbb..11e8353 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -664,6 +664,8 @@
     private final ProcessDependencies mProcessDependencies;
     private final ProcLocksReader mProcLocksReader;
 
+    private final Freezer mFreezer;
+
     public CachedAppOptimizer(ActivityManagerService am) {
         this(am, null, new DefaultProcessDependencies());
     }
@@ -680,6 +682,7 @@
         mTestCallback = callback;
         mSettingsObserver = new SettingsContentObserver();
         mProcLocksReader = new ProcLocksReader();
+        mFreezer = mAm.getFreezer();
     }
 
     /**
@@ -1050,89 +1053,6 @@
     }
 
     /**
-     * Informs binder that a process is about to be frozen. If freezer is enabled on a process via
-     * this method, this method will synchronously dispatch all pending transactions to the
-     * specified pid. This method will not add significant latencies when unfreezing.
-     * After freezing binder calls, binder will block all transaction to the frozen pid, and return
-     * an error to the sending process.
-     *
-     * @param pid the target pid for which binder transactions are to be frozen
-     * @param freeze specifies whether to flush transactions and then freeze (true) or unfreeze
-     * binder for the specificed pid.
-     * @param timeoutMs the timeout in milliseconds to wait for the binder interface to freeze
-     * before giving up.
-     *
-     * @throws RuntimeException in case a flush/freeze operation could not complete successfully.
-     * @return 0 if success, or -EAGAIN indicating there's pending transaction.
-     */
-    public static native int freezeBinder(int pid, boolean freeze, int timeoutMs);
-
-    /**
-     * Retrieves binder freeze info about a process.
-     * @param pid the pid for which binder freeze info is to be retrieved.
-     *
-     * @throws RuntimeException if the operation could not complete successfully.
-     * @return a bit field reporting the binder freeze info for the process.
-     */
-    private static native int getBinderFreezeInfo(int pid);
-
-    /**
-     * Returns the path to be checked to verify whether the freezer is supported by this system.
-     * @return absolute path to the file
-     */
-    private static native String getFreezerCheckPath();
-
-    /**
-     * Check if task_profiles.json includes valid freezer profiles and actions
-     * @return false if there are invalid profiles or actions
-     */
-    private static native boolean isFreezerProfileValid();
-
-    /**
-     * Determines whether the freezer is supported by this system
-     */
-    public static boolean isFreezerSupported() {
-        boolean supported = false;
-        FileReader fr = null;
-
-        try {
-            String path = getFreezerCheckPath();
-            Slog.d(TAG_AM, "Checking cgroup freezer: " + path);
-            fr = new FileReader(path);
-            char state = (char) fr.read();
-
-            if (state == '1' || state == '0') {
-                // Also check freezer binder ioctl
-                Slog.d(TAG_AM, "Checking binder freezer ioctl");
-                getBinderFreezeInfo(Process.myPid());
-
-                // Check if task_profiles.json contains invalid profiles
-                Slog.d(TAG_AM, "Checking freezer profiles");
-                supported = isFreezerProfileValid();
-            } else {
-                Slog.e(TAG_AM, "Unexpected value in cgroup.freeze");
-            }
-        } catch (java.io.FileNotFoundException e) {
-            Slog.w(TAG_AM, "File cgroup.freeze not present");
-        } catch (RuntimeException e) {
-            Slog.w(TAG_AM, "Unable to read freezer info");
-        } catch (Exception e) {
-            Slog.w(TAG_AM, "Unable to read cgroup.freeze: " + e.toString());
-        }
-
-        if (fr != null) {
-            try {
-                fr.close();
-            } catch (java.io.IOException e) {
-                Slog.e(TAG_AM, "Exception closing cgroup.freeze: " + e.toString());
-            }
-        }
-
-        Slog.d(TAG_AM, "Freezer supported: " + supported);
-        return supported;
-    }
-
-    /**
      * Reads the flag value from DeviceConfig to determine whether app freezer
      * should be enabled, and starts the freeze/compaction thread if needed.
      */
@@ -1146,7 +1066,7 @@
         } else if ("enabled".equals(configOverride)
                 || DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
                     KEY_USE_FREEZER, DEFAULT_USE_FREEZER)) {
-            mUseFreezer = isFreezerSupported();
+            mUseFreezer = mFreezer.isFreezerSupported();
             updateFreezerDebounceTimeout();
             updateFreezerExemptInstPkg();
         } else {
@@ -1528,7 +1448,7 @@
         boolean processKilled = false;
 
         try {
-            int freezeInfo = getBinderFreezeInfo(pid);
+            int freezeInfo = mFreezer.getBinderFreezeInfo(pid);
 
             if ((freezeInfo & SYNC_RECEIVED_WHILE_FROZEN) != 0) {
                 Slog.d(TAG_AM, "pid " + pid + " " + app.processName
@@ -1562,7 +1482,7 @@
         long freezeTime = opt.getFreezeUnfreezeTime();
 
         try {
-            freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS);
+            mFreezer.freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS);
         } catch (RuntimeException e) {
             Slog.e(TAG_AM, "Unable to unfreeze binder for " + pid + " " + app.processName
                     + ". Killing it");
@@ -1574,7 +1494,7 @@
 
         try {
             traceAppFreeze(app.processName, pid, reason);
-            Process.setProcessFrozen(pid, app.uid, false);
+            mFreezer.setProcessFrozen(pid, app.uid, false);
 
             opt.setFreezeUnfreezeTime(SystemClock.uptimeMillis());
             opt.setFrozen(false);
@@ -1617,7 +1537,7 @@
             }
             Slog.d(TAG_AM, "quick sync unfreeze " + pid + " for " +  reason);
             try {
-                freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS);
+                mFreezer.freezeBinder(pid, false, FREEZE_BINDER_TIMEOUT_MS);
             } catch (RuntimeException e) {
                 Slog.e(TAG_AM, "Unable to quick unfreeze binder for " + pid);
                 return;
@@ -1625,7 +1545,7 @@
 
             try {
                 traceAppFreeze(app.processName, pid, reason);
-                Process.setProcessFrozen(pid, app.uid, false);
+                mFreezer.setProcessFrozen(pid, app.uid, false);
             } catch (Exception e) {
                 Slog.e(TAG_AM, "Unable to quick unfreeze " + pid);
             }
@@ -2394,7 +2314,7 @@
                 // Freeze binder interface before the process, to flush any
                 // transactions that might be pending.
                 try {
-                    if (freezeBinder(pid, true, FREEZE_BINDER_TIMEOUT_MS) != 0) {
+                    if (mFreezer.freezeBinder(pid, true, FREEZE_BINDER_TIMEOUT_MS) != 0) {
                         handleBinderFreezerFailure(proc, "outstanding txns");
                         return;
                     }
@@ -2413,7 +2333,7 @@
 
                 try {
                     traceAppFreeze(proc.processName, pid, -1);
-                    Process.setProcessFrozen(pid, proc.uid, true);
+                    mFreezer.setProcessFrozen(pid, proc.uid, true);
                     opt.setFreezeUnfreezeTime(SystemClock.uptimeMillis());
                     opt.setFrozen(true);
                     opt.setHasCollectedFrozenPSS(false);
@@ -2452,7 +2372,7 @@
 
             try {
                 // post-check to prevent races
-                int freezeInfo = getBinderFreezeInfo(pid);
+                int freezeInfo = mFreezer.getBinderFreezeInfo(pid);
 
                 if ((freezeInfo & TXNS_PENDING_WHILE_FROZEN) != 0) {
                     synchronized (mProcLock) {
@@ -2620,6 +2540,22 @@
     }
 
     /**
+     * Freeze or unfreeze a process.  This should only be used for testing.
+     */
+    @VisibleForTesting
+    void forceFreezeForTest(ProcessRecord proc, boolean freeze) {
+        synchronized (mAm) {
+            synchronized (mProcLock) {
+                if (freeze) {
+                    forceFreezeAppAsyncLSP(proc);
+                } else {
+                    unfreezeAppInternalLSP(proc, UNFREEZE_REASON_NONE, true);
+                }
+            }
+        }
+    }
+
+    /**
      * Sending binder transactions to frozen apps most likely indicates there's a bug. Log it and
      * kill the frozen apps if they 1) receive sync binder transactions while frozen, or 2) miss
      * async binder transactions due to kernel binder buffer running out.
@@ -2660,7 +2596,7 @@
         for (int i = 0; i < pids.size(); i++) {
             int current = pids.get(i);
             try {
-                int freezeInfo = getBinderFreezeInfo(current);
+                int freezeInfo = mFreezer.getBinderFreezeInfo(current);
 
                 if ((freezeInfo & SYNC_RECEIVED_WHILE_FROZEN) != 0) {
                     killProcess(current, "Sync transaction while frozen",
diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java
index afb7bb4..1314521 100644
--- a/services/core/java/com/android/server/am/ContentProviderHelper.java
+++ b/services/core/java/com/android/server/am/ContentProviderHelper.java
@@ -1806,10 +1806,12 @@
                         ActivityManagerService.WAIT_FOR_CONTENT_PROVIDER_TIMEOUT_MSG, cpr);
             }
             final int userId = UserHandle.getUserId(cpr.uid);
+            boolean removed = false;
             // Don't remove from provider map if it doesn't match
             // could be a new content provider is starting
             if (mProviderMap.getProviderByClass(cpr.name, userId) == cpr) {
                 mProviderMap.removeProviderByClass(cpr.name, userId);
+                removed = true;
             }
             String[] names = cpr.info.authority.split(";");
             for (int j = 0; j < names.length; j++) {
@@ -1817,8 +1819,12 @@
                 // could be a new content provider is starting
                 if (mProviderMap.getProviderByName(names[j], userId) == cpr) {
                     mProviderMap.removeProviderByName(names[j], userId);
+                    removed = true;
                 }
             }
+            if (removed && cpr.proc != null) {
+                cpr.proc.mProviders.removeProvider(cpr.info.name);
+            }
         }
 
         for (int i = cpr.connections.size() - 1; i >= 0; i--) {
diff --git a/services/core/java/com/android/server/am/Freezer.java b/services/core/java/com/android/server/am/Freezer.java
new file mode 100644
index 0000000..3b3cf55
--- /dev/null
+++ b/services/core/java/com/android/server/am/Freezer.java
@@ -0,0 +1,115 @@
+/*
+ * 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.am;
+
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+
+import android.os.Process;
+
+/**
+ * A collection of interfaces to manage the freezer.  All access to the freezer goes through an
+ * instance of this class.  The class can be overridden for testing.
+ *
+ * Methods may be called without external synchronization.  Multiple instances of this class can be
+ * used concurrently.
+ */
+class Freezer {
+
+    /**
+     * Freeze or unfreeze the specified process.
+     *
+     * @param pid Identifier of the process to freeze or unfreeze.
+     * @param uid Identifier of the user the process is running under.
+     * @param frozen Specify whether to free (true) or unfreeze (false).
+     */
+    public void setProcessFrozen(int pid, int uid, boolean frozen) {
+        Process.setProcessFrozen(pid, uid, frozen);
+    }
+
+    /**
+     * Informs binder that a process is about to be frozen. If freezer is enabled on a process via
+     * this method, this method will synchronously dispatch all pending transactions to the
+     * specified pid. This method will not add significant latencies when unfreezing.
+     * After freezing binder calls, binder will block all transaction to the frozen pid, and return
+     * an error to the sending process.
+     *
+     * @param pid the target pid for which binder transactions are to be frozen
+     * @param freeze specifies whether to flush transactions and then freeze (true) or unfreeze
+     * binder for the specified pid.
+     * @param timeoutMs the timeout in milliseconds to wait for the binder interface to freeze
+     * before giving up.
+     *
+     * @throws RuntimeException in case a flush/freeze operation could not complete successfully.
+     * @return 0 if success, or -EAGAIN indicating there's pending transaction.
+     */
+    public int freezeBinder(int pid, boolean freeze, int timeoutMs) {
+        return nativeFreezeBinder(pid, freeze, timeoutMs);
+    }
+
+    /**
+     * Retrieves binder freeze info about a process.
+     * @param pid the pid for which binder freeze info is to be retrieved.
+     *
+     * @throws RuntimeException if the operation could not complete successfully.
+     * @return a bit field reporting the binder freeze info for the process.
+     */
+    public int getBinderFreezeInfo(int pid) {
+        return nativeGetBinderFreezeInfo(pid);
+    }
+
+    /**
+     * Determines whether the freezer is supported by this system.
+     * @return true if the freezer is supported.
+     */
+    public boolean isFreezerSupported() {
+        return nativeIsFreezerSupported();
+    }
+
+    // Native methods
+
+    /**
+     * Informs binder that a process is about to be frozen. If freezer is enabled on a process via
+     * this method, this method will synchronously dispatch all pending transactions to the
+     * specified pid. This method will not add significant latencies when unfreezing.
+     * After freezing binder calls, binder will block all transaction to the frozen pid, and return
+     * an error to the sending process.
+     *
+     * @param pid the target pid for which binder transactions are to be frozen
+     * @param freeze specifies whether to flush transactions and then freeze (true) or unfreeze
+     * binder for the specified pid.
+     * @param timeoutMs the timeout in milliseconds to wait for the binder interface to freeze
+     * before giving up.
+     *
+     * @throws RuntimeException in case a flush/freeze operation could not complete successfully.
+     * @return 0 if success, or -EAGAIN indicating there's pending transaction.
+     */
+    private static native int nativeFreezeBinder(int pid, boolean freeze, int timeoutMs);
+
+    /**
+     * Retrieves binder freeze info about a process.
+     * @param pid the pid for which binder freeze info is to be retrieved.
+     *
+     * @throws RuntimeException if the operation could not complete successfully.
+     * @return a bit field reporting the binder freeze info for the process.
+     */
+    private static native int nativeGetBinderFreezeInfo(int pid);
+
+    /**
+     * Return 0 if the freezer is supported on this platform and -1 otherwise.
+     */
+    private static native boolean nativeIsFreezerSupported();
+}
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 726e827..bb0c24b 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -3005,7 +3005,7 @@
         return freezePackageCgroup(packageUID, false);
     }
 
-    private static void freezeBinderAndPackageCgroup(List<Pair<ProcessRecord, Boolean>> procs,
+    private void freezeBinderAndPackageCgroup(List<Pair<ProcessRecord, Boolean>> procs,
                                                      int packageUID) {
         // Freeze all binder processes under the target UID (whose cgroup is about to be frozen).
         // Since we're going to kill these, we don't need to unfreze them later.
@@ -3019,7 +3019,7 @@
                 try {
                     int rc;
                     do {
-                        rc = CachedAppOptimizer.freezeBinder(pid, true, 10 /* timeout_ms */);
+                        rc = mService.getFreezer().freezeBinder(pid, true, 10 /* timeout_ms */);
                     } while (rc == -EAGAIN && nRetries++ < 1);
                     if (rc != 0) Slog.e(TAG, "Unable to freeze binder for " + pid + ": " + rc);
                 } catch (RuntimeException e) {
diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
index e242164..e0aa9bf 100644
--- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
+++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
@@ -21,11 +21,13 @@
 import static com.android.server.grammaticalinflection.GrammaticalInflectionUtils.checkSystemGrammaticalGenderPermission;
 
 import android.annotation.Nullable;
+import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.GrammaticalInflectionManager;
 import android.app.IGrammaticalInflectionManager;
 import android.content.AttributionSource;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.res.Configuration;
 import android.os.Binder;
@@ -36,6 +38,7 @@
 import android.os.ShellCallback;
 import android.os.SystemProperties;
 import android.os.Trace;
+import android.os.UserManager;
 import android.permission.PermissionManager;
 import android.util.AtomicFile;
 import android.util.Log;
@@ -271,6 +274,31 @@
                 throw new IllegalArgumentException("Unknown grammatical gender");
             }
 
+            // TODO(b/356895553): Don't allow profiles and background user to change system
+            //  grammaticalinflection
+            if (UserManager.isVisibleBackgroundUsersEnabled()
+                    && mContext.getPackageManager().hasSystemFeature(
+                    PackageManager.FEATURE_AUTOMOTIVE)) {
+                // The check is added only for automotive devices. On automotive devices, it is
+                // possible that multiple users are visible simultaneously using visible background
+                // users. In such cases, it is desired that only the current user (not the visible
+                // background user) can change the GrammaticalInflection of the device.
+                final long origId = Binder.clearCallingIdentity();
+                try {
+                    int currentUser = ActivityManager.getCurrentUser();
+                    if (userId != currentUser) {
+                        Log.w(TAG,
+                                "Only current user is allowed to update GrammaticalInflection if "
+                                        + "visible background users are enabled. Current User"
+                                        + currentUser + ". Calling User: " + userId);
+                        throw new SecurityException("Only current user is allowed to update "
+                                + "GrammaticalInflection.");
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(origId);
+                }
+            }
+
             final File file = getGrammaticalGenderFile(userId);
             synchronized (mLock) {
                 final AtomicFile atomicFile = new AtomicFile(file);
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index 5a9cf03..bd551fb 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -230,9 +230,17 @@
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME1),
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2),
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_COUNTER_RESET),
-                    record -> mPackageManager.checkPermission(
+                    record -> {
+                        final String category = record.getNotification().category;
+                        if (Notification.CATEGORY_ALARM.equals(category)
+                                || Notification.CATEGORY_CAR_EMERGENCY.equals(category)
+                                || Notification.CATEGORY_CAR_WARNING.equals(category)) {
+                            return true;
+                        }
+                        return mPackageManager.checkPermission(
                             permission.RECEIVE_EMERGENCY_BROADCAST,
-                            record.getSbn().getPackageName()) == PERMISSION_GRANTED);
+                            record.getSbn().getPackageName()) == PERMISSION_GRANTED;
+                    });
 
             return new StrategyAvalanche(
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1),
@@ -248,9 +256,17 @@
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME1),
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2),
                     mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_COUNTER_RESET),
-                    record -> mPackageManager.checkPermission(
+                    record -> {
+                        final String category = record.getNotification().category;
+                        if (Notification.CATEGORY_ALARM.equals(category)
+                                || Notification.CATEGORY_CAR_EMERGENCY.equals(category)
+                                || Notification.CATEGORY_CAR_WARNING.equals(category)) {
+                            return true;
+                        }
+                        return mPackageManager.checkPermission(
                             permission.RECEIVE_EMERGENCY_BROADCAST,
-                            record.getSbn().getPackageName()) == PERMISSION_GRANTED);
+                            record.getSbn().getPackageName()) == PERMISSION_GRANTED;
+                    });
         }
     }
 
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index c95be17..21d6c64 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -2725,11 +2725,16 @@
 
         @Override
         void onLongPress(long eventTime) {
-            // Long-press should be triggered only if app doesn't handle it.
-            mDeferredKeyActionExecutor.queueKeyAction(
-                    KeyEvent.KEYCODE_STEM_PRIMARY,
-                    eventTime,
-                    () -> stemPrimaryLongPress(eventTime));
+            if (mLongPressOnStemPrimaryBehavior == LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT) {
+                // Long-press to assistant gesture is not overridable by apps.
+                stemPrimaryLongPress(eventTime);
+            } else {
+                // Other long-press actions should be triggered only if app doesn't handle it.
+                mDeferredKeyActionExecutor.queueKeyAction(
+                        KeyEvent.KEYCODE_STEM_PRIMARY,
+                        eventTime,
+                        () -> stemPrimaryLongPress(eventTime));
+            }
         }
 
         @Override
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index c21f783..331a594 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -1301,7 +1301,7 @@
                 final NetworkStats stats = getUidNetworkStatsSnapshotForTemplateLocked(
                         new NetworkTemplate.Builder(MATCH_PROXY).build(),  /*includeTags=*/false);
                 if (stats != null) {
-                    ret.add(new NetworkStatsExt(sliceNetworkStatsByUidTagAndMetered(stats),
+                    ret.add(new NetworkStatsExt(sliceNetworkStatsByUidAndFgbg(stats),
                             new int[]{TRANSPORT_BLUETOOTH},
                             /*slicedByFgbg=*/true, /*slicedByTag=*/false,
                             /*slicedByMetered=*/false, TelephonyManager.NETWORK_TYPE_UNKNOWN,
diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
index aa63393..24ed1bb 100644
--- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java
+++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
@@ -23,7 +23,6 @@
 import android.app.ActivityManager;
 import android.graphics.Rect;
 import android.os.Environment;
-import android.os.SystemProperties;
 import android.os.Trace;
 import android.util.ArraySet;
 import android.util.IntArray;
@@ -33,7 +32,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
-import com.android.window.flags.Flags;
 
 import java.io.File;
 import java.io.PrintWriter;
@@ -109,7 +107,6 @@
                 !service.mContext
                         .getResources()
                         .getBoolean(com.android.internal.R.bool.config_disableTaskSnapshots)
-                && isSnapshotEnabled()
                 && !ActivityManager.isLowRamDeviceStatic(); // Don't support Android Go
         setSnapshotEnabled(snapshotEnabled);
     }
@@ -121,12 +118,6 @@
         return Math.max(Math.min(config, 1f), 0.1f);
     }
 
-    // TODO remove when enabled
-    static boolean isSnapshotEnabled() {
-        return SystemProperties.getInt("persist.wm.debug.activity_screenshot", 0) != 0
-                || Flags.activitySnapshotByDefault();
-    }
-
     static PersistInfoProvider createPersistInfoProvider(
             WindowManagerService service, BaseAppSnapshotPersister.DirectoryResolver resolver) {
         // Don't persist reduced file, instead we only persist the "HighRes" bitmap which has
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 924f765..48e1079 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -963,8 +963,7 @@
             mWindowManagerService = wms;
             final Context context = wms.mContext;
             mShowWindowlessSurface = context.getResources().getBoolean(
-                    com.android.internal.R.bool.config_predictShowStartingSurface)
-                    && Flags.activitySnapshotByDefault();
+                    com.android.internal.R.bool.config_predictShowStartingSurface);
         }
         private static final int UNKNOWN = 0;
         private static final int TASK_SWITCH = 1;
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index 9fa1a53..f1e94de 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -79,6 +79,7 @@
         "com_android_server_wm_TaskFpsCallbackController.cpp",
         "onload.cpp",
         ":lib_cachedAppOptimizer_native",
+        ":lib_freezer_native",
         ":lib_gameManagerService_native",
         ":lib_oomConnection_native",
         ":lib_anrTimer_native",
@@ -241,6 +242,13 @@
 }
 
 filegroup {
+    name: "lib_freezer_native",
+    srcs: [
+        "com_android_server_am_Freezer.cpp",
+    ],
+}
+
+filegroup {
     name: "lib_gameManagerService_native",
     srcs: [
         "com_android_server_app_GameManagerService.cpp",
diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
index 95e7b19..a91fd08 100644
--- a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
+++ b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
@@ -24,7 +24,6 @@
 #include <android-base/stringprintf.h>
 #include <android-base/unique_fd.h>
 #include <android_runtime/AndroidRuntime.h>
-#include <binder/IPCThreadState.h>
 #include <cutils/compiler.h>
 #include <dirent.h>
 #include <jni.h>
@@ -34,7 +33,6 @@
 #include <meminfo/procmeminfo.h>
 #include <meminfo/sysmeminfo.h>
 #include <nativehelper/JNIHelp.h>
-#include <processgroup/processgroup.h>
 #include <stddef.h>
 #include <stdio.h>
 #include <sys/mman.h>
@@ -63,10 +61,6 @@
 using VmaToAdviseFunc = std::function<int(const Vma&)>;
 using android::base::unique_fd;
 
-#define SYNC_RECEIVED_WHILE_FROZEN (1)
-#define ASYNC_RECEIVED_WHILE_FROZEN (2)
-#define TXNS_PENDING_WHILE_FROZEN (4)
-
 #define MAX_RW_COUNT (INT_MAX & kPageMask)
 
 // Defines the maximum amount of VMAs we can send per process_madvise syscall.
@@ -527,58 +521,6 @@
     compactProcessOrFallback(pid, compactionFlags);
 }
 
-static jint com_android_server_am_CachedAppOptimizer_freezeBinder(JNIEnv* env, jobject clazz,
-                                                                  jint pid, jboolean freeze,
-                                                                  jint timeout_ms) {
-    jint retVal = IPCThreadState::freeze(pid, freeze, timeout_ms);
-    if (retVal != 0 && retVal != -EAGAIN) {
-        jniThrowException(env, "java/lang/RuntimeException", "Unable to freeze/unfreeze binder");
-    }
-
-    return retVal;
-}
-
-static jint com_android_server_am_CachedAppOptimizer_getBinderFreezeInfo(JNIEnv *env,
-        jobject clazz, jint pid) {
-    uint32_t syncReceived = 0, asyncReceived = 0;
-
-    int error = IPCThreadState::getProcessFreezeInfo(pid, &syncReceived, &asyncReceived);
-
-    if (error < 0) {
-        jniThrowException(env, "java/lang/RuntimeException", strerror(error));
-    }
-
-    jint retVal = 0;
-
-    // bit 0 of sync_recv goes to bit 0 of retVal
-    retVal |= syncReceived & SYNC_RECEIVED_WHILE_FROZEN;
-    // bit 0 of async_recv goes to bit 1 of retVal
-    retVal |= (asyncReceived << 1) & ASYNC_RECEIVED_WHILE_FROZEN;
-    // bit 1 of sync_recv goes to bit 2 of retVal
-    retVal |= (syncReceived << 1) & TXNS_PENDING_WHILE_FROZEN;
-
-    return retVal;
-}
-
-static jstring com_android_server_am_CachedAppOptimizer_getFreezerCheckPath(JNIEnv* env,
-                                                                            jobject clazz) {
-    std::string path;
-
-    if (!getAttributePathForTask("FreezerState", getpid(), &path)) {
-        path = "";
-    }
-
-    return env->NewStringUTF(path.c_str());
-}
-
-static jboolean com_android_server_am_CachedAppOptimizer_isFreezerProfileValid(JNIEnv* env) {
-    uid_t uid = getuid();
-    pid_t pid = getpid();
-
-    return isProfileValidForProcess("Frozen", uid, pid) &&
-            isProfileValidForProcess("Unfrozen", uid, pid);
-}
-
 static const JNINativeMethod sMethods[] = {
         /* name, signature, funcPtr */
         {"cancelCompaction", "()V",
@@ -592,13 +534,7 @@
          (void*)com_android_server_am_CachedAppOptimizer_getMemoryFreedCompaction},
         {"compactSystem", "()V", (void*)com_android_server_am_CachedAppOptimizer_compactSystem},
         {"compactProcess", "(II)V", (void*)com_android_server_am_CachedAppOptimizer_compactProcess},
-        {"freezeBinder", "(IZI)I", (void*)com_android_server_am_CachedAppOptimizer_freezeBinder},
-        {"getBinderFreezeInfo", "(I)I",
-         (void*)com_android_server_am_CachedAppOptimizer_getBinderFreezeInfo},
-        {"getFreezerCheckPath", "()Ljava/lang/String;",
-         (void*)com_android_server_am_CachedAppOptimizer_getFreezerCheckPath},
-        {"isFreezerProfileValid", "()Z",
-         (void*)com_android_server_am_CachedAppOptimizer_isFreezerProfileValid}};
+};
 
 int register_android_server_am_CachedAppOptimizer(JNIEnv* env)
 {
diff --git a/services/core/jni/com_android_server_am_Freezer.cpp b/services/core/jni/com_android_server_am_Freezer.cpp
new file mode 100644
index 0000000..8148728
--- /dev/null
+++ b/services/core/jni/com_android_server_am_Freezer.cpp
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "Freezer"
+//#define LOG_NDEBUG 0
+#define ATRACE_TAG ATRACE_TAG_ACTIVITY_MANAGER
+
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <android-base/logging.h>
+#include <android-base/unique_fd.h>
+#include <binder/IPCThreadState.h>
+#include <nativehelper/JNIHelp.h>
+#include <processgroup/processgroup.h>
+
+namespace android {
+namespace {
+
+// Binder status bit flags.
+static const int SYNC_RECEIVED_WHILE_FROZEN = 1;
+static const int ASYNC_RECEIVED_WHILE_FROZEN = 2;
+static const int TXNS_PENDING_WHILE_FROZEN = 4;
+
+jint freezeBinder(JNIEnv* env, jobject, jint pid, jboolean freeze, jint timeout_ms) {
+    jint retVal = IPCThreadState::freeze(pid, freeze, timeout_ms);
+    if (retVal != 0 && retVal != -EAGAIN) {
+        jniThrowException(env, "java/lang/RuntimeException", "Unable to freeze/unfreeze binder");
+    }
+
+    return retVal;
+}
+
+jint getBinderFreezeInfo(JNIEnv *env, jobject, jint pid) {
+    uint32_t syncReceived = 0, asyncReceived = 0;
+
+    int error = IPCThreadState::getProcessFreezeInfo(pid, &syncReceived, &asyncReceived);
+
+    if (error < 0) {
+        jniThrowException(env, "java/lang/RuntimeException", strerror(error));
+    }
+
+    jint retVal = 0;
+
+    // bit 0 of sync_recv goes to bit 0 of retVal
+    retVal |= syncReceived & SYNC_RECEIVED_WHILE_FROZEN;
+    // bit 0 of async_recv goes to bit 1 of retVal
+    retVal |= (asyncReceived << 1) & ASYNC_RECEIVED_WHILE_FROZEN;
+    // bit 1 of sync_recv goes to bit 2 of retVal
+    retVal |= (syncReceived << 1) & TXNS_PENDING_WHILE_FROZEN;
+
+    return retVal;
+}
+
+bool isFreezerSupported(JNIEnv *env, jclass) {
+    std::string path;
+    if (!getAttributePathForTask("FreezerState", getpid(), &path)) {
+        ALOGI("No attribute for FreezerState");
+        return false;
+    }
+    base::unique_fd fid(open(path.c_str(), O_RDONLY));
+    if (fid < 0) {
+        ALOGI("Cannot open freezer path \"%s\": %s", path.c_str(), strerror(errno));
+        return false;
+    }
+
+    char state;
+    if (::read(fid, &state, 1) != 1) {
+        ALOGI("Failed to read freezer state: %s", strerror(errno));
+        return false;
+    }
+    if (state != '1' && state != '0') {
+        ALOGE("Unexpected value in cgroup.freeze: %d", state);
+        return false;
+    }
+
+    uid_t uid = getuid();
+    pid_t pid = getpid();
+
+    uint32_t syncReceived = 0, asyncReceived = 0;
+    int error = IPCThreadState::getProcessFreezeInfo(pid, &syncReceived, &asyncReceived);
+    if (error < 0) {
+        ALOGE("Unable to read freezer info: %s", strerror(errno));
+        return false;
+    }
+
+    if (!isProfileValidForProcess("Frozen", uid, pid)
+            || !isProfileValidForProcess("Unfrozen", uid, pid)) {
+        ALOGE("Missing freezer profiles");
+        return false;
+    }
+
+    return true;
+}
+
+static const JNINativeMethod sMethods[] = {
+    {"nativeIsFreezerSupported",    "()Z",       (void*) isFreezerSupported },
+    {"nativeFreezeBinder",          "(IZI)I",    (void*) freezeBinder },
+    {"nativeGetBinderFreezeInfo",   "(I)I",      (void*) getBinderFreezeInfo },
+};
+
+} // end of anonymous namespace
+
+int register_android_server_am_Freezer(JNIEnv* env)
+{
+    char const *className = "com/android/server/am/Freezer";
+    return jniRegisterNativeMethods(env, className, sMethods, NELEM(sMethods));
+}
+
+} // end of namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 314ff9d..3c55d18 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -54,6 +54,7 @@
 int register_android_hardware_display_DisplayViewport(JNIEnv* env);
 int register_android_server_am_OomConnection(JNIEnv* env);
 int register_android_server_am_CachedAppOptimizer(JNIEnv* env);
+int register_android_server_am_Freezer(JNIEnv* env);
 int register_android_server_am_LowMemDetector(JNIEnv* env);
 int register_android_server_utils_AnrTimer(JNIEnv *env);
 int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(JNIEnv* env);
@@ -118,6 +119,7 @@
     register_android_hardware_display_DisplayViewport(env);
     register_android_server_am_OomConnection(env);
     register_android_server_am_CachedAppOptimizer(env);
+    register_android_server_am_Freezer(env);
     register_android_server_am_LowMemDetector(env);
     register_android_server_utils_AnrTimer(env);
     register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(env);
diff --git a/services/tests/mockingservicestests/jni/Android.bp b/services/tests/mockingservicestests/jni/Android.bp
index 1eb9888..00543a8 100644
--- a/services/tests/mockingservicestests/jni/Android.bp
+++ b/services/tests/mockingservicestests/jni/Android.bp
@@ -21,6 +21,7 @@
 
     srcs: [
         ":lib_cachedAppOptimizer_native",
+        ":lib_freezer_native",
         ":lib_gameManagerService_native",
         ":lib_oomConnection_native",
         "onload.cpp",
diff --git a/services/tests/mockingservicestests/jni/onload.cpp b/services/tests/mockingservicestests/jni/onload.cpp
index fb91051..cb246d1 100644
--- a/services/tests/mockingservicestests/jni/onload.cpp
+++ b/services/tests/mockingservicestests/jni/onload.cpp
@@ -25,6 +25,7 @@
 
 namespace android {
 int register_android_server_am_CachedAppOptimizer(JNIEnv* env);
+int register_android_server_am_Freezer(JNIEnv* env);
 int register_android_server_app_GameManagerService(JNIEnv* env);
 int register_android_server_am_OomConnection(JNIEnv* env);
 };
@@ -42,8 +43,8 @@
     }
     ALOG_ASSERT(env, "Could not retrieve the env!");
     register_android_server_am_CachedAppOptimizer(env);
+    register_android_server_am_Freezer(env);
     register_android_server_app_GameManagerService(env);
     register_android_server_am_OomConnection(env);
     return JNI_VERSION_1_4;
 }
-
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java
index 03439e55..32ff569 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java
@@ -22,8 +22,16 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -33,12 +41,14 @@
 import android.os.HandlerThread;
 import android.os.MessageQueue;
 import android.os.Process;
+import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
 import android.provider.DeviceConfig;
 import android.text.TextUtils;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.modules.utils.testing.TestableDeviceConfig;
 import com.android.server.LocalServices;
@@ -55,9 +65,11 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -79,12 +91,17 @@
     private CountDownLatch mCountDown;
     private ActivityManagerService mAms;
     private Context mContext;
+    private TestFreezer mFreezer;
+    private CountDownLatch mFreezeCounter;
     private TestInjector mInjector;
     private TestProcessDependencies mProcessDependencies;
 
     @Mock
     private PackageManagerInternal mPackageManagerInt;
 
+    // Control whether the freezer mock reports that freezing is enabled or not.
+    private boolean mUseFreezer;
+
     @Rule
     public final ApplicationExitInfoTest.ServiceThreadRule
             mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule();
@@ -103,9 +120,12 @@
                 true /* allowIo */);
         mThread.start();
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
+
+        mUseFreezer = false;
+        mFreezer = new TestFreezer();
+
         mInjector = new TestInjector(mContext);
-        mAms = new ActivityManagerService(
-                new TestInjector(mContext), mServiceThreadRule.getThread());
+        mAms = new ActivityManagerService(mInjector, mServiceThreadRule.getThread());
         doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
         mProcessDependencies = new TestProcessDependencies();
         mCachedAppOptimizerUnderTest = new CachedAppOptimizer(mAms,
@@ -126,6 +146,7 @@
         mHandlerThread.quit();
         mThread.quit();
         mCountDown = null;
+        mFreezeCounter = null;
     }
 
     private ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, String processName,
@@ -179,7 +200,7 @@
         assertThat(mCachedAppOptimizerUnderTest.mProcStateThrottle)
                 .containsExactlyElementsIn(expected);
 
-        Assume.assumeTrue(mCachedAppOptimizerUnderTest.isFreezerSupported());
+        Assume.assumeTrue(mAms.isAppFreezerSupported());
         assertThat(mCachedAppOptimizerUnderTest.useFreezer()).isEqualTo(
                 CachedAppOptimizer.DEFAULT_USE_FREEZER);
     }
@@ -265,8 +286,8 @@
                 CachedAppOptimizer.DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB + 1);
         assertThat(mCachedAppOptimizerUnderTest.mProcStateThrottle).containsExactly(1, 2, 3);
 
-        Assume.assumeTrue(CachedAppOptimizer.isFreezerSupported());
-        if (CachedAppOptimizer.isFreezerSupported()) {
+        Assume.assumeTrue(mAms.isAppFreezerSupported());
+        if (mAms.isAppFreezerSupported()) {
             if (CachedAppOptimizer.DEFAULT_USE_FREEZER) {
                 assertThat(mCachedAppOptimizerUnderTest.useFreezer()).isFalse();
             } else {
@@ -300,7 +321,7 @@
 
     @Test
     public void useFreeze_doesNotListenToDeviceConfigChanges() throws InterruptedException {
-        Assume.assumeTrue(CachedAppOptimizer.isFreezerSupported());
+        Assume.assumeTrue(mAms.isAppFreezerSupported());
 
         assertThat(mCachedAppOptimizerUnderTest.useFreezer()).isFalse();
 
@@ -353,7 +374,7 @@
 
     @Test
     public void useFreeze_listensToDeviceConfigChangesBadValues() throws InterruptedException {
-        Assume.assumeTrue(CachedAppOptimizer.isFreezerSupported());
+        Assume.assumeTrue(mAms.isAppFreezerSupported());
         assertThat(mCachedAppOptimizerUnderTest.useFreezer()).isFalse();
 
         // When we push an invalid flag value...
@@ -982,6 +1003,40 @@
         }
     }
 
+    @Test
+    public void testFreezerDelegator() throws Exception {
+        mUseFreezer = true;
+        mProcessDependencies.setRss(new long[] {
+                    0 /*total_rss*/,
+                    0 /*file*/,
+                    0 /*anon*/,
+                    0 /*swap*/,
+                    0 /*shmem*/
+                });
+
+        // Force the system to use the freezer
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+                CachedAppOptimizer.KEY_USE_FREEZER, "true", false);
+        mCachedAppOptimizerUnderTest.init();
+        initActivityManagerService();
+
+        assertTrue(mAms.isAppFreezerSupported());
+        assertThat(mCachedAppOptimizerUnderTest.useFreezer()).isTrue();
+
+        int pid = 10000;
+        int uid = 2;
+        int pkgUid = 3;
+        ProcessRecord app = makeProcessRecord(pid, uid, pkgUid, "p1", "app1");
+
+        mFreezeCounter = new CountDownLatch(1);
+        mCachedAppOptimizerUnderTest.forceFreezeForTest(app, true);
+        assertTrue(mFreezeCounter.await(5, TimeUnit.SECONDS));
+
+        mFreezeCounter = new CountDownLatch(1);
+        mCachedAppOptimizerUnderTest.forceFreezeForTest(app, false);
+        assertTrue(mFreezeCounter.await(5, TimeUnit.SECONDS));
+    }
+
     private void setFlag(String key, String value, boolean defaultValue) throws Exception {
         mCountDown = new CountDownLatch(1);
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, key, value, defaultValue);
@@ -1042,6 +1097,11 @@
         public Handler getUiHandler(ActivityManagerService service) {
             return mHandler;
         }
+
+        @Override
+        public Freezer getFreezer() {
+            return mFreezer;
+        }
     }
 
     // Test implementation for ProcessDependencies.
@@ -1069,4 +1129,27 @@
             mRssAfterCompaction = newValues;
         }
     }
+
+    // Intercept Freezer calls.
+    private class TestFreezer extends Freezer {
+        @Override
+        public void setProcessFrozen(int pid, int uid, boolean frozen) {
+            mFreezeCounter.countDown();
+        }
+
+        @Override
+        public int freezeBinder(int pid, boolean freeze, int timeoutMs) {
+            return 0;
+        }
+
+        @Override
+        public int getBinderFreezeInfo(int pid) {
+            return 0;
+        }
+
+        @Override
+        public boolean isFreezerSupported() {
+            return mUseFreezer;
+        }
+    }
 }
diff --git a/services/tests/servicestests/jni/Android.bp b/services/tests/servicestests/jni/Android.bp
index c30e4eb..0a31037 100644
--- a/services/tests/servicestests/jni/Android.bp
+++ b/services/tests/servicestests/jni/Android.bp
@@ -21,6 +21,7 @@
 
     srcs: [
         ":lib_cachedAppOptimizer_native",
+        ":lib_freezer_native",
         ":lib_gameManagerService_native",
         ":lib_oomConnection_native",
         ":lib_anrTimer_native",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
index 7b71f85..1426d5d 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
@@ -48,6 +48,7 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.IntentFilter;
+import android.content.res.Resources;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.Region;
@@ -67,6 +68,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.R;
 import com.android.internal.util.ConcurrentUtils;
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.LocalServices;
@@ -118,6 +120,7 @@
     final FullScreenMagnificationController.ControllerContext mMockControllerCtx =
             mock(FullScreenMagnificationController.ControllerContext.class);
     final Context mMockContext = mock(Context.class);
+    final Resources mMockResources = mock(Resources.class);
     final AccessibilityTraceManager mMockTraceManager = mock(AccessibilityTraceManager.class);
     final WindowManagerInternal mMockWindowManager = mock(WindowManagerInternal.class);
     private final MagnificationAnimationCallback mAnimationCallback = mock(
@@ -162,6 +165,7 @@
         mResolver = new MockContentResolver();
         mResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
         when(mMockContext.getContentResolver()).thenReturn(mResolver);
+        when(mMockContext.getResources()).thenReturn(mMockResources);
         mOriginalMagnificationPersistedScale = Settings.Secure.getFloatForUser(mResolver,
                 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 2.0f,
                 CURRENT_USER_ID);
@@ -928,7 +932,8 @@
                     /* displayId= */ i,
                     /* isMagnifierActivated= */ true,
                     /* isAlwaysOnEnabled= */ false,
-                    /* expectedActivated= */ false);
+                    /* expectedActivated= */ false,
+                    /* expectedMagnified= */ false);
             resetMockWindowManager();
         }
     }
@@ -940,7 +945,24 @@
                     /* displayId= */ i,
                     /* isMagnifierActivated= */ true,
                     /* isAlwaysOnEnabled= */ true,
-                    /* expectedActivated= */ true);
+                    /* expectedActivated= */ true,
+                    /* expectedMagnified= */ false);
+            resetMockWindowManager();
+        }
+    }
+
+    @Test
+    public void testUserContextChange_magnifierActivatedAndKeepMagnifiedEnabled_stayActivated() {
+        when(mMockResources.getBoolean(
+                R.bool.config_magnification_keep_zoom_level_when_context_changed))
+                .thenReturn(true);
+        for (int i = 0; i < DISPLAY_COUNT; i++) {
+            contextChange_expectedValues(
+                    /* displayId= */ i,
+                    /* isMagnifierActivated= */ true,
+                    /* isAlwaysOnEnabled= */ true,
+                    /* expectedActivated= */ true,
+                    /* expectedMagnified= */ true);
             resetMockWindowManager();
         }
     }
@@ -952,7 +974,8 @@
                     /* displayId= */ i,
                     /* isMagnifierActivated= */ false,
                     /* isAlwaysOnEnabled= */ false,
-                    /* expectedActivated= */ false);
+                    /* expectedActivated= */ false,
+                    /* expectedMagnified= */ false);
             resetMockWindowManager();
         }
     }
@@ -964,14 +987,15 @@
                     /* displayId= */ i,
                     /* isMagnifierActivated= */ false,
                     /* isAlwaysOnEnabled= */ true,
-                    /* expectedActivated= */ false);
+                    /* expectedActivated= */ false,
+                    /* expectedMagnified= */ false);
             resetMockWindowManager();
         }
     }
 
     private void contextChange_expectedValues(
             int displayId, boolean isMagnifierActivated, boolean isAlwaysOnEnabled,
-            boolean expectedActivated) {
+            boolean expectedActivated, boolean expectedMagnified) {
         mFullScreenMagnificationController.setAlwaysOnMagnificationEnabled(isAlwaysOnEnabled);
         register(displayId);
         MagnificationCallbacks callbacks = getMagnificationCallbacks(displayId);
@@ -982,7 +1006,7 @@
         callbacks.onUserContextChanged();
         mMessageCapturingHandler.sendAllMessages();
         checkActivatedAndMagnifying(
-                /* activated= */ expectedActivated, /* magnifying= */ false, displayId);
+                /* activated= */ expectedActivated, expectedMagnified, displayId);
 
         if (expectedActivated) {
             verify(mMockThumbnail, times(2)).setThumbnailBounds(
@@ -1526,8 +1550,8 @@
     private void checkActivatedAndMagnifying(boolean activated, boolean magnifying, int displayId) {
         final boolean isActivated = mFullScreenMagnificationController.isActivated(displayId);
         final boolean isMagnifying = mFullScreenMagnificationController.getScale(displayId) > 1.0f;
-        assertTrue(isActivated == activated);
-        assertTrue(isMagnifying == magnifying);
+        assertEquals(isActivated, activated);
+        assertEquals(isMagnifying, magnifying);
     }
 
     private MagnificationCallbacks getMagnificationCallbacks(int displayId) {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index e06d939..de70280 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -2454,6 +2454,50 @@
     }
 
     @Test
+    public void testBeepVolume_politeNotif_Avalanche_exemptCategories() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE);
+        TestableFlagResolver flagResolver = new TestableFlagResolver();
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+        initAttentionHelper(flagResolver);
+
+        // Trigger avalanche trigger intent
+        final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        intent.putExtra("state", false);
+        mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
+
+        // CATEGORY_ALARM is exempted
+        NotificationRecord r = getBeepyNotification();
+        r.getNotification().category = Notification.CATEGORY_ALARM;
+        // Should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        verifyBeepVolume(1.0f);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+
+        // CATEGORY_CAR_EMERGENCY is exempted
+        Mockito.reset(mRingtonePlayer);
+        NotificationRecord r2 = getBeepyNotification();
+        r2.getNotification().category = Notification.CATEGORY_CAR_EMERGENCY;
+        // Should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        verifyBeepVolume(1.0f);
+        assertNotEquals(-1, r2.getLastAudiblyAlertedMs());
+
+        // CATEGORY_CAR_WARNING is exempted
+        Mockito.reset(mRingtonePlayer);
+        NotificationRecord r3 = getBeepyNotification();
+        r3.getNotification().category = Notification.CATEGORY_CAR_WARNING;
+        // Should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+        verifyBeepVolume(1.0f);
+        assertNotEquals(-1, r3.getLastAudiblyAlertedMs());
+
+        verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
+    }
+
+    @Test
     public void testBeepVolume_politeNotif_exemptEmergency() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
         mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
@@ -2492,6 +2536,73 @@
     }
 
     @Test
+    public void testBeepVolume_politeNotif_exemptCategories() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE);
+        TestableFlagResolver flagResolver = new TestableFlagResolver();
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+        // NOTIFICATION_COOLDOWN_ALL setting is enabled
+        Settings.System.putInt(getContext().getContentResolver(),
+                Settings.System.NOTIFICATION_COOLDOWN_ALL, 1);
+        initAttentionHelper(flagResolver);
+
+        // CATEGORY_ALARM is exempted
+        NotificationRecord r = getBeepyNotification();
+        r.getNotification().category = Notification.CATEGORY_ALARM;
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        Mockito.reset(mRingtonePlayer);
+
+        // update should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        // 2nd update should beep at 100% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        // CATEGORY_CAR_WARNING is exempted
+        r = getBeepyNotification();
+        r.getNotification().category = Notification.CATEGORY_CAR_WARNING;
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        Mockito.reset(mRingtonePlayer);
+
+        // update should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        // 2nd update should beep at 100% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        // CATEGORY_CAR_EMERGENCY is exempted
+        r = getBeepyNotification();
+        r.getNotification().category = Notification.CATEGORY_CAR_EMERGENCY;
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        Mockito.reset(mRingtonePlayer);
+
+        // update should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        // 2nd update should beep at 100% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        verify(mAccessibilityService, times(9)).sendAccessibilityEvent(any(), anyInt());
+    }
+
+    @Test
     public void testBeepVolume_politeNotif_applyPerApp() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
         mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index f7340ab..3c3c2f3 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -19,12 +19,18 @@
 import static android.app.AutomaticZenRule.TYPE_BEDTIME;
 import static android.app.Flags.FLAG_MODES_UI;
 import static android.app.Flags.modesUi;
+import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
+import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT;
+import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS;
+import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
+import static android.app.NotificationManager.Policy.suppressedEffectsToString;
 import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
 import static android.provider.Settings.Global.ZEN_MODE_OFF;
 import static android.service.notification.Condition.SOURCE_UNKNOWN;
 import static android.service.notification.Condition.SOURCE_USER_ACTION;
 import static android.service.notification.Condition.STATE_FALSE;
 import static android.service.notification.Condition.STATE_TRUE;
+import static android.service.notification.NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON;
 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE;
 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
@@ -219,8 +225,8 @@
         priorityCategories |= Policy.PRIORITY_CATEGORY_REMINDERS;
         priorityCategories |= Policy.PRIORITY_CATEGORY_EVENTS;
         priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS;
-        suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_LIGHTS;
-        suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_AMBIENT;
+        suppressedVisualEffects |= SUPPRESSED_EFFECT_LIGHTS;
+        suppressedVisualEffects |= SUPPRESSED_EFFECT_AMBIENT;
 
         Policy expectedPolicy = new Policy(priorityCategories, priorityCallSenders,
                 priorityMessageSenders, suppressedVisualEffects, 0, priorityConversationsSenders);
@@ -256,8 +262,8 @@
         priorityCategories |= Policy.PRIORITY_CATEGORY_REMINDERS;
         priorityCategories |= Policy.PRIORITY_CATEGORY_EVENTS;
         priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS;
-        suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_LIGHTS;
-        suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_AMBIENT;
+        suppressedVisualEffects |= SUPPRESSED_EFFECT_LIGHTS;
+        suppressedVisualEffects |= SUPPRESSED_EFFECT_AMBIENT;
 
         Policy expectedPolicy = new Policy(priorityCategories, priorityCallSenders,
                 priorityMessageSenders, suppressedVisualEffects,
@@ -309,8 +315,8 @@
             config.setAllowMessagesFrom(Policy.PRIORITY_SENDERS_STARRED);
             config.setAllowConversationsFrom(CONVERSATION_SENDERS_NONE);
             config.setSuppressedVisualEffects(config.getSuppressedVisualEffects()
-                    | Policy.SUPPRESSED_EFFECT_BADGE | Policy.SUPPRESSED_EFFECT_LIGHTS
-                    | Policy.SUPPRESSED_EFFECT_AMBIENT);
+                    | Policy.SUPPRESSED_EFFECT_BADGE | SUPPRESSED_EFFECT_LIGHTS
+                    | SUPPRESSED_EFFECT_AMBIENT);
         }
         ZenPolicy actual = config.getZenPolicy();
 
@@ -357,8 +363,8 @@
             config.setAllowConversationsFrom(CONVERSATION_SENDERS_NONE);
             config.setAllowPriorityChannels(false);
             config.setSuppressedVisualEffects(config.getSuppressedVisualEffects()
-                    | Policy.SUPPRESSED_EFFECT_BADGE | Policy.SUPPRESSED_EFFECT_LIGHTS
-                    | Policy.SUPPRESSED_EFFECT_AMBIENT);
+                    | Policy.SUPPRESSED_EFFECT_BADGE | SUPPRESSED_EFFECT_LIGHTS
+                    | SUPPRESSED_EFFECT_AMBIENT);
         }
         ZenPolicy actual = config.getZenPolicy();
 
@@ -1063,6 +1069,43 @@
                 .isEqualTo("name");
     }
 
+    @Test
+    public void toNotificationPolicy_withNewSuppressedEffects_returnsSuppressedEffects() {
+        ZenModeConfig config = getCustomConfig();
+        // From LegacyNotificationManagerTest.testSetNotificationPolicy_preP_setNewFields
+        // When a pre-P app sets SUPPRESSED_EFFECT_NOTIFICATION_LIST, it's converted by NMS into:
+        Policy policy = new Policy(0, 0, 0,
+                SUPPRESSED_EFFECT_FULL_SCREEN_INTENT | SUPPRESSED_EFFECT_LIGHTS
+                        | SUPPRESSED_EFFECT_PEEK | SUPPRESSED_EFFECT_AMBIENT);
+
+        config.applyNotificationPolicy(policy);
+        Policy result = config.toNotificationPolicy();
+
+        assertThat(suppressedEffectsOf(result)).isEqualTo(suppressedEffectsOf(policy));
+    }
+
+    @Test
+    public void toNotificationPolicy_withOldAndNewSuppressedEffects_returnsSuppressedEffects() {
+        ZenModeConfig config = getCustomConfig();
+        // From LegacyNotificationManagerTest.testSetNotificationPolicy_preP_setOldNewFields.
+        // When a pre-P app sets SUPPRESSED_EFFECT_SCREEN_ON | SUPPRESSED_EFFECT_STATUS_BAR, it's
+        // converted by NMS into:
+        Policy policy = new Policy(0, 0, 0,
+                SUPPRESSED_EFFECT_SCREEN_ON | SUPPRESSED_EFFECT_FULL_SCREEN_INTENT
+                        | SUPPRESSED_EFFECT_LIGHTS | SUPPRESSED_EFFECT_PEEK
+                        | SUPPRESSED_EFFECT_AMBIENT);
+
+        config.applyNotificationPolicy(policy);
+        Policy result = config.toNotificationPolicy();
+
+        assertThat(suppressedEffectsOf(result)).isEqualTo(suppressedEffectsOf(policy));
+    }
+
+    private static String suppressedEffectsOf(Policy policy) {
+        return suppressedEffectsToString(policy.suppressedVisualEffects) + "("
+                + policy.suppressedVisualEffects + ")";
+    }
+
     private ZenModeConfig getMutedRingerConfig() {
         ZenModeConfig config = new ZenModeConfig();
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
index 57587f7..9af0021 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -61,6 +61,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
@@ -80,19 +81,6 @@
                     ? Set.of("version", "manualRule", "automaticRules", "deletedRules")
                     : Set.of("version", "manualRule", "automaticRules");
 
-    // Differences for flagged fields are only generated if the flag is enabled.
-    // "Metadata" fields (userModifiedFields, deletionInstant, disabledOrigin) are not compared.
-    private static final Set<String> ZEN_RULE_EXEMPT_FIELDS =
-            android.app.Flags.modesApi()
-                    ? Set.of("userModifiedFields", "zenPolicyUserModifiedFields",
-                            "zenDeviceEffectsUserModifiedFields", "deletionInstant",
-                            "disabledOrigin")
-                    : Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION,
-                            RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL,
-                            RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, "userModifiedFields",
-                            "zenPolicyUserModifiedFields", "zenDeviceEffectsUserModifiedFields",
-                            "deletionInstant", "disabledOrigin");
-
     // allowPriorityChannels is flagged by android.app.modes_api
     public static final Set<String> ZEN_MODE_CONFIG_FLAGGED_FIELDS =
             Set.of("allowPriorityChannels");
@@ -102,8 +90,7 @@
 
     @Parameters(name = "{0}")
     public static List<FlagsParameterization> getParams() {
-        return FlagsParameterization.allCombinationsOf(
-                FLAG_MODES_UI);
+        return FlagsParameterization.progressionOf(FLAG_MODES_API, FLAG_MODES_UI);
     }
 
     public ZenModeDiffTest(FlagsParameterization flags) {
@@ -140,7 +127,7 @@
         ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
         ArrayMap<String, Object> expectedTo = new ArrayMap<>();
         List<Field> fieldsForDiff = getFieldsForDiffCheck(
-                ZenModeConfig.ZenRule.class, ZEN_RULE_EXEMPT_FIELDS);
+                ZenModeConfig.ZenRule.class, getZenRuleExemptFields());
         generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo);
 
         ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2);
@@ -158,6 +145,25 @@
         }
     }
 
+    private static Set<String> getZenRuleExemptFields() {
+        // "Metadata" fields are never compared.
+        Set<String> exemptFields = new LinkedHashSet<>(
+                Set.of("userModifiedFields", "zenPolicyUserModifiedFields",
+                        "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin"));
+        // Flagged fields are only compared if their flag is on.
+        if (!Flags.modesApi()) {
+            exemptFields.addAll(
+                    Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION,
+                            RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL,
+                            RuleDiff.FIELD_ZEN_DEVICE_EFFECTS,
+                            RuleDiff.FIELD_LEGACY_SUPPRESSED_EFFECTS));
+        }
+        if (!(Flags.modesApi() && Flags.modesUi())) {
+            exemptFields.add(RuleDiff.FIELD_LEGACY_SUPPRESSED_EFFECTS);
+        }
+        return exemptFields;
+    }
+
     @Test
     public void testConfigDiff_addRemoveSame() {
         // Default config, will test add, remove, and no change
diff --git a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
index eed4b0b..9b92ff4 100644
--- a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
@@ -194,6 +194,26 @@
     }
 
     @Test
+    public void stemLongKey_appHasOverridePermission_consumedByApp_triggerStatusBarToStartAssist() {
+        overrideBehavior(
+                STEM_PRIMARY_BUTTON_LONG_PRESS,
+                LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT);
+        setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
+        mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(false);
+        mPhoneWindowManager.setupAssistForLaunch();
+        mPhoneWindowManager.overrideSearchManager(null);
+        mPhoneWindowManager.overrideStatusBarManagerInternal();
+        mPhoneWindowManager.overrideIsUserSetupComplete(true);
+        mPhoneWindowManager.overrideFocusedWindowButtonOverridePermission(true);
+
+        setDispatchedKeyHandler(keyEvent -> true);
+
+        sendKey(KEYCODE_STEM_PRIMARY, /* longPress= */ true);
+
+        mPhoneWindowManager.assertStatusBarStartAssist();
+    }
+
+    @Test
     public void stemDoubleKey_EarlyShortPress_AllAppsThenSwitchToMostRecent()
             throws RemoteException {
         overrideBehavior(STEM_PRIMARY_BUTTON_SHORT_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS);
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index c6959ae..b9a001d 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -9604,9 +9604,8 @@
      * Defines the rules for data setup retry.
      *
      * The syntax of the retry rule:
-     * 1. Retry based on {@link NetworkCapabilities}. Note that only APN-type network capabilities
-     *    are supported. If the capabilities are not specified, then the retry rule only applies
-     *    to the current failed APN used in setup data call request.
+     * 1. Retry based on {@link NetworkCapabilities}. If the capabilities are not specified, then
+     * the retry rule only applies to the current failed APN used in setup data call request.
      * "capabilities=[netCaps1|netCaps2|...], [retry_interval=n1|n2|n3|n4...], [maximum_retries=n]"
      *
      * 2. Retry based on {@link DataFailCause}