Merge "Clean up outdated path of sleep token for swapping display" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 5e9af6a..a16aa2d 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -411,6 +411,13 @@
     host_supported: true,
 }
 
+cc_aconfig_library {
+    name: "android.companion.virtualdevice.flags-aconfig-cc-test",
+    aconfig_declarations: "android.companion.virtualdevice.flags-aconfig",
+    host_supported: true,
+    mode: "test",
+}
+
 java_aconfig_library {
     name: "android.companion.virtualdevice.flags-aconfig-java",
     aconfig_declarations: "android.companion.virtualdevice.flags-aconfig",
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 0947e33..5a3a8d5 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -21,6 +21,7 @@
 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
 import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
+import static android.app.Instrumentation.DEBUG_FINISH_ACTIVITY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.inMultiWindowMode;
 import static android.os.Process.myUid;
@@ -7297,6 +7298,9 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     private void finish(int finishTask) {
+        if (DEBUG_FINISH_ACTIVITY) {
+            Log.d("Instrumentation", "finishActivity: finishTask=" + finishTask, new Throwable());
+        }
         if (mParent == null) {
             int resultCode;
             Intent resultData;
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index db216b1..be27046 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -107,6 +107,8 @@
     // If set, will print the stack trace for activity starts within the process
     static final boolean DEBUG_START_ACTIVITY = Build.IS_DEBUGGABLE &&
             SystemProperties.getBoolean("persist.wm.debug.start_activity", false);
+    static final boolean DEBUG_FINISH_ACTIVITY = Build.IS_DEBUGGABLE &&
+            SystemProperties.getBoolean("persist.wm.debug.finish_activity", false);
 
     /**
      * @hide
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index b63e2cf..d05d23c 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -103,3 +103,10 @@
     description: "API for on-demand rotation of virtual displays"
     bug: "291748430"
 }
+
+flag {
+    namespace: "virtual_devices"
+    name: "high_resolution_scroll"
+    description: "Enable high resolution scroll"
+    bug: "335160780"
+}
diff --git a/core/java/android/content/ClipDescription.java b/core/java/android/content/ClipDescription.java
index 5953890..93724bb 100644
--- a/core/java/android/content/ClipDescription.java
+++ b/core/java/android/content/ClipDescription.java
@@ -136,6 +136,14 @@
             "android.intent.extra.LOGGING_INSTANCE_ID";
 
     /**
+     * The id of the task containing the window that initiated the drag that should be hidden.
+     * Only provided to internal drag handlers as a part of the DRAG_START event.
+     * @hide
+     */
+    public static final String EXTRA_HIDE_DRAG_SOURCE_TASK_ID =
+            "android.intent.extra.HIDE_DRAG_SOURCE_TASK_ID";
+
+    /**
      * Indicates that a ClipData contains potentially sensitive information, such as a
      * password or credit card number.
      * <p>
diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java
index ba356bb..6514872 100644
--- a/core/java/android/database/CursorWindow.java
+++ b/core/java/android/database/CursorWindow.java
@@ -16,6 +16,8 @@
 
 package android.database;
 
+import static java.util.Objects.requireNonNull;
+
 import android.annotation.BytesLong;
 import android.annotation.IntRange;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -640,6 +642,7 @@
      */
     public boolean putBlob(byte[] value,
             @IntRange(from = 0) int row, @IntRange(from = 0) int column) {
+        requireNonNull(value);
         acquireReference();
         try {
             return nativePutBlob(mWindowPtr, value, row - mStartPos, column);
@@ -658,6 +661,7 @@
      */
     public boolean putString(String value,
             @IntRange(from = 0) int row, @IntRange(from = 0) int column) {
+        requireNonNull(value);
         acquireReference();
         try {
             return nativePutString(mWindowPtr, value, row - mStartPos, column);
diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java
index cbac912..ca3e3d2 100644
--- a/core/java/android/hardware/Camera.java
+++ b/core/java/android/hardware/Camera.java
@@ -569,7 +569,6 @@
             return native_setup(
                     new WeakReference<>(this),
                     cameraId,
-                    ActivityThread.currentOpPackageName(),
                     rotationOverride,
                     forceSlowJpegMode,
                     clientAttribution.getParcel(),
@@ -660,7 +659,6 @@
     private native int native_setup(
             Object cameraThis,
             int cameraId,
-            String packageName,
             int rotationOverride,
             boolean forceSlowJpegMode,
             Parcel clientAttributionParcel,
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 2dbd4b8..6201359 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -980,6 +980,8 @@
         clientAttribution.uid = USE_CALLING_UID;
         clientAttribution.pid = USE_CALLING_PID;
         clientAttribution.deviceId = contextAttribution.deviceId;
+        clientAttribution.packageName = mContext.getOpPackageName();
+        clientAttribution.attributionTag = mContext.getAttributionTag();
         clientAttribution.next = new AttributionSourceState[0];
         return clientAttribution;
     }
@@ -1041,8 +1043,6 @@
                         cameraService.connectDevice(
                                 callbacks,
                                 cameraId,
-                                mContext.getOpPackageName(),
-                                mContext.getAttributionTag(),
                                 oomScoreOffset,
                                 mContext.getApplicationInfo().targetSdkVersion,
                                 rotationOverride,
diff --git a/core/java/android/util/SequenceUtils.java b/core/java/android/util/SequenceUtils.java
index f833ce3..4f8db0f 100644
--- a/core/java/android/util/SequenceUtils.java
+++ b/core/java/android/util/SequenceUtils.java
@@ -25,8 +25,8 @@
  * {@link #getInitSeq}.
  * 2. Whenever a newer info needs to be sent to the client side, the system server should first
  * update its seq with {@link #getNextSeq}, then send the new info with the new seq to the client.
- * 3. On the client side, when receiving a new info, it should only consume it if it is newer than
- * the last received info seq by checking {@link #isIncomingSeqNewer}.
+ * 3. On the client side, when receiving a new info, it should only consume it if it is not stale by
+ * checking {@link #isIncomingSeqStale}.
  *
  * @hide
  */
@@ -36,15 +36,22 @@
     }
 
     /**
-     * Returns {@code true} if the incomingSeq is newer than the curSeq.
+     * Returns {@code true} if the incomingSeq is stale, which means the client should not consume
+     * it.
      */
-    public static boolean isIncomingSeqNewer(int curSeq, int incomingSeq) {
+    public static boolean isIncomingSeqStale(int curSeq, int incomingSeq) {
+        if (curSeq == getInitSeq()) {
+            // curSeq can be set to the initial seq in the following cases:
+            // 1. The client process/field is newly created/recreated.
+            // 2. The field is not managed by the system server, such as WindowlessWindowManager.
+            // The client should always consume the incoming in these cases.
+            return false;
+        }
         // Convert to long for comparison.
         final long diff = (long) incomingSeq - curSeq;
-        // If there has been a sufficiently large jump, assume the sequence has wrapped around.
-        // For example, when the last seq is MAX_VALUE, the incoming seq will be MIN_VALUE + 1.
-        // diff = MIN_VALUE + 1 - MAX_VALUE. It is smaller than 0, but should be treated as newer.
-        return diff > 0 || diff < Integer.MIN_VALUE;
+        // When diff is 0, allow client to consume.
+        // When there has been a sufficiently large jump, assume the sequence has wrapped around.
+        return (diff < 0 && diff > Integer.MIN_VALUE) || diff > Integer.MAX_VALUE;
     }
 
     /** Returns the initial seq. */
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 4766942..f77e219 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -5512,6 +5512,14 @@
     public static final int DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG = 1 << 13;
 
     /**
+     * Flag indicating that this drag will result in the caller activity's task to be hidden for the
+     * duration of the drag, this means that the source activity will not receive drag events for
+     * the current drag gesture. Only the current voice interaction service may use this flag.
+     * @hide
+     */
+    public static final int DRAG_FLAG_HIDE_CALLING_TASK_ON_DRAG_START = 1 << 14;
+
+    /**
      * Vertical scroll factor cached by {@link #getVerticalScrollFactor}.
      */
     private float mVerticalScrollFactor;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 88dc3f4..cdf2788 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -23,7 +23,7 @@
 import static android.os.IInputConstants.INVALID_INPUT_EVENT_ID;
 import static android.os.Trace.TRACE_TAG_VIEW;
 import static android.util.SequenceUtils.getInitSeq;
-import static android.util.SequenceUtils.isIncomingSeqNewer;
+import static android.util.SequenceUtils.isIncomingSeqStale;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.DragEvent.ACTION_DRAG_LOCATION;
@@ -2329,23 +2329,23 @@
         if (mLastReportedFrames == null) {
             return;
         }
-        if (isIncomingSeqNewer(mLastReportedFrames.seq, inOutFrames.seq)) {
+        if (isIncomingSeqStale(mLastReportedFrames.seq, inOutFrames.seq)) {
+            // If the incoming is stale, use the last reported instead.
+            inOutFrames.setTo(mLastReportedFrames);
+        } else {
             // Keep track of the latest.
             mLastReportedFrames.setTo(inOutFrames);
-        } else {
-            // If the last reported frames is newer, use the last reported instead.
-            inOutFrames.setTo(mLastReportedFrames);
         }
     }
 
     private void onInsetsStateChanged(@NonNull InsetsState insetsState) {
         if (insetsControlSeq()) {
-            if (isIncomingSeqNewer(mLastReportedInsetsStateSeq, insetsState.getSeq())) {
-                mLastReportedInsetsStateSeq = insetsState.getSeq();
-            } else {
-                // The last reported InsetsState is newer. Skip.
+            if (isIncomingSeqStale(mLastReportedInsetsStateSeq, insetsState.getSeq())) {
+                // The incoming is stale. Skip.
                 return;
             }
+            // Keep track of the latest.
+            mLastReportedInsetsStateSeq = insetsState.getSeq();
         }
 
         if (mTranslator != null) {
@@ -2362,13 +2362,13 @@
         }
 
         if (insetsControlSeq()) {
-            if (isIncomingSeqNewer(mLastReportedActiveControlsSeq, activeControls.getSeq())) {
-                mLastReportedActiveControlsSeq = activeControls.getSeq();
-            } else {
-                // The last reported controls is newer. Skip.
+            if (isIncomingSeqStale(mLastReportedActiveControlsSeq, activeControls.getSeq())) {
+                // The incoming is stale. Skip.
                 activeControls.release();
                 return;
             }
+            // Keep track of the latest.
+            mLastReportedActiveControlsSeq = activeControls.getSeq();
         }
 
         final InsetsSourceControl[] controls = activeControls.get();
@@ -7530,7 +7530,6 @@
                             animationCallback.onBackCancelled();
                         } else {
                             topCallback.onBackInvoked();
-                            return FINISH_HANDLED;
                         }
                         break;
                 }
@@ -7538,14 +7537,16 @@
                 if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
                     if (!keyEvent.isCanceled()) {
                         topCallback.onBackInvoked();
-                        return FINISH_HANDLED;
                     } else {
                         Log.d(mTag, "Skip onBackInvoked(), reason: keyEvent.isCanceled=true");
                     }
                 }
             }
-
-            return FINISH_NOT_HANDLED;
+            if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
+                // forward a cancelled event so that following stages cancel their back logic
+                keyEvent.cancel();
+            }
+            return FORWARD;
         }
 
         @Override
diff --git a/core/java/com/android/internal/os/BinderTransactionNameResolver.java b/core/java/com/android/internal/os/BinderTransactionNameResolver.java
index 5f6f427..f1dc1f2 100644
--- a/core/java/com/android/internal/os/BinderTransactionNameResolver.java
+++ b/core/java/com/android/internal/os/BinderTransactionNameResolver.java
@@ -17,6 +17,7 @@
 package com.android.internal.os;
 
 import android.os.Binder;
+import android.text.TextUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -37,6 +38,8 @@
 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
 public class BinderTransactionNameResolver {
     private static final Method NO_GET_DEFAULT_TRANSACTION_NAME_METHOD;
+    private static final boolean USE_TRANSACTION_CODES_FOR_UNKNOWN_METHODS =
+            Flags.useTransactionCodesForUnknownMethods();
 
     /**
      * Generates the default transaction method name, which is just the transaction code.
@@ -81,7 +84,14 @@
         }
 
         try {
-            return (String) method.invoke(null, transactionCode);
+            String methodName = (String) method.invoke(null, transactionCode);
+            if (USE_TRANSACTION_CODES_FOR_UNKNOWN_METHODS) {
+                return TextUtils.isEmpty(methodName)
+                        ? String.valueOf(transactionCode)
+                        : methodName;
+            } else {
+                return methodName;
+            }
         } catch (IllegalAccessException | InvocationTargetException e) {
             throw new RuntimeException(e);
         }
diff --git a/core/java/com/android/internal/os/flags.aconfig b/core/java/com/android/internal/os/flags.aconfig
index c8d6810..30fa4f1 100644
--- a/core/java/com/android/internal/os/flags.aconfig
+++ b/core/java/com/android/internal/os/flags.aconfig
@@ -9,3 +9,14 @@
     is_fixed_read_only: true
     bug: "241474956"
 }
+
+flag {
+    name: "use_transaction_codes_for_unknown_methods"
+    namespace: "dropbox"
+    description: "Use transaction codes when the method names is unknown"
+    bug: "350041302"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
index 95b6146..d244874 100644
--- a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
@@ -48,7 +48,6 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Map;
 import java.util.TreeMap;
 import java.util.stream.Collectors;
 
@@ -66,7 +65,7 @@
     private final String mLegacyViewerConfigFilename;
     private final TraceBuffer mBuffer;
     private final LegacyProtoLogViewerConfigReader mViewerConfig;
-    private final Map<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
+    private final TreeMap<String, IProtoLogGroup> mLogGroups;
     private final Runnable mCacheUpdater;
     private final int mPerChunkSize;
 
@@ -75,19 +74,20 @@
     private final Object mProtoLogEnabledLock = new Object();
 
     public LegacyProtoLogImpl(String outputFile, String viewerConfigFilename,
-            Runnable cacheUpdater) {
+            TreeMap<String, IProtoLogGroup> logGroups, Runnable cacheUpdater) {
         this(new File(outputFile), viewerConfigFilename, BUFFER_CAPACITY,
-                new LegacyProtoLogViewerConfigReader(), PER_CHUNK_SIZE, cacheUpdater);
+                new LegacyProtoLogViewerConfigReader(), PER_CHUNK_SIZE, logGroups, cacheUpdater);
     }
 
     public LegacyProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity,
             LegacyProtoLogViewerConfigReader viewerConfig, int perChunkSize,
-            Runnable cacheUpdater) {
+            TreeMap<String, IProtoLogGroup> logGroups, Runnable cacheUpdater) {
         mLogFile = file;
         mBuffer = new TraceBuffer(bufferCapacity);
         mLegacyViewerConfigFilename = viewerConfigFilename;
         mViewerConfig = viewerConfig;
         mPerChunkSize = perChunkSize;
+        mLogGroups = logGroups;
         mCacheUpdater = cacheUpdater;
     }
 
@@ -97,25 +97,21 @@
     @VisibleForTesting
     @Override
     public void log(LogLevel level, IProtoLogGroup group, long messageHash, int paramsMask,
-            Object[] args) {
+            @Nullable String messageString, Object[] args) {
         if (group.isLogToProto()) {
             logToProto(messageHash, paramsMask, args);
         }
         if (group.isLogToLogcat()) {
-            logToLogcat(group.getTag(), level, messageHash, args);
+            logToLogcat(group.getTag(), level, messageHash, messageString, args);
         }
     }
 
-    @Override
-    public void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object... args) {
-        // This will be removed very soon so no point implementing it here.
-        throw new IllegalStateException(
-                "Not implemented. Only implemented for PerfettoProtoLogImpl.");
-    }
-
-    private void logToLogcat(String tag, LogLevel level, long messageHash, Object[] args) {
+    private void logToLogcat(String tag, LogLevel level, long messageHash,
+            @Nullable String messageString, Object[] args) {
         String message = null;
-        final String messageString = mViewerConfig.getViewerString(messageHash);
+        if (messageString == null) {
+            messageString = mViewerConfig.getViewerString(messageHash);
+        }
         if (messageString != null) {
             if (args != null) {
                 try {
@@ -414,12 +410,5 @@
         // so we ignore the level argument to this function.
         return group.isLogToLogcat() || (group.isLogToProto() && isProtoEnabled());
     }
-
-    @Override
-    public void registerGroups(IProtoLogGroup... protoLogGroups) {
-        for (IProtoLogGroup group : protoLogGroups) {
-            mLogGroups.put(group.name(), group);
-        }
-    }
 }
 
diff --git a/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java b/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java
deleted file mode 100644
index 37d09c2..0000000
--- a/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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.protolog;
-
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.internal.protolog.common.ILogger;
-import com.android.internal.protolog.common.IProtoLog;
-import com.android.internal.protolog.common.IProtoLogGroup;
-import com.android.internal.protolog.common.LogLevel;
-
-/**
- * Class only create and used to server temporarily for when there is source code pre-processing by
- * the ProtoLog tool, when the tracing to Perfetto flag is off, and the static REQUIRE_PROTOLOGTOOL
- * boolean is false. In which case we simply want to log protolog message to logcat. Note, that this
- * means that in such cases there is no real advantage of using protolog over logcat.
- *
- * @deprecated Should not be used. This is just a temporary class to support a legacy behavior.
- */
-@Deprecated
-public class LogcatOnlyProtoLogImpl implements IProtoLog {
-    @Override
-    public void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask,
-            Object[] args) {
-        throw new RuntimeException("Not supported when using LogcatOnlyProtoLogImpl");
-    }
-
-    @Override
-    public void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object[] args) {
-        String formattedString = TextUtils.formatSimple(messageString, args);
-        switch (logLevel) {
-            case VERBOSE -> Log.v(group.getTag(), formattedString);
-            case INFO -> Log.i(group.getTag(), formattedString);
-            case DEBUG -> Log.d(group.getTag(), formattedString);
-            case WARN -> Log.w(group.getTag(), formattedString);
-            case ERROR -> Log.e(group.getTag(), formattedString);
-            case WTF -> Log.wtf(group.getTag(), formattedString);
-        }
-    }
-
-    @Override
-    public boolean isProtoEnabled() {
-        return false;
-    }
-
-    @Override
-    public int startLoggingToLogcat(String[] groups, ILogger logger) {
-        return 0;
-    }
-
-    @Override
-    public int stopLoggingToLogcat(String[] groups, ILogger logger) {
-        return 0;
-    }
-
-    @Override
-    public boolean isEnabled(IProtoLogGroup group, LogLevel level) {
-        return true;
-    }
-
-    @Override
-    public void registerGroups(IProtoLogGroup... protoLogGroups) {
-        // Does nothing
-    }
-}
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 5f6766e..42fa6ac 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -42,7 +42,6 @@
 import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.SEQ_NEEDS_INCREMENTAL_STATE;
 import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.TIMESTAMP;
 
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData;
 import android.os.ShellCommand;
@@ -73,45 +72,37 @@
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
-import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * A service for the ProtoLog logging system.
  */
 public class PerfettoProtoLogImpl implements IProtoLog {
     private static final String LOG_TAG = "ProtoLog";
-    public static final String NULL_STRING = "null";
     private final AtomicInteger mTracingInstances = new AtomicInteger();
 
     private final ProtoLogDataSource mDataSource = new ProtoLogDataSource(
             this::onTracingInstanceStart,
-            this::onTracingFlush,
+            this::dumpTransitionTraceConfig,
             this::onTracingInstanceStop
     );
     private final ProtoLogViewerConfigReader mViewerConfigReader;
     private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
-    private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
+    private final TreeMap<String, IProtoLogGroup> mLogGroups;
     private final Runnable mCacheUpdater;
 
-    private final int[] mDefaultLogLevelCounts = new int[LogLevel.values().length];
-    private final Map<IProtoLogGroup, int[]> mLogLevelCounts = new ArrayMap<>();
-    private final Map<IProtoLogGroup, Integer> mCollectStackTraceGroupCounts = new ArrayMap<>();
+    private final Map<LogLevel, Integer> mDefaultLogLevelCounts = new ArrayMap<>();
+    private final Map<IProtoLogGroup, Map<LogLevel, Integer>> mLogLevelCounts = new ArrayMap<>();
 
-    private final Lock mBackgroundServiceLock = new ReentrantLock();
-    private ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor();
+    private final ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor();
 
-    public PerfettoProtoLogImpl(String viewerConfigFilePath, Runnable cacheUpdater) {
+    public PerfettoProtoLogImpl(String viewerConfigFilePath,
+            TreeMap<String, IProtoLogGroup> logGroups, Runnable cacheUpdater) {
         this(() -> {
             try {
                 return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
@@ -119,19 +110,16 @@
                 Slog.w(LOG_TAG, "Failed to load viewer config file " + viewerConfigFilePath, e);
                 return null;
             }
-        }, cacheUpdater);
-    }
-
-    public PerfettoProtoLogImpl() {
-        this(null, null, () -> {});
+        }, logGroups, cacheUpdater);
     }
 
     public PerfettoProtoLogImpl(
             ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+            TreeMap<String, IProtoLogGroup> logGroups,
             Runnable cacheUpdater
     ) {
         this(viewerConfigInputStreamProvider,
-                new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider),
+                new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider), logGroups,
                 cacheUpdater);
     }
 
@@ -139,6 +127,7 @@
     public PerfettoProtoLogImpl(
             ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
             ProtoLogViewerConfigReader viewerConfigReader,
+            TreeMap<String, IProtoLogGroup> logGroups,
             Runnable cacheUpdater
     ) {
         Producer.init(InitArguments.DEFAULTS);
@@ -151,6 +140,7 @@
         mDataSource.register(params);
         this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
         this.mViewerConfigReader = viewerConfigReader;
+        this.mLogGroups = logGroups;
         this.mCacheUpdater = cacheUpdater;
     }
 
@@ -159,70 +149,23 @@
      */
     @VisibleForTesting
     @Override
-    public void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask,
-            @Nullable Object[] args) {
-        log(logLevel, group, new Message(messageHash, paramsMask), args);
-    }
+    public void log(LogLevel level, IProtoLogGroup group, long messageHash, int paramsMask,
+            @Nullable String messageString, Object[] args) {
+        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "log");
 
-    @Override
-    public void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object... args) {
-        log(logLevel, group, new Message(messageString), args);
-    }
-
-    private void log(LogLevel logLevel, IProtoLogGroup group, Message message,
-            @Nullable Object[] args) {
-        if (isProtoEnabled()) {
-            long tsNanos = SystemClock.elapsedRealtimeNanos();
-            final String stacktrace;
-            if (mCollectStackTraceGroupCounts.getOrDefault(group, 0) > 0) {
-                stacktrace = collectStackTrace();
-            } else {
-                stacktrace = null;
-            }
-            try {
-                mBackgroundServiceLock.lock();
-                mBackgroundLoggingService.execute(() ->
-                        logToProto(logLevel, group, message, args, tsNanos,
-                                stacktrace));
-            } finally {
-                mBackgroundServiceLock.unlock();
-            }
-        }
-        if (group.isLogToLogcat()) {
-            logToLogcat(group.getTag(), logLevel, message, args);
-        }
-    }
-
-    private void onTracingFlush() {
-        final ExecutorService loggingService;
+        long tsNanos = SystemClock.elapsedRealtimeNanos();
         try {
-            mBackgroundServiceLock.lock();
-            loggingService = mBackgroundLoggingService;
-            mBackgroundLoggingService = Executors.newSingleThreadExecutor();
+            mBackgroundLoggingService.submit(() ->
+                    logToProto(level, group.name(), messageHash, paramsMask, args, tsNanos));
+            if (group.isLogToLogcat()) {
+                logToLogcat(group.getTag(), level, messageHash, messageString, args);
+            }
         } finally {
-            mBackgroundServiceLock.unlock();
+            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
         }
-
-        try {
-            loggingService.shutdown();
-            boolean finished = loggingService.awaitTermination(10, TimeUnit.SECONDS);
-
-            if (!finished) {
-                Log.e(LOG_TAG, "ProtoLog background tracing service didn't finish gracefully.");
-            }
-        } catch (InterruptedException e) {
-            Log.e(LOG_TAG, "Failed to wait for tracing to finish", e);
-        }
-
-        dumpTransitionTraceConfig();
     }
 
     private void dumpTransitionTraceConfig() {
-        if (mViewerConfigInputStreamProvider == null) {
-            // No viewer config available
-            return;
-        }
-
         ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
 
         if (pis == null) {
@@ -313,53 +256,39 @@
         os.end(outMessagesToken);
     }
 
-    private void logToLogcat(String tag, LogLevel level, Message message,
-            @Nullable Object[] args) {
-        String messageString = message.getMessage(mViewerConfigReader);
-
-        if (messageString == null) {
-            StringBuilder builder = new StringBuilder("UNKNOWN MESSAGE");
-            if (args != null) {
-                builder.append(" args = (");
-                builder.append(String.join(", ", Arrays.stream(args)
-                        .map(it -> {
-                            if (it == null) {
-                                return "null";
-                            } else {
-                                return it.toString();
-                            }
-                        }).toList()));
-                builder.append(")");
-            }
-            messageString = builder.toString();
-            args = new Object[0];
-        }
-
-        logToLogcat(tag, level, messageString, args);
-    }
-
-    private void logToLogcat(String tag, LogLevel level, String message, @Nullable Object[] args) {
+    private void logToLogcat(String tag, LogLevel level, long messageHash,
+            @Nullable String messageString, Object[] args) {
         Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logToLogcat");
         try {
-            doLogToLogcat(tag, level, message, args);
+            doLogToLogcat(tag, level, messageHash, messageString, args);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
         }
     }
 
-    private void doLogToLogcat(String tag, LogLevel level, @NonNull String messageString,
-            @Nullable Object[] args) {
-        String message;
-        if (args != null) {
-            try {
-                message = TextUtils.formatSimple(messageString, args);
-            } catch (IllegalArgumentException e) {
-                message = "FORMAT_ERROR \"" + messageString + "\", args=("
-                        + String.join(
-                                ", ", Arrays.stream(args).map(Object::toString).toList()) + ")";
+    private void doLogToLogcat(String tag, LogLevel level, long messageHash,
+            @androidx.annotation.Nullable String messageString, Object[] args) {
+        String message = null;
+        if (messageString == null) {
+            messageString = mViewerConfigReader.getViewerString(messageHash);
+        }
+        if (messageString != null) {
+            if (args != null) {
+                try {
+                    message = TextUtils.formatSimple(messageString, args);
+                } catch (Exception ex) {
+                    Slog.w(LOG_TAG, "Invalid ProtoLog format string.", ex);
+                }
+            } else {
+                message = messageString;
             }
-        } else {
-            message = messageString;
+        }
+        if (message == null) {
+            StringBuilder builder = new StringBuilder("UNKNOWN MESSAGE (" + messageHash + ")");
+            for (Object o : args) {
+                builder.append(" ").append(o);
+            }
+            message = builder.toString();
         }
         passToLogcat(tag, level, message);
     }
@@ -391,21 +320,25 @@
         }
     }
 
-    private void logToProto(LogLevel level, IProtoLogGroup logGroup, Message message, Object[] args,
-            long tsNanos, @Nullable String stacktrace) {
+    private void logToProto(LogLevel level, String groupName, long messageHash, int paramsMask,
+            Object[] args, long tsNanos) {
+        if (!isProtoEnabled()) {
+            return;
+        }
+
         Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logToProto");
         try {
-            doLogToProto(level, logGroup, message, args, tsNanos, stacktrace);
+            doLogToProto(level, groupName, messageHash, paramsMask, args, tsNanos);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
         }
     }
 
-    private void doLogToProto(LogLevel level, IProtoLogGroup logGroup, Message message,
-            Object[] args, long tsNanos, @Nullable String stacktrace) {
+    private void doLogToProto(LogLevel level, String groupName, long messageHash, int paramsMask,
+            Object[] args, long tsNanos) {
         mDataSource.trace(ctx -> {
             final ProtoLogDataSource.TlsState tlsState = ctx.getCustomTlsState();
-            final LogLevel logFrom = tlsState.getLogFromLevel(logGroup.name());
+            final LogLevel logFrom = tlsState.getLogFromLevel(groupName);
 
             if (level.ordinal() < logFrom.ordinal()) {
                 return;
@@ -417,44 +350,30 @@
                 // trace processing easier.
                 int argIndex = 0;
                 for (Object o : args) {
-                    int type = LogDataType.bitmaskToLogDataType(message.getMessageMask(), argIndex);
+                    int type = LogDataType.bitmaskToLogDataType(paramsMask, argIndex);
                     if (type == LogDataType.STRING) {
-                        if (o == null) {
-                            internStringArg(ctx, NULL_STRING);
-                        } else {
-                            internStringArg(ctx, o.toString());
-                        }
+                        internStringArg(ctx, o.toString());
                     }
                     argIndex++;
                 }
             }
 
             int internedStacktrace = 0;
-            if (tlsState.getShouldCollectStacktrace(logGroup.name())) {
+            if (tlsState.getShouldCollectStacktrace(groupName)) {
                 // Intern stackstraces before creating the trace packet for the proto message so
                 // that the interned stacktrace strings appear before in the trace to make the
                 // trace processing easier.
+                String stacktrace = collectStackTrace();
                 internedStacktrace = internStacktraceString(ctx, stacktrace);
             }
 
-            boolean needsIncrementalState = false;
-
-            long messageHash = 0;
-            if (message.mMessageHash != null) {
-                messageHash = message.mMessageHash;
-            }
-            if (message.mMessageString != null) {
-                needsIncrementalState = true;
-                messageHash =
-                        internProtoMessage(ctx, level, logGroup, message.mMessageString);
-            }
-
             final ProtoOutputStream os = ctx.newTracePacket();
             os.write(TIMESTAMP, tsNanos);
             long token = os.start(PROTOLOG_MESSAGE);
-
             os.write(MESSAGE_ID, messageHash);
 
+            boolean needsIncrementalState = false;
+
             if (args != null) {
 
                 int argIndex = 0;
@@ -462,39 +381,22 @@
                 ArrayList<Double> doubleParams = new ArrayList<>();
                 ArrayList<Boolean> booleanParams = new ArrayList<>();
                 for (Object o : args) {
-                    int type = LogDataType.bitmaskToLogDataType(message.getMessageMask(), argIndex);
+                    int type = LogDataType.bitmaskToLogDataType(paramsMask, argIndex);
                     try {
                         switch (type) {
                             case LogDataType.STRING:
-                                final int internedStringId;
-                                if (o == null) {
-                                    internedStringId = internStringArg(ctx, NULL_STRING);
-                                } else {
-                                    internedStringId = internStringArg(ctx, o.toString());
-                                }
+                                final int internedStringId = internStringArg(ctx, o.toString());
                                 os.write(STR_PARAM_IIDS, internedStringId);
                                 needsIncrementalState = true;
                                 break;
                             case LogDataType.LONG:
-                                if (o == null) {
-                                    longParams.add(0);
-                                } else {
-                                    longParams.add(((Number) o).longValue());
-                                }
+                                longParams.add(((Number) o).longValue());
                                 break;
                             case LogDataType.DOUBLE:
-                                if (o == null) {
-                                    doubleParams.add(0d);
-                                } else {
-                                    doubleParams.add(((Number) o).doubleValue());
-                                }
+                                doubleParams.add(((Number) o).doubleValue());
                                 break;
                             case LogDataType.BOOLEAN:
-                                if (o == null) {
-                                    booleanParams.add(false);
-                                } else {
-                                    booleanParams.add((boolean) o);
-                                }
+                                booleanParams.add((boolean) o);
                                 break;
                         }
                     } catch (ClassCastException ex) {
@@ -512,7 +414,7 @@
                 booleanParams.forEach(it -> os.write(BOOLEAN_PARAMS, it ? 1 : 0));
             }
 
-            if (tlsState.getShouldCollectStacktrace(logGroup.name())) {
+            if (tlsState.getShouldCollectStacktrace(groupName)) {
                 os.write(STACKTRACE_IID, internedStacktrace);
             }
 
@@ -525,63 +427,6 @@
         });
     }
 
-    private long internProtoMessage(
-            TracingContext<ProtoLogDataSource.Instance, ProtoLogDataSource.TlsState,
-                    ProtoLogDataSource.IncrementalState> ctx, LogLevel level,
-            IProtoLogGroup logGroup, String message) {
-        final ProtoLogDataSource.IncrementalState incrementalState = ctx.getIncrementalState();
-
-        if (!incrementalState.clearReported) {
-            final ProtoOutputStream os = ctx.newTracePacket();
-            os.write(SEQUENCE_FLAGS, SEQ_INCREMENTAL_STATE_CLEARED);
-            incrementalState.clearReported = true;
-        }
-
-
-        if (!incrementalState.protologGroupInterningSet.contains(logGroup.getId())) {
-            incrementalState.protologGroupInterningSet.add(logGroup.getId());
-
-            final ProtoOutputStream os = ctx.newTracePacket();
-            final long protologViewerConfigToken = os.start(PROTOLOG_VIEWER_CONFIG);
-            final long groupConfigToken = os.start(GROUPS);
-
-            os.write(ID, logGroup.getId());
-            os.write(NAME, logGroup.name());
-            os.write(TAG, logGroup.getTag());
-
-            os.end(groupConfigToken);
-            os.end(protologViewerConfigToken);
-        }
-
-        final Long messageHash = hash(level, logGroup.name(), message);
-        if (!incrementalState.protologMessageInterningSet.contains(messageHash)) {
-            incrementalState.protologMessageInterningSet.add(messageHash);
-
-            final ProtoOutputStream os = ctx.newTracePacket();
-            final long protologViewerConfigToken = os.start(PROTOLOG_VIEWER_CONFIG);
-            final long messageConfigToken = os.start(MESSAGES);
-
-            os.write(MessageData.MESSAGE_ID, messageHash);
-            os.write(MESSAGE, message);
-            os.write(LEVEL, level.ordinal());
-            os.write(GROUP_ID, logGroup.getId());
-
-            os.end(messageConfigToken);
-            os.end(protologViewerConfigToken);
-        }
-
-        return messageHash;
-    }
-
-    private Long hash(
-            LogLevel logLevel,
-            String logGroup,
-            String messageString
-    ) {
-        final String fullStringIdentifier =  messageString + logLevel + logGroup;
-        return UUID.nameUUIDFromBytes(fullStringIdentifier.getBytes()).getMostSignificantBits();
-    }
-
     private static final int STACK_SIZE_TO_PROTO_LOG_ENTRY_CALL = 12;
 
     private String collectStackTrace() {
@@ -621,7 +466,7 @@
                     ProtoLogDataSource.IncrementalState> ctx,
             Map<String, Integer> internMap,
             long fieldId,
-            @NonNull String string
+            String string
     ) {
         final ProtoLogDataSource.IncrementalState incrementalState = ctx.getIncrementalState();
 
@@ -678,17 +523,25 @@
 
     @Override
     public boolean isEnabled(IProtoLogGroup group, LogLevel level) {
-        final int[] groupLevelCount = mLogLevelCounts.get(group);
-        return (groupLevelCount == null && mDefaultLogLevelCounts[level.ordinal()] > 0)
-                || (groupLevelCount != null && groupLevelCount[level.ordinal()] > 0)
-                || group.isLogToLogcat();
+        return group.isLogToLogcat() || getLogFromLevel(group).ordinal() <= level.ordinal();
     }
 
-    @Override
-    public void registerGroups(IProtoLogGroup... protoLogGroups) {
-        for (IProtoLogGroup protoLogGroup : protoLogGroups) {
-            mLogGroups.put(protoLogGroup.name(), protoLogGroup);
+    private LogLevel getLogFromLevel(IProtoLogGroup group) {
+        if (mLogLevelCounts.containsKey(group)) {
+            for (LogLevel logLevel : LogLevel.values()) {
+                if (mLogLevelCounts.get(group).getOrDefault(logLevel, 0) > 0) {
+                    return logLevel;
+                }
+            }
+        } else {
+            for (LogLevel logLevel : LogLevel.values()) {
+                if (mDefaultLogLevelCounts.getOrDefault(logLevel, 0) > 0) {
+                    return logLevel;
+                }
+            }
         }
+
+        return LogLevel.WTF;
     }
 
     /**
@@ -767,51 +620,36 @@
     }
 
     private synchronized void onTracingInstanceStart(ProtoLogDataSource.ProtoLogConfig config) {
+        this.mTracingInstances.incrementAndGet();
+
         final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom;
-        for (int i = defaultLogFrom.ordinal(); i < LogLevel.values().length; i++) {
-            mDefaultLogLevelCounts[i]++;
-        }
+        mDefaultLogLevelCounts.put(defaultLogFrom,
+                mDefaultLogLevelCounts.getOrDefault(defaultLogFrom, 0) + 1);
 
         final Set<String> overriddenGroupTags = config.getGroupTagsWithOverriddenConfigs();
 
         for (String overriddenGroupTag : overriddenGroupTags) {
             IProtoLogGroup group = mLogGroups.get(overriddenGroupTag);
 
-            if (group == null) {
-                throw new IllegalArgumentException("Trying to set config for \""
-                        + overriddenGroupTag + "\" that isn't registered");
-            }
-
-            mLogLevelCounts.putIfAbsent(group, new int[LogLevel.values().length]);
-            final int[] logLevelsCountsForGroup = mLogLevelCounts.get(group);
+            mLogLevelCounts.putIfAbsent(group, new ArrayMap<>());
+            final Map<LogLevel, Integer> logLevelsCountsForGroup = mLogLevelCounts.get(group);
 
             final LogLevel logFromLevel = config.getConfigFor(overriddenGroupTag).logFrom;
-            for (int i = logFromLevel.ordinal(); i < LogLevel.values().length; i++) {
-                logLevelsCountsForGroup[i]++;
-            }
-
-            if (config.getConfigFor(overriddenGroupTag).collectStackTrace) {
-                mCollectStackTraceGroupCounts.put(group,
-                        mCollectStackTraceGroupCounts.getOrDefault(group, 0) + 1);
-            }
-
-            if (config.getConfigFor(overriddenGroupTag).collectStackTrace) {
-                mCollectStackTraceGroupCounts.put(group,
-                        mCollectStackTraceGroupCounts.getOrDefault(group, 0) + 1);
-            }
+            logLevelsCountsForGroup.put(logFromLevel,
+                    logLevelsCountsForGroup.getOrDefault(logFromLevel, 0) + 1);
         }
 
         mCacheUpdater.run();
-
-        this.mTracingInstances.incrementAndGet();
     }
 
     private synchronized void onTracingInstanceStop(ProtoLogDataSource.ProtoLogConfig config) {
         this.mTracingInstances.decrementAndGet();
 
         final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom;
-        for (int i = defaultLogFrom.ordinal(); i < LogLevel.values().length; i++) {
-            mDefaultLogLevelCounts[i]--;
+        mDefaultLogLevelCounts.put(defaultLogFrom,
+                mDefaultLogLevelCounts.get(defaultLogFrom) - 1);
+        if (mDefaultLogLevelCounts.get(defaultLogFrom) <= 0) {
+            mDefaultLogLevelCounts.remove(defaultLogFrom);
         }
 
         final Set<String> overriddenGroupTags = config.getGroupTagsWithOverriddenConfigs();
@@ -819,24 +657,18 @@
         for (String overriddenGroupTag : overriddenGroupTags) {
             IProtoLogGroup group = mLogGroups.get(overriddenGroupTag);
 
-            final int[] logLevelsCountsForGroup = mLogLevelCounts.get(group);
+            mLogLevelCounts.putIfAbsent(group, new ArrayMap<>());
+            final Map<LogLevel, Integer> logLevelsCountsForGroup = mLogLevelCounts.get(group);
 
             final LogLevel logFromLevel = config.getConfigFor(overriddenGroupTag).logFrom;
-            for (int i = defaultLogFrom.ordinal(); i < LogLevel.values().length; i++) {
-                logLevelsCountsForGroup[i]--;
+            logLevelsCountsForGroup.put(logFromLevel,
+                    logLevelsCountsForGroup.get(logFromLevel) - 1);
+            if (logLevelsCountsForGroup.get(logFromLevel) <= 0) {
+                logLevelsCountsForGroup.remove(logFromLevel);
             }
-            if (Arrays.stream(logLevelsCountsForGroup).allMatch(it -> it == 0)) {
+            if (logLevelsCountsForGroup.isEmpty()) {
                 mLogLevelCounts.remove(group);
             }
-
-            if (config.getConfigFor(overriddenGroupTag).collectStackTrace) {
-                mCollectStackTraceGroupCounts.put(group,
-                        mCollectStackTraceGroupCounts.get(group) - 1);
-
-                if (mCollectStackTraceGroupCounts.get(group) == 0) {
-                    mCollectStackTraceGroupCounts.remove(group);
-                }
-            }
         }
 
         mCacheUpdater.run();
@@ -849,36 +681,5 @@
             pw.flush();
         }
     }
-
-    private static class Message {
-        private final Long mMessageHash;
-        private final Integer mMessageMask;
-        private final String mMessageString;
-
-        private Message(Long messageHash, int messageMask) {
-            this.mMessageHash = messageHash;
-            this.mMessageMask = messageMask;
-            this.mMessageString = null;
-        }
-
-        private Message(String messageString) {
-            this.mMessageHash = null;
-            final List<Integer> argTypes = LogDataType.parseFormatString(messageString);
-            this.mMessageMask = LogDataType.logDataTypesToBitMask(argTypes);
-            this.mMessageString = messageString;
-        }
-
-        private int getMessageMask() {
-            return mMessageMask;
-        }
-
-        private String getMessage(ProtoLogViewerConfigReader viewerConfigReader) {
-            if (mMessageString != null) {
-                return mMessageString;
-            }
-
-            return viewerConfigReader.getViewerString(mMessageHash);
-        }
-    }
 }
 
diff --git a/core/java/com/android/internal/protolog/ProtoLog.java b/core/java/com/android/internal/protolog/ProtoLog.java
index 99d4418..0118c05 100644
--- a/core/java/com/android/internal/protolog/ProtoLog.java
+++ b/core/java/com/android/internal/protolog/ProtoLog.java
@@ -44,23 +44,21 @@
 // LINT.ThenChange(frameworks/base/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt)
 
     // Needs to be set directly otherwise the protologtool tries to transform the method call
-    @Deprecated
     public static boolean REQUIRE_PROTOLOGTOOL = true;
 
-    private static IProtoLog sProtoLogInstance;
-
     /**
      * DEBUG level log.
      *
      * @param group         {@code IProtoLogGroup} controlling this log call.
      * @param messageString constant format string for the logged message.
      * @param args          parameters to be used with the format string.
-     *
-     * NOTE: If source code is pre-processed by ProtoLogTool this is not the function call that is
-     *       executed. Check generated code for actual call.
      */
     public static void d(IProtoLogGroup group, String messageString, Object... args) {
-        logStringMessage(LogLevel.DEBUG, group, messageString, args);
+        // Stub, replaced by the ProtoLogTool.
+        if (REQUIRE_PROTOLOGTOOL) {
+            throw new UnsupportedOperationException(
+                    "ProtoLog calls MUST be processed with ProtoLogTool");
+        }
     }
 
     /**
@@ -69,12 +67,13 @@
      * @param group         {@code IProtoLogGroup} controlling this log call.
      * @param messageString constant format string for the logged message.
      * @param args          parameters to be used with the format string.
-     *
-     * NOTE: If source code is pre-processed by ProtoLogTool this is not the function call that is
-     *       executed. Check generated code for actual call.
      */
     public static void v(IProtoLogGroup group, String messageString, Object... args) {
-        logStringMessage(LogLevel.VERBOSE, group, messageString, args);
+        // Stub, replaced by the ProtoLogTool.
+        if (REQUIRE_PROTOLOGTOOL) {
+            throw new UnsupportedOperationException(
+                    "ProtoLog calls MUST be processed with ProtoLogTool");
+        }
     }
 
     /**
@@ -83,12 +82,13 @@
      * @param group         {@code IProtoLogGroup} controlling this log call.
      * @param messageString constant format string for the logged message.
      * @param args          parameters to be used with the format string.
-     *
-     * NOTE: If source code is pre-processed by ProtoLogTool this is not the function call that is
-     *       executed. Check generated code for actual call.
      */
     public static void i(IProtoLogGroup group, String messageString, Object... args) {
-        logStringMessage(LogLevel.INFO, group, messageString, args);
+        // Stub, replaced by the ProtoLogTool.
+        if (REQUIRE_PROTOLOGTOOL) {
+            throw new UnsupportedOperationException(
+                    "ProtoLog calls MUST be processed with ProtoLogTool");
+        }
     }
 
     /**
@@ -97,12 +97,13 @@
      * @param group         {@code IProtoLogGroup} controlling this log call.
      * @param messageString constant format string for the logged message.
      * @param args          parameters to be used with the format string.
-     *
-     * NOTE: If source code is pre-processed by ProtoLogTool this is not the function call that is
-     *       executed. Check generated code for actual call.
      */
     public static void w(IProtoLogGroup group, String messageString, Object... args) {
-        logStringMessage(LogLevel.WARN, group, messageString, args);
+        // Stub, replaced by the ProtoLogTool.
+        if (REQUIRE_PROTOLOGTOOL) {
+            throw new UnsupportedOperationException(
+                    "ProtoLog calls MUST be processed with ProtoLogTool");
+        }
     }
 
     /**
@@ -111,12 +112,13 @@
      * @param group         {@code IProtoLogGroup} controlling this log call.
      * @param messageString constant format string for the logged message.
      * @param args          parameters to be used with the format string.
-     *
-     * NOTE: If source code is pre-processed by ProtoLogTool this is not the function call that is
-     *       executed. Check generated code for actual call.
      */
     public static void e(IProtoLogGroup group, String messageString, Object... args) {
-        logStringMessage(LogLevel.ERROR, group, messageString, args);
+        // Stub, replaced by the ProtoLogTool.
+        if (REQUIRE_PROTOLOGTOOL) {
+            throw new UnsupportedOperationException(
+                    "ProtoLog calls MUST be processed with ProtoLogTool");
+        }
     }
 
     /**
@@ -125,12 +127,13 @@
      * @param group         {@code IProtoLogGroup} controlling this log call.
      * @param messageString constant format string for the logged message.
      * @param args          parameters to be used with the format string.
-     *
-     * NOTE: If source code is pre-processed by ProtoLogTool this is not the function call that is
-     *       executed. Check generated code for actual call.
      */
     public static void wtf(IProtoLogGroup group, String messageString, Object... args) {
-        logStringMessage(LogLevel.WTF, group, messageString, args);
+        // Stub, replaced by the ProtoLogTool.
+        if (REQUIRE_PROTOLOGTOOL) {
+            throw new UnsupportedOperationException(
+                    "ProtoLog calls MUST be processed with ProtoLogTool");
+        }
     }
 
     /**
@@ -139,7 +142,11 @@
      * @return true iff this is being logged.
      */
     public static boolean isEnabled(IProtoLogGroup group, LogLevel level) {
-        return sProtoLogInstance.isEnabled(group, level);
+        if (REQUIRE_PROTOLOGTOOL) {
+            throw new UnsupportedOperationException(
+                    "ProtoLog calls MUST be processed with ProtoLogTool");
+        }
+        return false;
     }
 
     /**
@@ -147,38 +154,10 @@
      * @return A singleton instance of ProtoLog.
      */
     public static IProtoLog getSingleInstance() {
-        return sProtoLogInstance;
-    }
-
-    /**
-     * Registers available protolog groups. A group must be registered before it can be used.
-     * @param protoLogGroups The groups to register for use in protolog.
-     */
-    public static void registerGroups(IProtoLogGroup... protoLogGroups) {
-        sProtoLogInstance.registerGroups(protoLogGroups);
-    }
-
-    private static void logStringMessage(LogLevel logLevel, IProtoLogGroup group,
-            String stringMessage, Object... args) {
-        if (sProtoLogInstance == null) {
-            throw new IllegalStateException(
-                    "Trying to use ProtoLog before it is initialized in this process.");
+        if (REQUIRE_PROTOLOGTOOL) {
+            throw new UnsupportedOperationException(
+                    "ProtoLog calls MUST be processed with ProtoLogTool");
         }
-
-        if (sProtoLogInstance.isEnabled(group, logLevel)) {
-            sProtoLogInstance.log(logLevel, group, stringMessage, args);
-        }
-    }
-
-    static {
-        if (android.tracing.Flags.perfettoProtologTracing()) {
-            sProtoLogInstance = new PerfettoProtoLogImpl();
-        } else {
-            if (REQUIRE_PROTOLOGTOOL) {
-                throw new RuntimeException("REQUIRE_PROTOLOGTOOL not set to false.");
-            } else {
-                sProtoLogInstance = new LogcatOnlyProtoLogImpl();
-            }
-        }
+        return null;
     }
 }
diff --git a/core/java/com/android/internal/protolog/ProtoLogDataSource.java b/core/java/com/android/internal/protolog/ProtoLogDataSource.java
index 84f3237..6ab79b9 100644
--- a/core/java/com/android/internal/protolog/ProtoLogDataSource.java
+++ b/core/java/com/android/internal/protolog/ProtoLogDataSource.java
@@ -40,7 +40,6 @@
 
 import java.io.IOException;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -139,8 +138,6 @@
     }
 
     public static class IncrementalState {
-        public final Set<Integer> protologGroupInterningSet = new HashSet<>();
-        public final Set<Long> protologMessageInterningSet = new HashSet<>();
         public final Map<String, Integer> argumentInterningMap = new HashMap<>();
         public final Map<String, Integer> stacktraceInterningMap = new HashMap<>();
         public boolean clearReported = false;
diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java
index 3082295..6d142af 100644
--- a/core/java/com/android/internal/protolog/ProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java
@@ -54,33 +54,48 @@
     private static Runnable sCacheUpdater;
 
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void d(IProtoLogGroup group, long messageHash, int paramsMask, Object... args) {
-        getSingleInstance().log(LogLevel.DEBUG, group, messageHash, paramsMask, args);
+    public static void d(IProtoLogGroup group, long messageHash, int paramsMask,
+            @Nullable String messageString,
+            Object... args) {
+        getSingleInstance()
+                .log(LogLevel.DEBUG, group, messageHash, paramsMask, messageString, args);
     }
 
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void v(IProtoLogGroup group, long messageHash, int paramsMask, Object... args) {
-        getSingleInstance().log(LogLevel.VERBOSE, group, messageHash, paramsMask, args);
+    public static void v(IProtoLogGroup group, long messageHash, int paramsMask,
+            @Nullable String messageString,
+            Object... args) {
+        getSingleInstance().log(LogLevel.VERBOSE, group, messageHash, paramsMask, messageString,
+                args);
     }
 
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void i(IProtoLogGroup group, long messageHash, int paramsMask, Object... args) {
-        getSingleInstance().log(LogLevel.INFO, group, messageHash, paramsMask, args);
+    public static void i(IProtoLogGroup group, long messageHash, int paramsMask,
+            @Nullable String messageString,
+            Object... args) {
+        getSingleInstance().log(LogLevel.INFO, group, messageHash, paramsMask, messageString, args);
     }
 
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void w(IProtoLogGroup group, long messageHash, int paramsMask, Object... args) {
-        getSingleInstance().log(LogLevel.WARN, group, messageHash, paramsMask, args);
+    public static void w(IProtoLogGroup group, long messageHash, int paramsMask,
+            @Nullable String messageString,
+            Object... args) {
+        getSingleInstance().log(LogLevel.WARN, group, messageHash, paramsMask, messageString, args);
     }
 
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void e(IProtoLogGroup group, long messageHash, int paramsMask, Object... args) {
-        getSingleInstance().log(LogLevel.ERROR, group, messageHash, paramsMask, args);
+    public static void e(IProtoLogGroup group, long messageHash, int paramsMask,
+            @Nullable String messageString,
+            Object... args) {
+        getSingleInstance()
+                .log(LogLevel.ERROR, group, messageHash, paramsMask, messageString, args);
     }
 
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void wtf(IProtoLogGroup group, long messageHash, int paramsMask, Object... args) {
-        getSingleInstance().log(LogLevel.WTF, group, messageHash, paramsMask, args);
+    public static void wtf(IProtoLogGroup group, long messageHash, int paramsMask,
+            @Nullable String messageString,
+            Object... args) {
+        getSingleInstance().log(LogLevel.WTF, group, messageHash, paramsMask, messageString, args);
     }
 
     /**
@@ -92,27 +107,18 @@
     }
 
     /**
-     * Registers available protolog groups. A group must be registered before it can be used.
-     * @param protoLogGroups The groups to register for use in protolog.
-     */
-    public static void registerGroups(IProtoLogGroup... protoLogGroups) {
-        getSingleInstance().registerGroups(protoLogGroups);
-    }
-
-    /**
      * Returns the single instance of the ProtoLogImpl singleton class.
      */
     public static synchronized IProtoLog getSingleInstance() {
         if (sServiceInstance == null) {
             if (android.tracing.Flags.perfettoProtologTracing()) {
-                sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater);
+                sServiceInstance = new PerfettoProtoLogImpl(
+                        sViewerConfigPath, sLogGroups, sCacheUpdater);
             } else {
                 sServiceInstance = new LegacyProtoLogImpl(
-                        sLegacyOutputFilePath, sLegacyViewerConfigPath, sCacheUpdater);
+                        sLegacyOutputFilePath, sLegacyViewerConfigPath, sLogGroups, sCacheUpdater);
             }
 
-            IProtoLogGroup[] groups = sLogGroups.values().toArray(new IProtoLogGroup[0]);
-            sServiceInstance.registerGroups(groups);
             sCacheUpdater.run();
         }
         return sServiceInstance;
diff --git a/core/java/com/android/internal/protolog/common/IProtoLog.java b/core/java/com/android/internal/protolog/common/IProtoLog.java
index f5695ac..f72d9f7 100644
--- a/core/java/com/android/internal/protolog/common/IProtoLog.java
+++ b/core/java/com/android/internal/protolog/common/IProtoLog.java
@@ -27,19 +27,11 @@
      * @param group The group this message belongs to.
      * @param messageHash The hash of the message.
      * @param paramsMask The parameters mask of the message.
-     * @param args The arguments of the message.
-     */
-    void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask,
-            Object[] args);
-
-    /**
-     * Log a ProtoLog message
-     * @param logLevel Log level of the proto message.
-     * @param group The group this message belongs to.
      * @param messageString The message string.
      * @param args The arguments of the message.
      */
-    void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object... args);
+    void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask,
+             String messageString, Object[] args);
 
     /**
      * Check if ProtoLog is tracing.
@@ -68,10 +60,4 @@
      * @return If we need to log this group and level to either ProtoLog or Logcat.
      */
     boolean isEnabled(IProtoLogGroup group, LogLevel level);
-
-    /**
-     * Registers available protolog groups. A group must be registered before it can be used.
-     * @param protoLogGroups The groups to register for use in protolog.
-     */
-    void registerGroups(IProtoLogGroup... protoLogGroups);
 }
diff --git a/core/java/com/android/internal/widget/NotificationRowIconView.java b/core/java/com/android/internal/widget/NotificationRowIconView.java
index f5f04a7..98e6e85 100644
--- a/core/java/com/android/internal/widget/NotificationRowIconView.java
+++ b/core/java/com/android/internal/widget/NotificationRowIconView.java
@@ -35,8 +35,10 @@
 import android.view.RemotableViewMethod;
 import android.widget.RemoteViews;
 
+import com.android.internal.R;
+
 /**
- * An image view that holds the icon displayed on the left side of a notification row.
+ * An image view that holds the icon displayed at the start of a notification row.
  */
 @RemoteViews.RemoteView
 public class NotificationRowIconView extends CachingIconView {
@@ -98,9 +100,12 @@
                 setPadding(0, 0, 0, 0);
 
                 // Make the background white in case the icon itself doesn't have one.
-                int white = Color.rgb(255, 255, 255);
-                ColorFilter colorFilter = new PorterDuffColorFilter(white,
+                ColorFilter colorFilter = new PorterDuffColorFilter(Color.WHITE,
                         PorterDuff.Mode.SRC_ATOP);
+
+                if (mOriginalBackground == null) {
+                    setBackground(getContext().getDrawable(R.drawable.notification_icon_circle));
+                }
                 getBackground().mutate().setColorFilter(colorFilter);
             } else {
                 // Restore original padding and background if needed
diff --git a/core/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp
index b8fd3d0..3f74fac 100644
--- a/core/jni/android_hardware_Camera.cpp
+++ b/core/jni/android_hardware_Camera.cpp
@@ -582,8 +582,8 @@
 
 // connect to camera service
 static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, jobject weak_this,
-                                                 jint cameraId, jstring clientPackageName,
-                                                 jint rotationOverride, jboolean forceSlowJpegMode,
+                                                 jint cameraId, jint rotationOverride,
+                                                 jboolean forceSlowJpegMode,
                                                  jobject jClientAttributionParcel,
                                                  jint devicePolicy) {
     AttributionSourceState clientAttribution;
@@ -591,16 +591,8 @@
         return -EACCES;
     }
 
-    // Convert jstring to String16
-    const char16_t *rawClientName = reinterpret_cast<const char16_t*>(
-        env->GetStringChars(clientPackageName, NULL));
-    jsize rawClientNameLen = env->GetStringLength(clientPackageName);
-    std::string clientName = toStdString(rawClientName, rawClientNameLen);
-    env->ReleaseStringChars(clientPackageName,
-                            reinterpret_cast<const jchar*>(rawClientName));
-
     int targetSdkVersion = android_get_application_target_sdk_version();
-    sp<Camera> camera = Camera::connect(cameraId, clientName, targetSdkVersion, rotationOverride,
+    sp<Camera> camera = Camera::connect(cameraId, targetSdkVersion, rotationOverride,
                                         forceSlowJpegMode, clientAttribution, devicePolicy);
     if (camera == NULL) {
         return -EACCES;
@@ -1089,7 +1081,7 @@
          (void *)android_hardware_Camera_getNumberOfCameras},
         {"_getCameraInfo", "(IILandroid/os/Parcel;ILandroid/hardware/Camera$CameraInfo;)V",
          (void *)android_hardware_Camera_getCameraInfo},
-        {"native_setup", "(Ljava/lang/Object;ILjava/lang/String;IZLandroid/os/Parcel;I)I",
+        {"native_setup", "(Ljava/lang/Object;IIZLandroid/os/Parcel;I)I",
          (void *)android_hardware_Camera_native_setup},
         {"native_release", "()V", (void *)android_hardware_Camera_release},
         {"setPreviewSurface", "(Landroid/view/Surface;)V",
diff --git a/core/res/res/layout/notification_template_conversation_icon_container.xml b/core/res/res/layout/notification_template_conversation_icon_container.xml
index 0438dc5..b45483b 100644
--- a/core/res/res/layout/notification_template_conversation_icon_container.xml
+++ b/core/res/res/layout/notification_template_conversation_icon_container.xml
@@ -77,7 +77,7 @@
                 android:scaleType="center"
                 />
 
-            <com.android.internal.widget.CachingIconView
+            <com.android.internal.widget.NotificationRowIconView
                 android:id="@+id/icon"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c9b5d41..419a615 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4059,6 +4059,7 @@
   <java-symbol type="id" name="snooze_button" />
   <java-symbol type="dimen" name="text_size_body_2_material" />
   <java-symbol type="dimen" name="notification_icon_circle_size" />
+  <java-symbol type="drawable" name="notification_icon_circle" />
   <java-symbol type="dimen" name="messaging_avatar_size" />
   <java-symbol type="dimen" name="messaging_group_sending_progress_size" />
   <java-symbol type="dimen" name="messaging_image_rounding" />
diff --git a/core/tests/coretests/src/android/util/SequenceUtilsTest.java b/core/tests/coretests/src/android/util/SequenceUtilsTest.java
index 020520d..6ca1751 100644
--- a/core/tests/coretests/src/android/util/SequenceUtilsTest.java
+++ b/core/tests/coretests/src/android/util/SequenceUtilsTest.java
@@ -19,7 +19,7 @@
 
 import static android.util.SequenceUtils.getInitSeq;
 import static android.util.SequenceUtils.getNextSeq;
-import static android.util.SequenceUtils.isIncomingSeqNewer;
+import static android.util.SequenceUtils.isIncomingSeqStale;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -60,30 +60,35 @@
     }
 
     @Test
-    public void testIsIncomingSeqNewer() {
-        assertTrue(isIncomingSeqNewer(getInitSeq() + 1, getInitSeq() + 10));
-        assertFalse(isIncomingSeqNewer(getInitSeq() + 10, getInitSeq() + 1));
-        assertTrue(isIncomingSeqNewer(-100, 100));
-        assertFalse(isIncomingSeqNewer(100, -100));
-        assertTrue(isIncomingSeqNewer(1, 2));
-        assertFalse(isIncomingSeqNewer(2, 1));
+    public void testIsIncomingSeqStale() {
+        assertFalse(isIncomingSeqStale(getInitSeq() + 1, getInitSeq() + 10));
+        assertTrue(isIncomingSeqStale(getInitSeq() + 10, getInitSeq() + 1));
+        assertFalse(isIncomingSeqStale(-100, 100));
+        assertTrue(isIncomingSeqStale(100, -100));
+        assertFalse(isIncomingSeqStale(1, 2));
+        assertTrue(isIncomingSeqStale(2, 1));
 
         // Possible incoming seq are all newer than the initial seq.
-        assertTrue(isIncomingSeqNewer(getInitSeq(), getInitSeq() + 1));
-        assertTrue(isIncomingSeqNewer(getInitSeq(), -100));
-        assertTrue(isIncomingSeqNewer(getInitSeq(), 0));
-        assertTrue(isIncomingSeqNewer(getInitSeq(), 100));
-        assertTrue(isIncomingSeqNewer(getInitSeq(), Integer.MAX_VALUE));
-        assertTrue(isIncomingSeqNewer(getInitSeq(), getNextSeq(Integer.MAX_VALUE)));
+        assertFalse(isIncomingSeqStale(getInitSeq(), getInitSeq()));
+        assertFalse(isIncomingSeqStale(getInitSeq(), getInitSeq() + 1));
+        assertFalse(isIncomingSeqStale(getInitSeq(), -100));
+        assertFalse(isIncomingSeqStale(getInitSeq(), 0));
+        assertFalse(isIncomingSeqStale(getInitSeq(), 100));
+        assertFalse(isIncomingSeqStale(getInitSeq(), Integer.MAX_VALUE));
+        assertFalse(isIncomingSeqStale(getInitSeq(), getNextSeq(Integer.MAX_VALUE)));
 
         // False for the same seq.
-        assertFalse(isIncomingSeqNewer(getInitSeq(), getInitSeq()));
-        assertFalse(isIncomingSeqNewer(100, 100));
-        assertFalse(isIncomingSeqNewer(Integer.MAX_VALUE, Integer.MAX_VALUE));
+        assertFalse(isIncomingSeqStale(100, 100));
+        assertFalse(isIncomingSeqStale(Integer.MAX_VALUE, Integer.MAX_VALUE));
 
-        // True when there is a large jump (overflow).
-        assertTrue(isIncomingSeqNewer(Integer.MAX_VALUE, getInitSeq() + 1));
-        assertTrue(isIncomingSeqNewer(Integer.MAX_VALUE, getInitSeq() + 100));
-        assertTrue(isIncomingSeqNewer(Integer.MAX_VALUE, getNextSeq(Integer.MAX_VALUE)));
+        // False when there is a large jump (overflow).
+        assertFalse(isIncomingSeqStale(Integer.MAX_VALUE, getInitSeq() + 1));
+        assertFalse(isIncomingSeqStale(Integer.MAX_VALUE, getInitSeq() + 100));
+        assertFalse(isIncomingSeqStale(Integer.MAX_VALUE, getNextSeq(Integer.MAX_VALUE)));
+
+        // True when the large jump is opposite (curSeq is newer).
+        assertTrue(isIncomingSeqStale(getInitSeq() + 1, Integer.MAX_VALUE));
+        assertTrue(isIncomingSeqStale(getInitSeq() + 100, Integer.MAX_VALUE));
+        assertTrue(isIncomingSeqStale(getNextSeq(Integer.MAX_VALUE), Integer.MAX_VALUE));
     }
 }
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index 9337bf6..033ac7c 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import static android.util.SequenceUtils.getInitSeq;
 import static android.view.Surface.FRAME_RATE_CATEGORY_DEFAULT;
 import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH;
 import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT;
@@ -1555,9 +1556,9 @@
         final InsetsState state0 = new InsetsState();
         final InsetsState state1 = new InsetsState();
         state0.setDisplayFrame(new Rect(0, 0, 500, 1000));
-        state0.setSeq(10000);
+        state0.setSeq(getInitSeq() + 10000);
         state1.setDisplayFrame(new Rect(0, 0, 1500, 2000));
-        state1.setSeq(10001);
+        state1.setSeq(getInitSeq() + 10001);
         final InsetsSourceControl.Array array = new InsetsSourceControl.Array();
 
         sInstrumentation.runOnMainSync(() -> {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
index 822a07c..544f0f3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -894,9 +894,7 @@
 
     private static boolean isDraggingToFullscreenAllowed(
             @NonNull DividerAttributes dividerAttributes) {
-        // TODO(b/293654166) Use DividerAttributes.isDraggingToFullscreenAllowed when extension is
-        // updated to v7.
-        return false;
+        return dividerAttributes.isDraggingToFullscreenAllowed();
     }
 
     /**
diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS
index ebebd8a..cb422ea 100644
--- a/libs/WindowManager/Shell/OWNERS
+++ b/libs/WindowManager/Shell/OWNERS
@@ -1,5 +1,5 @@
 xutan@google.com
 
 # Give submodule owners in shell resource approval
-per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, nmusgrave@google.com, pbdr@google.com, tkachenkoi@google.com, mpodolian@google.com, liranb@google.com
+per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, tkachenkoi@google.com, mpodolian@google.com, liranb@google.com
 per-file res*/*/tv_*.xml = bronger@google.com
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
index 61ec49c..8c2cfd1e 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
@@ -20,7 +20,6 @@
 import android.provider.Settings
 import android.util.Log
 import com.android.window.flags.Flags
-import com.android.wm.shell.shared.DesktopModeStatus
 
 /*
  * A shared class to check desktop mode flags state.
@@ -35,13 +34,9 @@
     private val shouldOverrideByDevOption: Boolean
 ) {
   // All desktop mode related flags will be added here
-  DESKTOP_WINDOWING_MODE(DesktopModeStatus::isDesktopModeFlagEnabled, true),
+  DESKTOP_WINDOWING_MODE(Flags::enableDesktopWindowingMode, true),
   WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity, true);
 
-  // Local cache for toggle override, which is initialized once on its first access. It needs to be
-  // refreshed only on reboots as overridden state takes effect on reboots.
-  private var cachedToggleOverride: ToggleOverride? = null
-
   /**
    * Determines state of flag based on the actual flag and desktop mode developer option overrides.
    *
@@ -139,13 +134,19 @@
         }
   }
 
-  private companion object {
-    const val TAG = "DesktopModeFlags"
+  companion object {
+    private const val TAG = "DesktopModeFlags"
 
     /**
      * Key for non-persistent System Property which is used to store desktop windowing developer
      * option overrides.
      */
-    const val SYSTEM_PROPERTY_OVERRIDE_KEY = "sys.wmshell.desktopmode.dev_toggle_override"
+    private const val SYSTEM_PROPERTY_OVERRIDE_KEY = "sys.wmshell.desktopmode.dev_toggle_override"
+
+    /**
+     * Local cache for toggle override, which is initialized once on its first access. It needs to
+     * be refreshed only on reboots as overridden state takes effect on reboots.
+     */
+    private var cachedToggleOverride: ToggleOverride? = null
   }
 }
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
similarity index 93%
rename from libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java
rename to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 67d46f4..fc4710f 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.shared;
+package com.android.wm.shell.shared.desktopmode;
 
 import android.annotation.NonNull;
 import android.content.Context;
@@ -23,7 +23,6 @@
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.window.flags.Flags;
-import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
 
 /**
  * Constants for desktop mode feature
@@ -102,16 +101,6 @@
             "persist.wm.debug.desktop_max_task_limit", DEFAULT_MAX_TASK_LIMIT);
 
     /**
-     * Return {@code true} if desktop windowing flag is enabled. Only to be used for testing.
-     * Callers should use {@link #canEnterDesktopMode(Context)} to query the state of desktop
-     * windowing.
-     */
-    @VisibleForTesting
-    public static boolean isDesktopModeFlagEnabled() {
-        return Flags.enableDesktopWindowingMode();
-    }
-
-    /**
      * Return {@code true} if veiled resizing is active. If false, fluid resizing is used.
      */
     public static boolean isVeiledResizeEnabled() {
@@ -167,7 +156,7 @@
 
     /** Returns if desktop mode dev option should be enabled if there is no user override. */
     public static boolean shouldDevOptionBeEnabledByDefault() {
-        return isDesktopModeFlagEnabled();
+        return Flags.enableDesktopWindowingMode();
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index ebdea1b..f014e55 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -24,6 +24,9 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
+import static com.android.wm.shell.compatui.impl.CompatUIEventsKt.CAMERA_CONTROL_STATE_UPDATE;
+import static com.android.wm.shell.compatui.impl.CompatUIEventsKt.SIZE_COMPAT_RESTART_BUTTON_APPEARED;
+import static com.android.wm.shell.compatui.impl.CompatUIEventsKt.SIZE_COMPAT_RESTART_BUTTON_CLICKED;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
@@ -31,7 +34,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager.RunningTaskInfo;
-import android.app.CameraCompatTaskInfo.CameraCompatControlState;
 import android.app.TaskInfo;
 import android.app.WindowConfiguration;
 import android.content.LocusId;
@@ -57,6 +59,11 @@
 import com.android.wm.shell.common.ScreenshotUtils;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.compatui.CompatUIController;
+import com.android.wm.shell.compatui.api.CompatUIHandler;
+import com.android.wm.shell.compatui.api.CompatUIInfo;
+import com.android.wm.shell.compatui.impl.CompatUIEvents.CameraControlStateUpdated;
+import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonAppeared;
+import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonClicked;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.startingsurface.StartingWindowController;
 import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -75,8 +82,7 @@
  * Unified task organizer for all components in the shell.
  * TODO(b/167582004): may consider consolidating this class and TaskOrganizer
  */
-public class ShellTaskOrganizer extends TaskOrganizer implements
-        CompatUIController.CompatUICallback {
+public class ShellTaskOrganizer extends TaskOrganizer {
     private static final String TAG = "ShellTaskOrganizer";
 
     // Intentionally using negative numbers here so the positive numbers can be used
@@ -194,12 +200,11 @@
      * In charge of showing compat UI. Can be {@code null} if the device doesn't support size
      * compat or if this isn't the main {@link ShellTaskOrganizer}.
      *
-     * <p>NOTE: only the main {@link ShellTaskOrganizer} should have a {@link CompatUIController},
-     * and register itself as a {@link CompatUIController.CompatUICallback}. Subclasses should be
-     * initialized with a {@code null} {@link CompatUIController}.
+     * <p>NOTE: only the main {@link ShellTaskOrganizer} should have a {@link CompatUIHandler},
+     * Subclasses should be initialized with a {@code null} {@link CompatUIHandler}.
      */
     @Nullable
-    private final CompatUIController mCompatUI;
+    private final CompatUIHandler mCompatUI;
 
     @NonNull
     private final ShellCommandHandler mShellCommandHandler;
@@ -223,7 +228,7 @@
 
     public ShellTaskOrganizer(ShellInit shellInit,
             ShellCommandHandler shellCommandHandler,
-            @Nullable CompatUIController compatUI,
+            @Nullable CompatUIHandler compatUI,
             Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<RecentTasksController> recentTasks,
             ShellExecutor mainExecutor) {
@@ -235,7 +240,7 @@
     protected ShellTaskOrganizer(ShellInit shellInit,
             ShellCommandHandler shellCommandHandler,
             ITaskOrganizerController taskOrganizerController,
-            @Nullable CompatUIController compatUI,
+            @Nullable CompatUIHandler compatUI,
             Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<RecentTasksController> recentTasks,
             ShellExecutor mainExecutor) {
@@ -252,7 +257,21 @@
     private void onInit() {
         mShellCommandHandler.addDumpCallback(this::dump, this);
         if (mCompatUI != null) {
-            mCompatUI.setCompatUICallback(this);
+            mCompatUI.setCallback(compatUIEvent -> {
+                switch(compatUIEvent.getEventId()) {
+                    case SIZE_COMPAT_RESTART_BUTTON_APPEARED:
+                        onSizeCompatRestartButtonAppeared(compatUIEvent.asType());
+                        break;
+                    case SIZE_COMPAT_RESTART_BUTTON_CLICKED:
+                        onSizeCompatRestartButtonClicked(compatUIEvent.asType());
+                        break;
+                    case CAMERA_CONTROL_STATE_UPDATE:
+                        onCameraControlStateUpdated(compatUIEvent.asType());
+                        break;
+                    default:
+
+                }
+            });
         }
         registerOrganizer();
     }
@@ -680,6 +699,22 @@
         }
     }
 
+    /**
+     * Shows/hides the given task surface.  Not for general use as changing the task visibility may
+     * conflict with other Transitions.  This is currently ONLY used to temporarily hide a task
+     * while a drag is in session.
+     */
+    public void setTaskSurfaceVisibility(int taskId, boolean visible) {
+        synchronized (mLock) {
+            final TaskAppearedInfo info = mTasks.get(taskId);
+            if (info != null) {
+                SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+                t.setVisibility(info.getLeash(), visible);
+                t.apply();
+            }
+        }
+    }
+
     private boolean updateTaskListenerIfNeeded(RunningTaskInfo taskInfo, SurfaceControl leash,
             TaskListener oldListener, TaskListener newListener) {
         if (oldListener == newListener) return false;
@@ -727,45 +762,6 @@
         }
     }
 
-    @Override
-    public void onSizeCompatRestartButtonAppeared(int taskId) {
-        final TaskAppearedInfo info;
-        synchronized (mLock) {
-            info = mTasks.get(taskId);
-        }
-        if (info == null) {
-            return;
-        }
-        logSizeCompatRestartButtonEventReported(info,
-                FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__APPEARED);
-    }
-
-    @Override
-    public void onSizeCompatRestartButtonClicked(int taskId) {
-        final TaskAppearedInfo info;
-        synchronized (mLock) {
-            info = mTasks.get(taskId);
-        }
-        if (info == null) {
-            return;
-        }
-        logSizeCompatRestartButtonEventReported(info,
-                FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__CLICKED);
-        restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token);
-    }
-
-    @Override
-    public void onCameraControlStateUpdated(int taskId, @CameraCompatControlState int state) {
-        final TaskAppearedInfo info;
-        synchronized (mLock) {
-            info = mTasks.get(taskId);
-        }
-        if (info == null) {
-            return;
-        }
-        updateCameraCompatControlState(info.getTaskInfo().token, state);
-    }
-
     /** Reparents a child window surface to the task surface. */
     public void reparentChildSurfaceToTask(int taskId, SurfaceControl sc,
             SurfaceControl.Transaction t) {
@@ -783,6 +779,50 @@
         taskListener.reparentChildSurfaceToTask(taskId, sc, t);
     }
 
+    @VisibleForTesting
+    void onSizeCompatRestartButtonAppeared(@NonNull SizeCompatRestartButtonAppeared compatUIEvent) {
+        final int taskId = compatUIEvent.getTaskId();
+        final TaskAppearedInfo info;
+        synchronized (mLock) {
+            info = mTasks.get(taskId);
+        }
+        if (info == null) {
+            return;
+        }
+        logSizeCompatRestartButtonEventReported(info,
+                FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__APPEARED);
+    }
+
+    @VisibleForTesting
+    void onSizeCompatRestartButtonClicked(@NonNull SizeCompatRestartButtonClicked compatUIEvent) {
+        final int taskId = compatUIEvent.getTaskId();
+        final TaskAppearedInfo info;
+        synchronized (mLock) {
+            info = mTasks.get(taskId);
+        }
+        if (info == null) {
+            return;
+        }
+        logSizeCompatRestartButtonEventReported(info,
+                FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__CLICKED);
+        restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token);
+    }
+
+    @VisibleForTesting
+    void onCameraControlStateUpdated(@NonNull CameraControlStateUpdated compatUIEvent) {
+        final int taskId = compatUIEvent.getTaskId();
+        final int state = compatUIEvent.getState();
+        final TaskAppearedInfo info;
+        synchronized (mLock) {
+            info = mTasks.get(taskId);
+        }
+        if (info == null) {
+            return;
+        }
+        updateCameraCompatControlState(info.getTaskInfo().token, state);
+    }
+
+
     private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info,
             int event) {
         ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo;
@@ -810,10 +850,10 @@
         // on this Task if there is any.
         if (taskListener == null || !taskListener.supportCompatUI()
                 || !taskInfo.appCompatTaskInfo.hasCompatUI() || !taskInfo.isVisible) {
-            mCompatUI.onCompatInfoChanged(taskInfo, null /* taskListener */);
+            mCompatUI.onCompatInfoChanged(new CompatUIInfo(taskInfo, null /* taskListener */));
             return;
         }
-        mCompatUI.onCompatInfoChanged(taskInfo, taskListener);
+        mCompatUI.onCompatInfoChanged(new CompatUIInfo(taskInfo, taskListener));
     }
 
     private TaskListener getTaskListener(RunningTaskInfo runningTaskInfo) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
index 3ad60e7..1bc1795 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
@@ -492,6 +492,11 @@
         return mHideHandle;
     }
 
+    /** Returns true if the divider is currently being physically controlled by the user. */
+    boolean isMoving() {
+        return mMoving;
+    }
+
     private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener {
         @Override
         public boolean onDoubleTap(MotionEvent e) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index bdef4f4..51f9de8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -652,9 +652,18 @@
                 .ofInt(from, to)
                 .setDuration(duration);
         mDividerFlingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+
+        // If the divider is being physically controlled by the user, we use a cool parallax effect
+        // on the task windows. So if this "snap" animation is an extension of a user-controlled
+        // movement, we pass in true here to continue the parallax effect smoothly.
+        boolean isBeingMovedByUser = mSplitWindowManager.getDividerView() != null
+                && mSplitWindowManager.getDividerView().isMoving();
+
         mDividerFlingAnimator.addUpdateListener(
                 animation -> updateDividerBounds(
-                        (int) animation.getAnimatedValue(), false /* shouldUseParallaxEffect */)
+                        (int) animation.getAnimatedValue(),
+                        isBeingMovedByUser /* shouldUseParallaxEffect */
+                )
         );
         mDividerFlingAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
index 5d121c2..46c1a43 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
@@ -37,7 +37,6 @@
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
 import android.view.SurfaceSession;
-import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowlessWindowManager;
 
@@ -192,7 +191,7 @@
         mDividerView.setInteractive(interactive, hideHandle, from);
     }
 
-    View getDividerView() {
+    DividerView getDividerView() {
         return mDividerView;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 2520c25..c02c9cf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -20,7 +20,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.CameraCompatTaskInfo.CameraCompatControlState;
 import android.app.TaskInfo;
 import android.content.ComponentName;
 import android.content.Context;
@@ -50,6 +49,10 @@
 import com.android.wm.shell.common.DockStateReader;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.compatui.api.CompatUIEvent;
+import com.android.wm.shell.compatui.api.CompatUIHandler;
+import com.android.wm.shell.compatui.api.CompatUIInfo;
+import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonClicked;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
@@ -71,17 +74,7 @@
  * activities are in compatibility mode.
  */
 public class CompatUIController implements OnDisplaysChangedListener,
-        DisplayImeController.ImePositionProcessor, KeyguardChangeListener {
-
-    /** Callback for compat UI interaction. */
-    public interface CompatUICallback {
-        /** Called when the size compat restart button appears. */
-        void onSizeCompatRestartButtonAppeared(int taskId);
-        /** Called when the size compat restart button is clicked. */
-        void onSizeCompatRestartButtonClicked(int taskId);
-        /** Called when the camera compat control state is updated. */
-        void onCameraControlStateUpdated(int taskId, @CameraCompatControlState int state);
-    }
+        DisplayImeController.ImePositionProcessor, KeyguardChangeListener, CompatUIHandler {
 
     private static final String TAG = "CompatUIController";
 
@@ -170,7 +163,7 @@
     private final Function<Integer, Integer> mDisappearTimeSupplier;
 
     @Nullable
-    private CompatUICallback mCompatUICallback;
+    private Consumer<CompatUIEvent> mCallback;
 
     // Indicates if the keyguard is currently showing, in which case compat UIs shouldn't
     // be shown.
@@ -230,20 +223,21 @@
         mCompatUIShellCommandHandler.onInit();
     }
 
-    /** Sets the callback for Compat UI interactions. */
-    public void setCompatUICallback(@NonNull CompatUICallback compatUiCallback) {
-        mCompatUICallback = compatUiCallback;
+    /** Sets the callback for UI interactions. */
+    @Override
+    public void setCallback(@Nullable Consumer<CompatUIEvent> callback) {
+        mCallback = callback;
     }
 
     /**
      * Called when the Task info changed. Creates and updates the compat UI if there is an
      * activity in size compat, or removes the UI if there is no size compat activity.
      *
-     * @param taskInfo {@link TaskInfo} task the activity is in.
-     * @param taskListener listener to handle the Task Surface placement.
+     * @param compatUIInfo {@link CompatUIInfo} encapsulates information about the task and listener
      */
-    public void onCompatInfoChanged(@NonNull TaskInfo taskInfo,
-            @Nullable ShellTaskOrganizer.TaskListener taskListener) {
+    public void onCompatInfoChanged(@NonNull CompatUIInfo compatUIInfo) {
+        final TaskInfo taskInfo = compatUIInfo.getTaskInfo();
+        final ShellTaskOrganizer.TaskListener taskListener = compatUIInfo.getListener();
         if (taskInfo != null && !taskInfo.appCompatTaskInfo.topActivityInSizeCompat) {
             mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId);
         }
@@ -466,7 +460,7 @@
     CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo,
             ShellTaskOrganizer.TaskListener taskListener) {
         return new CompatUIWindowManager(context,
-                taskInfo, mSyncQueue, mCompatUICallback, taskListener,
+                taskInfo, mSyncQueue, mCallback, taskListener,
                 mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState,
                 mCompatUIConfiguration, this::onRestartButtonClicked);
     }
@@ -478,9 +472,9 @@
                 taskInfoState.first)) {
             // We need to show the dialog
             mSetOfTaskIdsShowingRestartDialog.add(taskInfoState.first.taskId);
-            onCompatInfoChanged(taskInfoState.first, taskInfoState.second);
+            onCompatInfoChanged(new CompatUIInfo(taskInfoState.first, taskInfoState.second));
         } else {
-            mCompatUICallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId);
+            mCallback.accept(new SizeCompatRestartButtonClicked(taskInfoState.first.taskId));
         }
     }
 
@@ -575,13 +569,13 @@
     private void onRestartDialogCallback(
             Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) {
         mTaskIdToRestartDialogWindowManagerMap.remove(stateInfo.first.taskId);
-        mCompatUICallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId);
+        mCallback.accept(new SizeCompatRestartButtonClicked(stateInfo.first.taskId));
     }
 
     private void onRestartDialogDismissCallback(
             Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) {
         mSetOfTaskIdsShowingRestartDialog.remove(stateInfo.first.taskId);
-        onCompatInfoChanged(stateInfo.first, stateInfo.second);
+        onCompatInfoChanged(new CompatUIInfo(stateInfo.first, stateInfo.second));
     }
 
     private void createOrUpdateReachabilityEduLayout(@NonNull TaskInfo taskInfo,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index 3ab1fad..1931212 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -40,8 +40,10 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.compatui.CompatUIController.CompatUICallback;
 import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
+import com.android.wm.shell.compatui.api.CompatUIEvent;
+import com.android.wm.shell.compatui.impl.CompatUIEvents.CameraControlStateUpdated;
+import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonAppeared;
 
 import java.util.function.Consumer;
 
@@ -50,10 +52,13 @@
  */
 class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
 
-    private final CompatUICallback mCallback;
+    @NonNull
+    private final Consumer<CompatUIEvent> mCallback;
 
+    @NonNull
     private final CompatUIConfiguration mCompatUIConfiguration;
 
+    @NonNull
     private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked;
 
     // Remember the last reported states in case visibility changes due to keyguard or IME updates.
@@ -65,6 +70,7 @@
     int mCameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN;
 
     @VisibleForTesting
+    @NonNull
     CompatUIHintsState mCompatUIHintsState;
 
     @Nullable
@@ -73,11 +79,15 @@
 
     private final float mHideScmTolerance;
 
-    CompatUIWindowManager(Context context, TaskInfo taskInfo,
-            SyncTransactionQueue syncQueue, CompatUICallback callback,
-            ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout,
-            CompatUIHintsState compatUIHintsState, CompatUIConfiguration compatUIConfiguration,
-            Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked) {
+    CompatUIWindowManager(@NonNull Context context, @NonNull TaskInfo taskInfo,
+                          @NonNull SyncTransactionQueue syncQueue,
+                          @NonNull Consumer<CompatUIEvent> callback,
+                          @Nullable ShellTaskOrganizer.TaskListener taskListener,
+                          @Nullable DisplayLayout displayLayout,
+                          @NonNull CompatUIHintsState compatUIHintsState,
+                          @NonNull CompatUIConfiguration compatUIConfiguration,
+                          @NonNull Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>>
+                                  onRestartButtonClicked) {
         super(context, taskInfo, syncQueue, taskListener, displayLayout);
         mCallback = callback;
         mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
@@ -122,7 +132,7 @@
         updateVisibilityOfViews();
 
         if (mHasSizeCompat) {
-            mCallback.onSizeCompatRestartButtonAppeared(mTaskId);
+            mCallback.accept(new SizeCompatRestartButtonAppeared(mTaskId));
         }
 
         return mLayout;
@@ -177,7 +187,7 @@
                 mCameraCompatControlState == CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED
                         ? CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED
                         : CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-        mCallback.onCameraControlStateUpdated(mTaskId, mCameraCompatControlState);
+        mCallback.accept(new CameraControlStateUpdated(mTaskId, mCameraCompatControlState));
         mLayout.updateCameraTreatmentButton(mCameraCompatControlState);
     }
 
@@ -188,7 +198,7 @@
             return;
         }
         mCameraCompatControlState = CAMERA_COMPAT_CONTROL_DISMISSED;
-        mCallback.onCameraControlStateUpdated(mTaskId, CAMERA_COMPAT_CONTROL_DISMISSED);
+        mCallback.accept(new CameraControlStateUpdated(mTaskId, CAMERA_COMPAT_CONTROL_DISMISSED));
         mLayout.setCameraControlVisibility(/* show= */ false);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIEvent.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIEvent.kt
new file mode 100644
index 0000000..4a0cf98
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIEvent.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.compatui.api
+
+/**
+ * Abstraction for all the possible Compat UI Component events.
+ */
+interface CompatUIEvent {
+    /**
+     * Unique event identifier
+     */
+    val eventId: Int
+
+    @Suppress("UNCHECKED_CAST")
+    fun <T : CompatUIEvent> asType(): T? = this as? T
+
+    fun <T : CompatUIEvent> asType(clazz: Class<T>): T? {
+        return if (clazz.isInstance(this)) clazz.cast(this) else null
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt
new file mode 100644
index 0000000..817e554
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.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.compatui.api
+
+import java.util.function.Consumer
+
+/**
+ * Abstraction for the objects responsible to handle all the CompatUI components and the
+ * communication with the server.
+ */
+interface CompatUIHandler {
+    /**
+     * Invoked when a new model is coming from the server.
+     */
+    fun onCompatInfoChanged(compatUIInfo: CompatUIInfo)
+
+    /**
+     * Optional reference to the object responsible to send {@link CompatUIEvent}
+     */
+    fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?)
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIInfo.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIInfo.kt
new file mode 100644
index 0000000..dbbf049
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIInfo.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.api
+
+import android.app.TaskInfo
+import com.android.wm.shell.ShellTaskOrganizer
+
+/**
+ * Encapsulate the info of the message from core.
+ */
+data class CompatUIInfo(val taskInfo: TaskInfo, val listener: ShellTaskOrganizer.TaskListener?)
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIEvents.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIEvents.kt
new file mode 100644
index 0000000..58ce8ed
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIEvents.kt
@@ -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.wm.shell.compatui.impl
+
+import android.app.AppCompatTaskInfo
+import android.app.CameraCompatTaskInfo
+import com.android.wm.shell.compatui.api.CompatUIEvent
+
+internal const val SIZE_COMPAT_RESTART_BUTTON_APPEARED = 0
+internal const val SIZE_COMPAT_RESTART_BUTTON_CLICKED = 1
+internal const val CAMERA_CONTROL_STATE_UPDATE = 2
+
+/**
+ * All the {@link CompatUIEvent} the Compat UI Framework can handle
+ */
+sealed class CompatUIEvents(override val eventId: Int) : CompatUIEvent {
+    /** Sent when the size compat restart button appears. */
+    data class SizeCompatRestartButtonAppeared(val taskId: Int) :
+            CompatUIEvents(SIZE_COMPAT_RESTART_BUTTON_APPEARED)
+
+    /** Sent when the size compat restart button is clicked. */
+    data class SizeCompatRestartButtonClicked(val taskId: Int) :
+            CompatUIEvents(SIZE_COMPAT_RESTART_BUTTON_CLICKED)
+
+    /** Sent when the camera compat control state is updated. */
+    data class CameraControlStateUpdated(
+            val taskId: Int,
+            @CameraCompatTaskInfo.CameraCompatControlState val state: Int
+    ) : CompatUIEvents(CAMERA_CONTROL_STATE_UPDATE)
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
new file mode 100644
index 0000000..a181eaf
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.impl
+
+import com.android.wm.shell.compatui.api.CompatUIEvent
+import com.android.wm.shell.compatui.api.CompatUIHandler
+import com.android.wm.shell.compatui.api.CompatUIInfo
+import java.util.function.Consumer
+
+/**
+ * Default implementation of {@link CompatUIHandler} to handle CompatUI components
+ */
+class DefaultCompatUIHandler : CompatUIHandler {
+
+    private var compatUIEventSender: Consumer<CompatUIEvent>? = null
+    override fun onCompatInfoChanged(compatUIInfo: CompatUIInfo) {
+        // Empty at the moment
+    }
+
+    override fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?) {
+        this.compatUIEventSender = compatUIEventSender
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 609e5af..9bdc0b2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -71,6 +71,8 @@
 import com.android.wm.shell.compatui.CompatUIConfiguration;
 import com.android.wm.shell.compatui.CompatUIController;
 import com.android.wm.shell.compatui.CompatUIShellCommandHandler;
+import com.android.wm.shell.compatui.api.CompatUIHandler;
+import com.android.wm.shell.compatui.impl.DefaultCompatUIHandler;
 import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
@@ -88,12 +90,12 @@
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.recents.RecentsTransitionHandler;
 import com.android.wm.shell.recents.TaskStackTransitionObserver;
-import com.android.wm.shell.shared.DesktopModeStatus;
 import com.android.wm.shell.shared.ShellTransitions;
 import com.android.wm.shell.shared.annotations.ShellAnimationThread;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.shared.annotations.ShellSplashscreenThread;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.startingsurface.StartingSurface;
@@ -211,7 +213,7 @@
             Context context,
             ShellInit shellInit,
             ShellCommandHandler shellCommandHandler,
-            Optional<CompatUIController> compatUI,
+            Optional<CompatUIHandler> compatUI,
             Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<RecentTasksController> recentTasksOptional,
             @ShellMainThread ShellExecutor mainExecutor) {
@@ -230,7 +232,7 @@
 
     @WMSingleton
     @Provides
-    static Optional<CompatUIController> provideCompatUIController(
+    static Optional<CompatUIHandler> provideCompatUIController(
             Context context,
             ShellInit shellInit,
             ShellController shellController,
@@ -247,6 +249,9 @@
         if (!context.getResources().getBoolean(R.bool.config_enableCompatUIController)) {
             return Optional.empty();
         }
+        if (Flags.appCompatUiFramework()) {
+            return Optional.of(new DefaultCompatUIHandler());
+        }
         return Optional.of(
                 new CompatUIController(
                         context,
@@ -898,7 +903,7 @@
         // Use optional-of-lazy for the dependency that this provider relies on.
         // Lazy ensures that this provider will not be the cause the dependency is created
         // when it will not be returned due to the condition below.
-        return desktopTasksController.flatMap((lazy)-> {
+        return desktopTasksController.flatMap((lazy) -> {
             if (DesktopModeStatus.canEnterDesktopMode(context)) {
                 return Optional.of(lazy.get());
             }
@@ -917,7 +922,7 @@
         // Use optional-of-lazy for the dependency that this provider relies on.
         // Lazy ensures that this provider will not be the cause the dependency is created
         // when it will not be returned due to the condition below.
-        return desktopModeTaskRepository.flatMap((lazy)-> {
+        return desktopModeTaskRepository.flatMap((lazy) -> {
             if (DesktopModeStatus.canEnterDesktopMode(context)) {
                 return Optional.of(lazy.get());
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index e792f7a..aa499d9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -77,10 +77,10 @@
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.recents.RecentsTransitionHandler;
-import com.android.wm.shell.shared.DesktopModeStatus;
 import com.android.wm.shell.shared.annotations.ShellAnimationThread;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
@@ -585,9 +585,10 @@
     @Provides
     static ExitDesktopTaskTransitionHandler provideExitDesktopTaskTransitionHandler(
             Transitions transitions,
-            Context context
-    ) {
-        return new ExitDesktopTaskTransitionHandler(transitions, context);
+            Context context,
+            InteractionJankMonitor interactionJankMonitor) {
+        return new ExitDesktopTaskTransitionHandler(
+            transitions, context, interactionJankMonitor);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index a67dee3..275f725d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -46,7 +46,7 @@
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
-import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.shared.TransitionUtil
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index e247912..985901d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -74,9 +74,9 @@
 import com.android.wm.shell.recents.RecentTasksController
 import com.android.wm.shell.recents.RecentsTransitionHandler
 import com.android.wm.shell.recents.RecentsTransitionStateListener
-import com.android.wm.shell.shared.DesktopModeStatus
-import com.android.wm.shell.shared.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE
-import com.android.wm.shell.shared.DesktopModeStatus.useDesktopOverrideDensity
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useDesktopOverrideDensity
 import com.android.wm.shell.shared.TransitionUtil
 import com.android.wm.shell.shared.annotations.ExternalThread
 import com.android.wm.shell.shared.annotations.ShellMainThread
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index c85f76d..3f5bd1a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -25,7 +25,7 @@
 import androidx.annotation.VisibleForTesting
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.protolog.ShellProtoLogGroup
-import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.TransitionObserver
 import com.android.wm.shell.util.KtProtoLog
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index dae75f9..f01f645 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -23,7 +23,7 @@
 import android.window.TransitionInfo
 import com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
-import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.util.KtProtoLog
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
index 891f75c..171378f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
@@ -42,6 +42,8 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.Cuj;
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
 import com.android.wm.shell.transition.Transitions;
 
@@ -60,6 +62,7 @@
 
     private final Context mContext;
     private final Transitions mTransitions;
+    private final InteractionJankMonitor mInteractionJankMonitor;
     private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
     private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback;
     private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
@@ -67,17 +70,21 @@
 
     public ExitDesktopTaskTransitionHandler(
             Transitions transitions,
-            Context context) {
-        this(transitions, SurfaceControl.Transaction::new, context);
+            Context context,
+            InteractionJankMonitor interactionJankMonitor
+            ) {
+        this(transitions, SurfaceControl.Transaction::new, context, interactionJankMonitor);
     }
 
     private ExitDesktopTaskTransitionHandler(
             Transitions transitions,
             Supplier<SurfaceControl.Transaction> supplier,
-            Context context) {
+            Context context,
+            InteractionJankMonitor interactionJankMonitor) {
         mTransitions = transitions;
         mTransactionSupplier = supplier;
         mContext = context;
+        mInteractionJankMonitor = interactionJankMonitor;
     }
 
     /**
@@ -146,6 +153,8 @@
             final int screenHeight = metrics.heightPixels;
             final SurfaceControl sc = change.getLeash();
             final Rect endBounds = change.getEndAbsBounds();
+            mInteractionJankMonitor
+                .begin(sc, mContext, Cuj.CUJ_DESKTOP_MODE_EXIT_MODE);
             // Hide the first (fullscreen) frame because the animation will start from the freeform
             // size.
             startT.hide(sc)
@@ -175,6 +184,7 @@
                     if (mOnAnimationFinishedCallback != null) {
                         mOnAnimationFinishedCallback.accept(finishT);
                     }
+                    mInteractionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_EXIT_MODE);
                     mTransitions.getMainExecutor().execute(
                             () -> finishCallback.onTransitionFinished(null));
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
index b1cbe8d..3572d16 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
@@ -97,7 +97,7 @@
 adb logcat -s "SurfaceControlRegistry"
 ```
 
-## Tracing activity starts in the app process
+## Tracing activity starts & finishes in the app process
 
 It's sometimes useful to know when to see a stack trace of when an activity starts in the app code
 (ie. if you are repro'ing a bug related to activity starts). You can enable this system property to
@@ -113,6 +113,19 @@
 adb reboot
 ```
 
+Likewise, to trace where a finish() call may be made in the app process, you can enable this system
+property:
+```shell
+# Enabling
+adb shell setprop persist.wm.debug.finish_activity true
+adb reboot
+adb logcat -s "Instrumentation"
+
+# Disabling
+adb shell setprop persist.wm.debug.finish_activity \"\"
+adb reboot
+```
+
 ## Dumps
 
 Because the Shell library is built as a part of SystemUI, dumping the state is currently done as a
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index b3c3a3d..e0b0866 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
@@ -52,6 +52,7 @@
 import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
+import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
 import androidx.annotation.BinderThread;
@@ -353,6 +354,12 @@
                 pd.dragSession.initialize();
                 pd.activeDragCount++;
                 pd.dragLayout.prepare(pd.dragSession, mLogger.logStart(pd.dragSession));
+                if (pd.dragSession.hideDragSourceTaskId != -1) {
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+                            "Hiding task surface: taskId=%d", pd.dragSession.hideDragSourceTaskId);
+                    mShellTaskOrganizer.setTaskSurfaceVisibility(
+                            pd.dragSession.hideDragSourceTaskId, false /* visible */);
+                }
                 setDropTargetWindowVisibility(pd, View.VISIBLE);
                 notifyListeners(l -> {
                     l.onDragStarted();
@@ -382,6 +389,13 @@
                 if (pd.dragLayout.hasDropped()) {
                     mLogger.logDrop();
                 } else {
+                    if (pd.dragSession.hideDragSourceTaskId != -1) {
+                        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+                                "Re-showing task surface: taskId=%d",
+                                pd.dragSession.hideDragSourceTaskId);
+                        mShellTaskOrganizer.setTaskSurfaceVisibility(
+                                pd.dragSession.hideDragSourceTaskId, true /* visible */);
+                    }
                     pd.activeDragCount--;
                     pd.dragLayout.hide(event, () -> {
                         if (pd.activeDragCount == 0) {
@@ -435,7 +449,16 @@
     private boolean handleDrop(DragEvent event, PerDisplay pd) {
         final SurfaceControl dragSurface = event.getDragSurface();
         pd.activeDragCount--;
-        return pd.dragLayout.drop(event, dragSurface, () -> {
+        // Find the token of the task to hide as a part of entering split
+        WindowContainerToken hideTaskToken = null;
+        if (pd.dragSession.hideDragSourceTaskId != -1) {
+            ActivityManager.RunningTaskInfo info = mShellTaskOrganizer.getRunningTaskInfo(
+                    pd.dragSession.hideDragSourceTaskId);
+            if (info != null) {
+                hideTaskToken = info.token;
+            }
+        }
+        return pd.dragLayout.drop(event, dragSurface, hideTaskToken, () -> {
             if (pd.activeDragCount == 0) {
                 // Hide the window if another drag hasn't been started while animating the drop
                 setDropTargetWindowVisibility(pd, View.INVISIBLE);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
index 9c7476d..95fe8b6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
@@ -59,6 +59,7 @@
 import android.os.UserHandle;
 import android.util.Log;
 import android.util.Slog;
+import android.window.WindowContainerToken;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
@@ -234,8 +235,13 @@
         return null;
     }
 
+    /**
+     * Handles the drop on a given {@param target}.  If a {@param hideTaskToken} is set, then the
+     * handling of the drop will attempt to hide the given task as a part of the same window
+     * container transaction if possible.
+     */
     @VisibleForTesting
-    void handleDrop(Target target) {
+    void handleDrop(Target target, @Nullable WindowContainerToken hideTaskToken) {
         if (target == null || !mTargets.contains(target)) {
             return;
         }
@@ -254,16 +260,17 @@
                 ? mFullscreenStarter
                 : mSplitscreenStarter;
         if (mSession.appData != null) {
-            launchApp(mSession, starter, position);
+            launchApp(mSession, starter, position, hideTaskToken);
         } else {
-            launchIntent(mSession, starter, position);
+            launchIntent(mSession, starter, position, hideTaskToken);
         }
     }
 
     /**
      * Launches an app provided by SysUI.
      */
-    private void launchApp(DragSession session, Starter starter, @SplitPosition int position) {
+    private void launchApp(DragSession session, Starter starter, @SplitPosition int position,
+            @Nullable WindowContainerToken hideTaskToken) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching app data at position=%d",
                 position);
         final ClipDescription description = session.getClipDescription();
@@ -283,8 +290,12 @@
 
         if (isTask) {
             final int taskId = session.appData.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID);
-            starter.startTask(taskId, position, opts);
+            starter.startTask(taskId, position, opts, hideTaskToken);
         } else if (isShortcut) {
+            if (hideTaskToken != null) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+                        "Can not hide task token with starting shortcut");
+            }
             final String packageName = session.appData.getStringExtra(EXTRA_PACKAGE_NAME);
             final String id = session.appData.getStringExtra(EXTRA_SHORTCUT_ID);
             starter.startShortcut(packageName, id, position, opts, user);
@@ -297,14 +308,15 @@
                 }
             }
             starter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */,
-                    position, opts);
+                    position, opts, hideTaskToken);
         }
     }
 
     /**
      * Launches an intent sender provided by an application.
      */
-    private void launchIntent(DragSession session, Starter starter, @SplitPosition int position) {
+    private void launchIntent(DragSession session, Starter starter, @SplitPosition int position,
+            @Nullable WindowContainerToken hideTaskToken) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching intent at position=%d",
                 position);
         final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic();
@@ -319,18 +331,20 @@
         final Bundle opts = baseActivityOpts.toBundle();
         starter.startIntent(session.launchableIntent,
                 session.launchableIntent.getCreatorUserHandle().getIdentifier(),
-                null /* fillIntent */, position, opts);
+                null /* fillIntent */, position, opts, hideTaskToken);
     }
 
     /**
      * Interface for actually committing the task launches.
      */
     public interface Starter {
-        void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options);
+        void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options,
+                @Nullable WindowContainerToken hideTaskToken);
         void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
                 @Nullable Bundle options, UserHandle user);
         void startIntent(PendingIntent intent, int userId, Intent fillInIntent,
-                @SplitPosition int position, @Nullable Bundle options);
+                @SplitPosition int position, @Nullable Bundle options,
+                @Nullable WindowContainerToken hideTaskToken);
         void enterSplitScreen(int taskId, boolean leftOrTop);
 
         /**
@@ -352,7 +366,12 @@
         }
 
         @Override
-        public void startTask(int taskId, int position, @Nullable Bundle options) {
+        public void startTask(int taskId, int position, @Nullable Bundle options,
+                @Nullable WindowContainerToken hideTaskToken) {
+            if (hideTaskToken != null) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+                        "Default starter does not support hide task token");
+            }
             try {
                 ActivityTaskManager.getService().startActivityFromRecents(taskId, options);
             } catch (RemoteException e) {
@@ -375,7 +394,12 @@
 
         @Override
         public void startIntent(PendingIntent intent, int userId, @Nullable Intent fillInIntent,
-                int position, @Nullable Bundle options) {
+                int position, @Nullable Bundle options,
+                @Nullable WindowContainerToken hideTaskToken) {
+            if (hideTaskToken != null) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+                        "Default starter does not support hide task token");
+            }
             try {
                 intent.send(mContext, 0, fillInIntent, null, null, null, options);
             } catch (PendingIntent.CanceledException e) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index 910175e..f0514e3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -51,8 +51,10 @@
 import android.view.WindowInsets;
 import android.view.WindowInsets.Type;
 import android.widget.LinearLayout;
+import android.window.WindowContainerToken;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.internal.logging.InstanceId;
 import com.android.internal.protolog.ProtoLog;
@@ -483,13 +485,13 @@
     /**
      * Handles the drop onto a target and animates out the visible drop targets.
      */
-    public boolean drop(DragEvent event, SurfaceControl dragSurface,
-            Runnable dropCompleteCallback) {
+    public boolean drop(DragEvent event, @NonNull SurfaceControl dragSurface,
+            @Nullable WindowContainerToken hideTaskToken, Runnable dropCompleteCallback) {
         final boolean handledDrop = mCurrentTarget != null;
         mHasDropped = true;
 
         // Process the drop
-        mPolicy.handleDrop(mCurrentTarget);
+        mPolicy.handleDrop(mCurrentTarget, hideTaskToken);
 
         // Start animating the drop UI out with the drag surface
         hide(event, dropCompleteCallback);
@@ -499,7 +501,7 @@
         return handledDrop;
     }
 
-    private void hideDragSurface(SurfaceControl dragSurface) {
+    private void hideDragSurface(@NonNull SurfaceControl dragSurface) {
         final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
         final ValueAnimator dragSurfaceAnimator = ValueAnimator.ofFloat(0f, 1f);
         // Currently the splash icon animation runs with the default ValueAnimator duration of
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java
index 3bedef2..dcbdfa3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.ClipDescription.EXTRA_HIDE_DRAG_SOURCE_TASK_ID;
 
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
@@ -27,6 +28,7 @@
 import android.content.ClipDescription;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
+import android.os.PersistableBundle;
 
 import androidx.annotation.Nullable;
 
@@ -63,6 +65,7 @@
     @WindowConfiguration.ActivityType
     int runningTaskActType = ACTIVITY_TYPE_STANDARD;
     boolean dragItemSupportsSplitscreen;
+    int hideDragSourceTaskId = -1;
 
     DragSession(ActivityTaskManager activityTaskManager,
             DisplayLayout dispLayout, ClipData data, int dragFlags) {
@@ -70,6 +73,11 @@
         mInitialDragData = data;
         mInitialDragFlags = dragFlags;
         displayLayout = dispLayout;
+        hideDragSourceTaskId = data.getDescription().getExtras() != null
+                ? data.getDescription().getExtras().getInt(EXTRA_HIDE_DRAG_SOURCE_TASK_ID, -1)
+                : -1;
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+                "Extracting drag source taskId: taskId=%d", hideDragSourceTaskId);
     }
 
     /**
@@ -84,16 +92,27 @@
      * Updates the running task for this drag session.
      */
     void updateRunningTask() {
+        final boolean hideDragSourceTask = hideDragSourceTaskId != -1;
         final List<ActivityManager.RunningTaskInfo> tasks =
-                mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */);
+                mActivityTaskManager.getTasks(hideDragSourceTask ? 2 : 1,
+                        false /* filterOnlyVisibleRecents */);
         if (!tasks.isEmpty()) {
-            final ActivityManager.RunningTaskInfo task = tasks.get(0);
-            runningTaskInfo = task;
-            runningTaskWinMode = task.getWindowingMode();
-            runningTaskActType = task.getActivityType();
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
-                    "Running task: id=%d component=%s", task.taskId,
-                    task.baseIntent != null ? task.baseIntent.getComponent() : "null");
+            for (int i = tasks.size() - 1; i >= 0; i--) {
+                final ActivityManager.RunningTaskInfo task = tasks.get(i);
+                if (hideDragSourceTask && hideDragSourceTaskId == task.taskId) {
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+                            "Skipping running task: id=%d component=%s", task.taskId,
+                            task.baseIntent != null ? task.baseIntent.getComponent() : "null");
+                    continue;
+                }
+                runningTaskInfo = task;
+                runningTaskWinMode = task.getWindowingMode();
+                runningTaskActType = task.getActivityType();
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+                        "Running task: id=%d component=%s", task.taskId,
+                        task.baseIntent != null ? task.baseIntent.getComponent() : "null");
+                break;
+            }
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 1641668..4531967 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -29,7 +29,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 814eaae..9539a45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -37,6 +37,7 @@
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.view.IRecentsAnimationRunner;
+import android.window.WindowContainerToken;
 
 import androidx.annotation.BinderThread;
 import androidx.annotation.NonNull;
@@ -52,9 +53,9 @@
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import com.android.wm.shell.shared.DesktopModeStatus;
 import com.android.wm.shell.shared.annotations.ExternalThread;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
@@ -457,11 +458,31 @@
     }
 
     /**
-     * Find the background task that match the given component.
+     * Returns the top running leaf task ignoring {@param ignoreTaskToken} if it is specified.
+     * NOTE: This path currently makes assumptions that ignoreTaskToken is for the top task.
+     */
+    @Nullable
+    public ActivityManager.RunningTaskInfo getTopRunningTask(
+            @Nullable WindowContainerToken ignoreTaskToken) {
+        List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(2,
+                false /* filterOnlyVisibleRecents */);
+        for (int i = tasks.size() - 1; i >= 0; i--) {
+            final ActivityManager.RunningTaskInfo task = tasks.get(i);
+            if (task.token.equals(ignoreTaskToken)) {
+                continue;
+            }
+            return task;
+        }
+        return null;
+    }
+
+    /**
+     * Find the background task that match the given component.  Ignores tasks match
+     * {@param ignoreTaskToken} if it is non-null.
      */
     @Nullable
     public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName,
-            int userId) {
+            int userId, @Nullable WindowContainerToken ignoreTaskToken) {
         if (componentName == null) {
             return null;
         }
@@ -473,6 +494,9 @@
             if (task.isVisible) {
                 continue;
             }
+            if (task.token.equals(ignoreTaskToken)) {
+                continue;
+            }
             if (componentName.equals(task.baseIntent.getComponent()) && userId == task.userId) {
                 return task;
             }
@@ -640,7 +664,7 @@
         @Override
         public ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) {
             final ActivityManager.RunningTaskInfo[][] tasks =
-                    new ActivityManager.RunningTaskInfo[][] {null};
+                    new ActivityManager.RunningTaskInfo[][]{null};
             executeRemoteCallWithTaskPermission(mController, "getRunningTasks",
                     (controller) -> tasks[0] = ActivityTaskManager.getInstance().getTasks(maxNum)
                             .toArray(new ActivityManager.RunningTaskInfo[0]),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index b4941a5..e659151 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -64,6 +64,7 @@
 import android.view.WindowManager;
 import android.widget.Toast;
 import android.window.RemoteTransition;
+import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
 import androidx.annotation.BinderThread;
@@ -526,7 +527,15 @@
         mStageCoordinator.requestEnterSplitSelect(taskInfo, wct, splitPosition, taskBounds);
     }
 
-    public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) {
+    /**
+     * Starts an existing task into split.
+     * TODO(b/351900580): We should remove this path and use StageCoordinator#startTask() instead
+     * @param hideTaskToken is not supported.
+     */
+    public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options,
+            @Nullable WindowContainerToken hideTaskToken) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+                "Legacy startTask does not support hide task token");
         final int[] result = new int[1];
         IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
             @Override
@@ -584,8 +593,8 @@
         if (options == null) options = new Bundle();
         final ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
 
-        if (samePackage(packageName, getPackageName(reverseSplitPosition(position)),
-                user.getIdentifier(), getUserId(reverseSplitPosition(position)))) {
+        if (samePackage(packageName, getPackageName(reverseSplitPosition(position), null),
+                user.getIdentifier(), getUserId(reverseSplitPosition(position), null))) {
             if (mMultiInstanceHelpher.supportsMultiInstanceSplit(
                     getShortcutComponent(packageName, shortcutId, user, mLauncherApps))) {
                 activityOptions.setApplyMultipleTaskFlagForShortcut(true);
@@ -676,10 +685,11 @@
      * See {@link #startIntent(PendingIntent, int, Intent, int, Bundle)}
      * @param instanceId to be used by {@link SplitscreenEventLogger}
      */
-    public void startIntent(PendingIntent intent, int userId, @Nullable Intent fillInIntent,
-            @SplitPosition int position, @Nullable Bundle options, @NonNull InstanceId instanceId) {
+    public void startIntentWithInstanceId(PendingIntent intent, int userId,
+            @Nullable Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options,
+            @NonNull InstanceId instanceId) {
         mStageCoordinator.onRequestToSplit(instanceId, ENTER_REASON_LAUNCHER);
-        startIntent(intent, userId, fillInIntent, position, options);
+        startIntent(intent, userId, fillInIntent, position, options, null /* hideTaskToken */);
     }
 
     private void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, int userId1,
@@ -825,9 +835,15 @@
                 instanceId);
     }
 
+    /**
+     * Starts the given intent into split.
+     * @param hideTaskToken If non-null, a task matching this token will be moved to back in the
+     *                      same window container transaction as the starting of the intent.
+     */
     @Override
     public void startIntent(PendingIntent intent, int userId1, @Nullable Intent fillInIntent,
-            @SplitPosition int position, @Nullable Bundle options) {
+            @SplitPosition int position, @Nullable Bundle options,
+            @Nullable WindowContainerToken hideTaskToken) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                 "startIntent(): intent=%s user=%d fillInIntent=%s position=%d", intent, userId1,
                 fillInIntent, position);
@@ -838,23 +854,24 @@
         fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
 
         final String packageName1 = SplitScreenUtils.getPackageName(intent);
-        final String packageName2 = getPackageName(reverseSplitPosition(position));
-        final int userId2 = getUserId(reverseSplitPosition(position));
+        final String packageName2 = getPackageName(reverseSplitPosition(position), hideTaskToken);
+        final int userId2 = getUserId(reverseSplitPosition(position), hideTaskToken);
         final ComponentName component = intent.getIntent().getComponent();
 
         // To prevent accumulating large number of instances in the background, reuse task
         // in the background. If we don't explicitly reuse, new may be created even if the app
         // isn't multi-instance because WM won't automatically remove/reuse the previous instance
         final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
-                .map(recentTasks -> recentTasks.findTaskInBackground(component, userId1))
+                .map(recentTasks -> recentTasks.findTaskInBackground(component, userId1,
+                        hideTaskToken))
                 .orElse(null);
         if (taskInfo != null) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                     "Found suitable background task=%s", taskInfo);
             if (ENABLE_SHELL_TRANSITIONS) {
-                mStageCoordinator.startTask(taskInfo.taskId, position, options);
+                mStageCoordinator.startTask(taskInfo.taskId, position, options, hideTaskToken);
             } else {
-                startTask(taskInfo.taskId, position, options);
+                startTask(taskInfo.taskId, position, options, hideTaskToken);
             }
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Start task in background");
             return;
@@ -879,19 +896,23 @@
             }
         }
 
-        mStageCoordinator.startIntent(intent, fillInIntent, position, options);
+        mStageCoordinator.startIntent(intent, fillInIntent, position, options, hideTaskToken);
     }
 
-    /** Retrieve package name of a specific split position if split screen is activated, otherwise
-     *  returns the package name of the top running task. */
+    /**
+     * Retrieve package name of a specific split position if split screen is activated, otherwise
+     * returns the package name of the top running task.
+     * TODO(b/351900580): Merge this with getUserId() so we don't make multiple binder calls
+     */
     @Nullable
-    private String getPackageName(@SplitPosition int position) {
+    private String getPackageName(@SplitPosition int position,
+            @Nullable WindowContainerToken ignoreTaskToken) {
         ActivityManager.RunningTaskInfo taskInfo;
         if (isSplitScreenVisible()) {
             taskInfo = getTaskInfo(position);
         } else {
             taskInfo = mRecentTasksOptional
-                    .map(recentTasks -> recentTasks.getTopRunningTask())
+                    .map(recentTasks -> recentTasks.getTopRunningTask(ignoreTaskToken))
                     .orElse(null);
             if (!isValidToSplit(taskInfo)) {
                 return null;
@@ -901,15 +922,19 @@
         return taskInfo != null ? SplitScreenUtils.getPackageName(taskInfo.baseIntent) : null;
     }
 
-    /** Retrieve user id of a specific split position if split screen is activated, otherwise
-     *  returns the user id of the top running task. */
-    private int getUserId(@SplitPosition int position) {
+    /**
+     * Retrieve user id of a specific split position if split screen is activated, otherwise
+     * returns the user id of the top running task.
+     * TODO: Merge this with getPackageName() so we don't make multiple binder calls
+     */
+    private int getUserId(@SplitPosition int position,
+            @Nullable WindowContainerToken ignoreTaskToken) {
         ActivityManager.RunningTaskInfo taskInfo;
         if (isSplitScreenVisible()) {
             taskInfo = getTaskInfo(position);
         } else {
             taskInfo = mRecentTasksOptional
-                    .map(recentTasks -> recentTasks.getTopRunningTask())
+                    .map(recentTasks -> recentTasks.getTopRunningTask(ignoreTaskToken))
                     .orElse(null);
             if (!isValidToSplit(taskInfo)) {
                 return -1;
@@ -1290,7 +1315,8 @@
         @Override
         public void startTask(int taskId, int position, @Nullable Bundle options) {
             executeRemoteCallWithTaskPermission(mController, "startTask",
-                    (controller) -> controller.startTask(taskId, position, options));
+                    (controller) -> controller.startTask(taskId, position, options,
+                            null /* hideTaskToken */));
         }
 
         @Override
@@ -1402,8 +1428,8 @@
         public void startIntent(PendingIntent intent, int userId, Intent fillInIntent, int position,
                 @Nullable Bundle options, InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController, "startIntent",
-                    (controller) -> controller.startIntent(intent, userId, fillInIntent, position,
-                            options, instanceId));
+                    (controller) -> controller.startIntentWithInstanceId(intent, userId,
+                            fillInIntent, position, options, instanceId));
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index d9e9776..4104234 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -592,12 +592,21 @@
         }
     }
 
-    /** Use this method to launch an existing Task via a taskId */
-    void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) {
+    /**
+     * Use this method to launch an existing Task via a taskId.
+     * @param hideTaskToken If non-null, a task matching this token will be moved to back in the
+     *                      same window container transaction as the starting of the intent.
+     */
+    void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options,
+            @Nullable WindowContainerToken hideTaskToken) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startTask: task=%d position=%d", taskId, position);
         mSplitRequest = new SplitRequest(taskId, position);
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
+        if (hideTaskToken != null) {
+            ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Reordering hide-task to bottom");
+            wct.reorder(hideTaskToken, false /* onTop */);
+        }
         wct.startTask(taskId, options);
         // If this should be mixed, send the task to avoid split handle transition directly.
         if (mMixedHandler != null && mMixedHandler.isTaskInPip(taskId, mTaskOrganizer)) {
@@ -623,9 +632,13 @@
                 extraTransitType, !mIsDropEntering);
     }
 
-    /** Launches an activity into split. */
+    /**
+     * Launches an activity into split.
+     * @param hideTaskToken If non-null, a task matching this token will be moved to back in the
+     *                      same window container transaction as the starting of the intent.
+     */
     void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
-            @Nullable Bundle options) {
+            @Nullable Bundle options, @Nullable WindowContainerToken hideTaskToken) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startIntent: intent=%s position=%d", intent.getIntent(),
                 position);
         mSplitRequest = new SplitRequest(intent.getIntent(), position);
@@ -636,6 +649,10 @@
 
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
+        if (hideTaskToken != null) {
+            ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Reordering hide-task to bottom");
+            wct.reorder(hideTaskToken, false /* onTop */);
+        }
         wct.sendPendingIntent(intent, fillInIntent, options);
 
         // If this should be mixed, just send the intent to avoid split handle transition directly.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
index dd4595a..3a68009 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
@@ -27,7 +27,6 @@
 
 import com.android.internal.protolog.ProtoLog;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 import java.util.ArrayList;
 
@@ -76,7 +75,6 @@
      */
     @VisibleForTesting
     public void init() {
-        ProtoLog.registerGroups(ShellProtoLogGroup.values());
         ProtoLog.v(WM_SHELL_INIT, "Initializing Shell Components: %d", mInitCallbacks.size());
         SurfaceControl.setDebugUsageAfterRelease(true);
         // Init in order of registration
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 440fc4d..1be33e5 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
@@ -91,7 +91,7 @@
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
 import com.android.wm.shell.desktopmode.DesktopWallpaperActivity;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
-import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreen.StageType;
 import com.android.wm.shell.splitscreen.SplitScreenController;
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 2a95fa3..b62194c 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
@@ -68,7 +68,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener;
 import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index d48ce53..2fd3eaa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -33,7 +33,7 @@
 import com.android.window.flags.Flags;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 /**
  * Utility class that contains logic common to classes implementing {@link DragPositioningCallback}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 216990c..03dbbb3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -55,7 +55,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams.OccludingCaptionElement;
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer;
 
diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS
index b8a19ad..a77fd51 100644
--- a/libs/WindowManager/Shell/tests/OWNERS
+++ b/libs/WindowManager/Shell/tests/OWNERS
@@ -9,7 +9,7 @@
 chenghsiuchang@google.com
 atsjenk@google.com
 jorgegil@google.com
-nmusgrave@google.com
+vaniadesmonda@google.com
 pbdr@google.com
 tkachenkoi@google.com
 mpodolian@google.com
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/MaximizeAppWindow.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/MaximizeAppWindow.kt
new file mode 100644
index 0000000..20e2167c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/MaximizeAppWindow.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.scenarios
+
+import android.app.Instrumentation
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import com.android.wm.shell.flicker.service.common.Utils
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+/** Base scenario test for maximize app window CUJ in desktop mode. */
+@Ignore("Base Test Class")
+abstract class MaximizeAppWindow
+{
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+
+    @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL,
+        Rotation.ROTATION_0)
+
+    @Before
+    fun setup() {
+        Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+        testApp.enterDesktopWithDrag(wmHelper, device)
+    }
+
+    @Test
+    open fun maximizeAppWindow() {
+        testApp.maximiseDesktopApp(wmHelper, device)
+    }
+
+    @After
+    fun teardown() {
+        testApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index 92be4f9..6b69542 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -59,6 +59,7 @@
         "guava-android-testlib",
         "com.android.window.flags.window-aconfig-java",
         "platform-test-annotations",
+        "flag-junit",
     ],
 
     libs: [
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index 8303317..e91828b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -45,6 +45,7 @@
 import static org.mockito.Mockito.verify;
 
 import android.app.ActivityManager.RunningTaskInfo;
+import android.app.TaskInfo;
 import android.content.LocusId;
 import android.content.pm.ParceledListSlice;
 import android.os.Binder;
@@ -63,6 +64,8 @@
 
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.compatui.CompatUIController;
+import com.android.wm.shell.compatui.api.CompatUIInfo;
+import com.android.wm.shell.compatui.impl.CompatUIEvents;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellInit;
@@ -70,6 +73,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -371,7 +375,7 @@
         mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null);
 
         // sizeCompatActivity is null if top activity is not in size compat.
-        verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo1, null /* taskListener */);
 
         // sizeCompatActivity is non-null if top activity is in size compat.
         clearInvocations(mCompatUI);
@@ -381,7 +385,7 @@
         taskInfo2.appCompatTaskInfo.topActivityInSizeCompat = true;
         taskInfo2.isVisible = true;
         mOrganizer.onTaskInfoChanged(taskInfo2);
-        verify(mCompatUI).onCompatInfoChanged(taskInfo2, taskListener);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo2, taskListener);
 
         // Not show size compat UI if task is not visible.
         clearInvocations(mCompatUI);
@@ -391,11 +395,11 @@
         taskInfo3.appCompatTaskInfo.topActivityInSizeCompat = true;
         taskInfo3.isVisible = false;
         mOrganizer.onTaskInfoChanged(taskInfo3);
-        verify(mCompatUI).onCompatInfoChanged(taskInfo3, null /* taskListener */);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo3, null /* taskListener */);
 
         clearInvocations(mCompatUI);
         mOrganizer.onTaskVanished(taskInfo1);
-        verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo1, null /* taskListener */);
     }
 
     @Test
@@ -410,7 +414,7 @@
 
         // Task listener sent to compat UI is null if top activity isn't eligible for letterbox
         // education.
-        verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo1, null /* taskListener */);
 
         // Task listener is non-null if top activity is eligible for letterbox education and task
         // is visible.
@@ -421,7 +425,7 @@
         taskInfo2.appCompatTaskInfo.topActivityEligibleForLetterboxEducation = true;
         taskInfo2.isVisible = true;
         mOrganizer.onTaskInfoChanged(taskInfo2);
-        verify(mCompatUI).onCompatInfoChanged(taskInfo2, taskListener);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo2, taskListener);
 
         // Task listener is null if task is invisible.
         clearInvocations(mCompatUI);
@@ -431,11 +435,11 @@
         taskInfo3.appCompatTaskInfo.topActivityEligibleForLetterboxEducation = true;
         taskInfo3.isVisible = false;
         mOrganizer.onTaskInfoChanged(taskInfo3);
-        verify(mCompatUI).onCompatInfoChanged(taskInfo3, null /* taskListener */);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo3, null /* taskListener */);
 
         clearInvocations(mCompatUI);
         mOrganizer.onTaskVanished(taskInfo1);
-        verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo1, null /* taskListener */);
     }
 
     @Test
@@ -451,7 +455,7 @@
 
         // Task listener sent to compat UI is null if top activity doesn't request a camera
         // compat control.
-        verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo1, null /* taskListener */);
 
         // Task listener is non-null when request a camera compat control for a visible task.
         clearInvocations(mCompatUI);
@@ -462,7 +466,7 @@
                 CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
         taskInfo2.isVisible = true;
         mOrganizer.onTaskInfoChanged(taskInfo2);
-        verify(mCompatUI).onCompatInfoChanged(taskInfo2, taskListener);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo2, taskListener);
 
         // CompatUIController#onCompatInfoChanged is called when requested state for a camera
         // compat control changes for a visible task.
@@ -474,7 +478,7 @@
                 CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
         taskInfo3.isVisible = true;
         mOrganizer.onTaskInfoChanged(taskInfo3);
-        verify(mCompatUI).onCompatInfoChanged(taskInfo3, taskListener);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo3, taskListener);
 
         // CompatUIController#onCompatInfoChanged is called when a top activity goes in size compat
         // mode for a visible task that has a compat control.
@@ -487,7 +491,7 @@
                 CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
         taskInfo4.isVisible = true;
         mOrganizer.onTaskInfoChanged(taskInfo4);
-        verify(mCompatUI).onCompatInfoChanged(taskInfo4, taskListener);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo4, taskListener);
 
         // Task linster is null when a camera compat control is dimissed for a visible task.
         clearInvocations(mCompatUI);
@@ -498,7 +502,7 @@
                 CAMERA_COMPAT_CONTROL_DISMISSED;
         taskInfo5.isVisible = true;
         mOrganizer.onTaskInfoChanged(taskInfo5);
-        verify(mCompatUI).onCompatInfoChanged(taskInfo5, null /* taskListener */);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo5, null /* taskListener */);
 
         // Task linster is null when request a camera compat control for a invisible task.
         clearInvocations(mCompatUI);
@@ -509,11 +513,11 @@
                 CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
         taskInfo6.isVisible = false;
         mOrganizer.onTaskInfoChanged(taskInfo6);
-        verify(mCompatUI).onCompatInfoChanged(taskInfo6, null /* taskListener */);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo6, null /* taskListener */);
 
         clearInvocations(mCompatUI);
         mOrganizer.onTaskVanished(taskInfo1);
-        verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */);
+        verifyOnCompatInfoChangedInvokedWith(taskInfo1, null /* taskListener */);
     }
 
     @Test
@@ -640,7 +644,8 @@
 
         mOrganizer.onTaskAppeared(task1, /* leash= */ null);
 
-        mOrganizer.onSizeCompatRestartButtonClicked(task1.taskId);
+        mOrganizer.onSizeCompatRestartButtonClicked(
+                new CompatUIEvents.SizeCompatRestartButtonClicked(task1.taskId));
 
         verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token);
     }
@@ -713,4 +718,13 @@
         taskInfo.isVisible = true;
         return taskInfo;
     }
+
+    private void verifyOnCompatInfoChangedInvokedWith(TaskInfo taskInfo,
+                                                      ShellTaskOrganizer.TaskListener listener) {
+        final ArgumentCaptor<CompatUIInfo> capture = ArgumentCaptor.forClass(CompatUIInfo.class);
+        verify(mCompatUI).onCompatInfoChanged(capture.capture());
+        final CompatUIInfo captureValue = capture.getValue();
+        assertEquals(captureValue.getTaskInfo(), taskInfo);
+        assertEquals(captureValue.getListener(), listener);
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java
index 636c632..f5847cc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java
@@ -70,7 +70,7 @@
                 mContext,
                 configuration, mCallbacks);
         splitWindowManager.init(mSplitLayout, new InsetsState(), false /* isRestoring */);
-        mDividerView = spy((DividerView) splitWindowManager.getDividerView());
+        mDividerView = spy(splitWindowManager.getDividerView());
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 9c00864..fc7a777 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -38,6 +38,9 @@
 import android.app.TaskInfo;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.view.InsetsSource;
 import android.view.InsetsState;
@@ -45,6 +48,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.window.flags.Flags;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayController;
@@ -55,6 +59,7 @@
 import com.android.wm.shell.common.DockStateReader;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.compatui.api.CompatUIInfo;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
@@ -63,6 +68,7 @@
 
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -82,6 +88,10 @@
     private static final int DISPLAY_ID = 0;
     private static final int TASK_ID = 12;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private CompatUIController mController;
     private ShellInit mShellInit;
     @Mock
@@ -168,28 +178,32 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void instantiateController_addInitCallback() {
         verify(mShellInit, times(1)).addInitCallback(any(), any());
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void instantiateController_registerKeyguardChangeListener() {
         verify(mMockShellController, times(1)).addKeyguardChangeListener(any());
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testListenerRegistered() {
         verify(mMockDisplayController).addDisplayWindowListener(mController);
         verify(mMockImeController).addPositionProcessor(mController);
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnCompatInfoChanged() {
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
                 CAMERA_COMPAT_CONTROL_HIDDEN);
 
         // Verify that the compat controls are added with non-null task listener.
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
         verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo),
@@ -202,7 +216,7 @@
         clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController);
         taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
                 CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
                 /* canShow= */ true);
@@ -213,9 +227,9 @@
 
         // Verify that compat controls and letterbox education are removed with null task listener.
         clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController);
-        mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID,
+        mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
                 /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN),
-                /* taskListener= */ null);
+                /* taskListener= */ null));
 
         verify(mMockCompatLayout).release();
         verify(mMockLetterboxEduLayout).release();
@@ -223,6 +237,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnCompatInfoChanged_createLayoutReturnsFalse() {
         doReturn(false).when(mMockCompatLayout).createLayout(anyBoolean());
         doReturn(false).when(mMockLetterboxEduLayout).createLayout(anyBoolean());
@@ -230,7 +245,7 @@
 
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
                 CAMERA_COMPAT_CONTROL_HIDDEN);
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
         verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo),
@@ -240,7 +255,7 @@
 
         // Verify that the layout is created again.
         clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController);
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mMockCompatLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
         verify(mMockLetterboxEduLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
@@ -253,6 +268,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnCompatInfoChanged_updateCompatInfoReturnsFalse() {
         doReturn(false).when(mMockCompatLayout).updateCompatInfo(any(), any(), anyBoolean());
         doReturn(false).when(mMockLetterboxEduLayout).updateCompatInfo(any(), any(), anyBoolean());
@@ -260,7 +276,7 @@
 
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
                 CAMERA_COMPAT_CONTROL_HIDDEN);
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
         verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo),
@@ -270,7 +286,7 @@
 
         clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout,
                 mController);
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
                 /* canShow= */ true);
@@ -282,7 +298,7 @@
         // Verify that the layout is created again.
         clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout,
                 mController);
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mMockCompatLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
         verify(mMockLetterboxEduLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
@@ -296,6 +312,7 @@
 
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnDisplayAdded() {
         mController.onDisplayAdded(DISPLAY_ID);
         mController.onDisplayAdded(DISPLAY_ID + 1);
@@ -305,11 +322,11 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnDisplayRemoved() {
         mController.onDisplayAdded(DISPLAY_ID);
-        mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN),
-                mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
+                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
 
         mController.onDisplayRemoved(DISPLAY_ID + 1);
 
@@ -328,9 +345,10 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnDisplayConfigurationChanged() {
-        mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
+                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
 
         mController.onDisplayConfigurationChanged(DISPLAY_ID + 1, new Configuration());
 
@@ -346,10 +364,11 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testInsetsChanged() {
         mController.onDisplayAdded(DISPLAY_ID);
-        mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
+                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
         InsetsState insetsState = new InsetsState();
         InsetsSource insetsSource = new InsetsSource(
                 InsetsSource.createId(null, 0, navigationBars()), navigationBars());
@@ -373,9 +392,10 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testChangeLayoutsVisibilityOnImeShowHide() {
-        mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
+                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
 
         // Verify that the restart button is hidden after IME is showing.
         mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true);
@@ -387,7 +407,7 @@
         // Verify button remains hidden while IME is showing.
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
                 CAMERA_COMPAT_CONTROL_HIDDEN);
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
                 /* canShow= */ false);
@@ -405,9 +425,10 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testChangeLayoutsVisibilityOnKeyguardShowingChanged() {
-        mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
+                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
 
         // Verify that the restart button is hidden after keyguard becomes showing.
         mController.onKeyguardVisibilityChanged(true, false, false);
@@ -419,7 +440,7 @@
         // Verify button remains hidden while keyguard is showing.
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
                 CAMERA_COMPAT_CONTROL_HIDDEN);
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
                 /* canShow= */ false);
@@ -437,9 +458,10 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testLayoutsRemainHiddenOnKeyguardShowingFalseWhenImeIsShowing() {
-        mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
+                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
 
         mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true);
         mController.onKeyguardVisibilityChanged(true, false, false);
@@ -466,9 +488,10 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testLayoutsRemainHiddenOnImeHideWhenKeyguardIsShowing() {
-        mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID,
-                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID,
+                /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener));
 
         mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true);
         mController.onKeyguardVisibilityChanged(true, false, false);
@@ -495,6 +518,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testRestartLayoutRecreatedIfNeeded() {
         final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID,
                 /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN);
@@ -502,14 +526,15 @@
                 .needsToBeRecreated(any(TaskInfo.class),
                         any(ShellTaskOrganizer.TaskListener.class));
 
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mMockRestartDialogLayout, times(2))
                 .createLayout(anyBoolean());
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testRestartLayoutNotRecreatedIfNotNeeded() {
         final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID,
                 /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN);
@@ -517,14 +542,15 @@
                 .needsToBeRecreated(any(TaskInfo.class),
                         any(ShellTaskOrganizer.TaskListener.class));
 
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mMockRestartDialogLayout, times(1))
                 .createLayout(anyBoolean());
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateActiveTaskInfo_newTask_visibleAndFocused_updated() {
         // Simulate user aspect ratio button being shown for previous task
         mController.setHasShownUserAspectRatioSettingsButton(true);
@@ -545,6 +571,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateActiveTaskInfo_newTask_notVisibleOrFocused_notUpdated() {
         // Create new task
         final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID,
@@ -606,6 +633,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateActiveTaskInfo_sameTask_notUpdated() {
         // Create new task
         final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID,
@@ -634,6 +662,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateActiveTaskInfo_transparentTask_notUpdated() {
         // Create new task
         final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID,
@@ -674,7 +703,7 @@
                 CAMERA_COMPAT_CONTROL_HIDDEN);
         taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = false;
 
-        mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mController, never()).createLetterboxEduWindowManager(any(), eq(taskInfo),
                 eq(mMockTaskListener));
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
index cd3e8cb..33d69f5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
@@ -23,6 +23,7 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
@@ -31,6 +32,9 @@
 import android.app.CameraCompatTaskInfo.CameraCompatControlState;
 import android.app.TaskInfo;
 import android.graphics.Rect;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.util.Pair;
 import android.view.LayoutInflater;
@@ -40,16 +44,20 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.window.flags.Flags;
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
+import com.android.wm.shell.compatui.api.CompatUIEvent;
+import com.android.wm.shell.compatui.impl.CompatUIEvents;
 
 import junit.framework.Assert;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -70,8 +78,12 @@
 
     private static final int TASK_ID = 1;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Mock private SyncTransactionQueue mSyncTransactionQueue;
-    @Mock private CompatUIController.CompatUICallback mCallback;
+    @Mock private Consumer<CompatUIEvent> mCallback;
     @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked;
     @Mock private ShellTaskOrganizer.TaskListener mTaskListener;
     @Mock private SurfaceControlViewHost mViewHost;
@@ -101,6 +113,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnClickForRestartButton() {
         final ImageButton button = mLayout.findViewById(R.id.size_compat_restart_button);
         button.performClick();
@@ -117,6 +130,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnLongClickForRestartButton() {
         doNothing().when(mWindowManager).onRestartButtonLongClicked();
 
@@ -127,6 +141,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnClickForSizeCompatHint() {
         mWindowManager.mHasSizeCompat = true;
         mWindowManager.createLayout(/* canShow= */ true);
@@ -137,6 +152,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateCameraTreatmentButton_treatmentAppliedByDefault() {
         mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
         mWindowManager.createLayout(/* canShow= */ true);
@@ -145,16 +161,17 @@
         button.performClick();
 
         verify(mWindowManager).onCameraTreatmentButtonClicked();
-        verify(mCallback).onCameraControlStateUpdated(
-                TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
+        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID,
+                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
 
         button.performClick();
 
-        verify(mCallback).onCameraControlStateUpdated(
-                TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
+        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID,
+                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateCameraTreatmentButton_treatmentSuggestedByDefault() {
         mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
         mWindowManager.createLayout(/* canShow= */ true);
@@ -163,16 +180,17 @@
         button.performClick();
 
         verify(mWindowManager).onCameraTreatmentButtonClicked();
-        verify(mCallback).onCameraControlStateUpdated(
-                TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
+        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID,
+                CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
 
         button.performClick();
 
-        verify(mCallback).onCameraControlStateUpdated(
-                TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
+        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID,
+                CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnCameraDismissButtonClicked() {
         mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
         mWindowManager.createLayout(/* canShow= */ true);
@@ -181,12 +199,12 @@
         button.performClick();
 
         verify(mWindowManager).onCameraDismissButtonClicked();
-        verify(mCallback).onCameraControlStateUpdated(
-                TASK_ID, CAMERA_COMPAT_CONTROL_DISMISSED);
+        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID, CAMERA_COMPAT_CONTROL_DISMISSED);
         verify(mLayout).setCameraControlVisibility(/* show */ false);
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnLongClickForCameraTreatmentButton() {
         doNothing().when(mWindowManager).onCameraButtonLongClicked();
 
@@ -198,6 +216,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnLongClickForCameraDismissButton() {
         doNothing().when(mWindowManager).onCameraButtonLongClicked();
 
@@ -208,6 +227,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnClickForCameraCompatHint() {
         mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
         mWindowManager.createLayout(/* canShow= */ true);
@@ -229,4 +249,15 @@
         taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000));
         return taskInfo;
     }
+
+    private void verifyOnCameraControlStateUpdatedInvokedWith(int taskId, int state) {
+        final ArgumentCaptor<CompatUIEvent> captureValue = ArgumentCaptor.forClass(
+                CompatUIEvent.class);
+        verify(mCallback).accept(captureValue.capture());
+        final CompatUIEvents.CameraControlStateUpdated compatUIEvent =
+                (CompatUIEvents.CameraControlStateUpdated) captureValue.getValue();
+        Assert.assertEquals((compatUIEvent).getTaskId(), taskId);
+        Assert.assertEquals((compatUIEvent).getState(), state);
+        clearInvocations(mCallback);
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
index 41a81c1..eb3da8f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -25,6 +25,7 @@
 import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -42,6 +43,9 @@
 import android.app.TaskInfo;
 import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.AndroidTestingRunner;
 import android.util.Pair;
@@ -60,6 +64,8 @@
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
+import com.android.wm.shell.compatui.api.CompatUIEvent;
+import com.android.wm.shell.compatui.impl.CompatUIEvents;
 
 import junit.framework.Assert;
 
@@ -85,12 +91,16 @@
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final int TASK_ID = 1;
     private static final int TASK_WIDTH = 2000;
     private static final int TASK_HEIGHT = 2000;
 
     @Mock private SyncTransactionQueue mSyncTransactionQueue;
-    @Mock private CompatUIController.CompatUICallback mCallback;
+    @Mock private Consumer<CompatUIEvent> mCallback;
     @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked;
     @Mock private ShellTaskOrganizer.TaskListener mTaskListener;
     @Mock private CompatUILayout mLayout;
@@ -129,6 +139,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testCreateSizeCompatButton() {
         // Doesn't create layout if show is false.
         mWindowManager.mHasSizeCompat = true;
@@ -174,6 +185,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testCreateCameraCompatControl() {
         // Doesn't create layout if show is false.
         mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
@@ -212,6 +224,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testRelease() {
         mWindowManager.mHasSizeCompat = true;
         mWindowManager.createLayout(/* canShow= */ true);
@@ -224,6 +237,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateCompatInfo() {
         mWindowManager.mHasSizeCompat = true;
         mWindowManager.createLayout(/* canShow= */ true);
@@ -315,6 +329,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateCompatInfoLayoutNotInflatedYet() {
         mWindowManager.createLayout(/* canShow= */ false);
 
@@ -347,6 +362,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateDisplayLayout() {
         final DisplayInfo displayInfo = new DisplayInfo();
         displayInfo.logicalWidth = 1000;
@@ -366,6 +382,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateDisplayLayoutInsets() {
         final DisplayInfo displayInfo = new DisplayInfo();
         displayInfo.logicalWidth = 1000;
@@ -390,6 +407,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateVisibility() {
         // Create button if it is not created.
         mWindowManager.mLayout = null;
@@ -415,6 +433,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testAttachToParentSurface() {
         final SurfaceControl.Builder b = new SurfaceControl.Builder();
         mWindowManager.attachToParentSurface(b);
@@ -423,37 +442,38 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnCameraDismissButtonClicked() {
         mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
         mWindowManager.createLayout(/* canShow= */ true);
         clearInvocations(mLayout);
         mWindowManager.onCameraDismissButtonClicked();
 
-        verify(mCallback).onCameraControlStateUpdated(TASK_ID, CAMERA_COMPAT_CONTROL_DISMISSED);
+        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID, CAMERA_COMPAT_CONTROL_DISMISSED);
         verify(mLayout).setCameraControlVisibility(/* show= */ false);
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnCameraTreatmentButtonClicked() {
         mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
         mWindowManager.createLayout(/* canShow= */ true);
         clearInvocations(mLayout);
         mWindowManager.onCameraTreatmentButtonClicked();
 
-        verify(mCallback).onCameraControlStateUpdated(
-                TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
-        verify(mLayout).updateCameraTreatmentButton(
+        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID,
                 CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
+        verify(mLayout).updateCameraTreatmentButton(CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
 
         mWindowManager.onCameraTreatmentButtonClicked();
 
-        verify(mCallback).onCameraControlStateUpdated(
-                TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
-        verify(mLayout).updateCameraTreatmentButton(
+        verifyOnCameraControlStateUpdatedInvokedWith(TASK_ID,
                 CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
+        verify(mLayout).updateCameraTreatmentButton(CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED);
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnRestartButtonClicked() {
         mWindowManager.onRestartButtonClicked();
 
@@ -468,8 +488,9 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnRestartButtonLongClicked_showHint() {
-       // Not create hint popup.
+        // Not create hint popup.
         mWindowManager.mHasSizeCompat = true;
         mWindowManager.mCompatUIHintsState.mHasShownSizeCompatHint = true;
         mWindowManager.createLayout(/* canShow= */ true);
@@ -483,6 +504,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnCameraControlLongClicked_showHint() {
        // Not create hint popup.
         mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
@@ -498,6 +520,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testWhenDockedStateHasChanged_needsToBeRecreated() {
         ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
         newTaskInfo.configuration.uiMode |= Configuration.UI_MODE_TYPE_DESK;
@@ -506,6 +529,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testShouldShowSizeCompatRestartButton() {
         mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_HIDE_SCM_BUTTON);
         doReturn(85).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance();
@@ -558,4 +582,15 @@
         taskInfo.configuration.smallestScreenWidthDp = LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
         return taskInfo;
     }
+
+    private void verifyOnCameraControlStateUpdatedInvokedWith(int taskId, int state) {
+        final ArgumentCaptor<CompatUIEvent> captureValue = ArgumentCaptor.forClass(
+                CompatUIEvent.class);
+        verify(mCallback).accept(captureValue.capture());
+        final CompatUIEvents.CameraControlStateUpdated compatUIEvent =
+                (CompatUIEvents.CameraControlStateUpdated) captureValue.getValue();
+        Assert.assertEquals((compatUIEvent).getTaskId(), taskId);
+        Assert.assertEquals((compatUIEvent).getState(), state);
+        clearInvocations(mCallback);
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java
index 172c263..e8191db 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java
@@ -16,12 +16,17 @@
 
 package com.android.wm.shell.compatui;
 
+import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -32,6 +37,7 @@
 import com.android.wm.shell.ShellTestCase;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -54,6 +60,10 @@
     private View mDismissButton;
     private View mDialogContainer;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -66,6 +76,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnFinishInflate() {
         assertEquals(mLayout.getDialogContainerView(),
                 mLayout.findViewById(R.id.letterbox_education_dialog_container));
@@ -76,6 +87,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnDismissButtonClicked() {
         assertTrue(mDismissButton.performClick());
 
@@ -83,6 +95,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnBackgroundClicked() {
         assertTrue(mLayout.performClick());
 
@@ -90,6 +103,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnDialogContainerClicked() {
         assertTrue(mDialogContainer.performClick());
 
@@ -97,6 +111,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testSetDismissOnClickListenerNull() {
         mLayout.setDismissOnClickListener(null);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
index a60a1cb..b5664ac 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
@@ -19,6 +19,7 @@
 import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -37,6 +38,9 @@
 import android.app.TaskInfo;
 import android.graphics.Insets;
 import android.graphics.Rect;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.util.Pair;
 import android.view.DisplayCutout;
@@ -61,6 +65,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -116,6 +121,10 @@
     private CompatUIConfiguration mCompatUIConfiguration;
     private TestShellExecutor mExecutor;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -153,6 +162,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testCreateLayout_notEligible_doesNotCreateLayout() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ false);
 
@@ -162,6 +172,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testCreateLayout_eligibleAndDocked_doesNotCreateLayout() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */
                 true, /* isDocked */ true);
@@ -172,6 +183,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testCreateLayout_taskBarEducationIsShowing_doesNotCreateLayout() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true,
                 USER_ID_1, /* isTaskbarEduShowing= */ true);
@@ -182,6 +194,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testCreateLayout_canShowFalse_returnsTrueButDoesNotCreateLayout() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true);
 
@@ -192,6 +205,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testCreateLayout_canShowTrue_createsLayoutCorrectly() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true);
 
@@ -238,6 +252,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testCreateLayout_alreadyShownToUser_createsLayoutForOtherUserOnly() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true,
                 USER_ID_1, /* isTaskbarEduShowing= */ false);
@@ -271,6 +286,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testCreateLayout_windowManagerReleasedBeforeTransitionsIsIdle_doesNotStartAnim() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true);
 
@@ -288,6 +304,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateCompatInfo_updatesLayoutCorrectly() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true);
 
@@ -316,6 +333,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateCompatInfo_notEligibleUntilUpdate_createsLayoutAfterUpdate() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ false);
 
@@ -329,6 +347,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateCompatInfo_canShowFalse_doesNothing() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true);
 
@@ -343,6 +362,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateDisplayLayout_updatesLayoutCorrectly() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true);
 
@@ -364,6 +384,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testRelease_animationIsCancelled() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true);
 
@@ -374,6 +395,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testDeviceThemeChange_educationDialogUnseen_recreated() {
         LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true);
         ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java
index 4f71b83..0da14d6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java
@@ -16,6 +16,8 @@
 
 package com.android.wm.shell.compatui;
 
+import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertNotNull;
 
@@ -23,6 +25,9 @@
 import static org.mockito.Mockito.verify;
 
 import android.app.TaskInfo;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.LayoutInflater;
@@ -34,6 +39,7 @@
 import com.android.wm.shell.ShellTestCase;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -62,6 +68,10 @@
     @Mock
     private TaskInfo mTaskInfo;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -74,6 +84,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnFinishInflate() {
         assertNotNull(mMoveUpButton);
         assertNotNull(mMoveDownButton);
@@ -82,6 +93,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void handleVisibility_educationNotEnabled_buttonsAreHidden() {
         mLayout.handleVisibility(/* horizontalEnabled */ false, /* verticalEnabled */
                 false, /* letterboxVerticalPosition */
@@ -94,6 +106,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void handleVisibility_horizontalEducationEnableduiConfigurationIsUpdated() {
         mLayout.handleVisibility(/* horizontalEnabled */ true, /* verticalEnabled */
                 false, /* letterboxVerticalPosition */ -1, /* letterboxHorizontalPosition */
@@ -106,6 +119,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void handleVisibility_verticalEducationEnabled_uiConfigurationIsUpdated() {
         mLayout.handleVisibility(/* horizontalEnabled */ false, /* verticalEnabled */
                 true, /* letterboxVerticalPosition */ 0, /* letterboxHorizontalPosition */
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
index 5867a85..eafb414 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
@@ -16,12 +16,17 @@
 
 package com.android.wm.shell.compatui;
 
+import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 
 import android.app.ActivityManager;
 import android.app.TaskInfo;
 import android.content.res.Configuration;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 
 import androidx.test.filters.SmallTest;
@@ -35,6 +40,7 @@
 import junit.framework.Assert;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -65,6 +71,10 @@
     private TaskInfo mTaskInfo;
     private ReachabilityEduWindowManager mWindowManager;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -80,6 +90,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testCreateLayout_notEligible_doesNotCreateLayout() {
         assertFalse(mWindowManager.createLayout(/* canShow= */ true));
 
@@ -87,6 +98,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testWhenDockedStateHasChanged_needsToBeRecreated() {
         ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
         newTaskInfo.configuration.uiMode =
@@ -97,6 +109,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testWhenDarkLightThemeHasChanged_needsToBeRecreated() {
         ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
         mTaskInfo.configuration.uiMode =
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java
index e2dcdb0..6b0c5dd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java
@@ -16,6 +16,8 @@
 
 package com.android.wm.shell.compatui;
 
+import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -23,6 +25,9 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -34,6 +39,7 @@
 import com.android.wm.shell.ShellTestCase;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -60,6 +66,10 @@
     private View mDialogContainer;
     private CheckBox mDontRepeatCheckBox;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -76,6 +86,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnFinishInflate() {
         assertEquals(mLayout.getDialogContainerView(),
                 mLayout.findViewById(R.id.letterbox_restart_dialog_container));
@@ -86,6 +97,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnDismissButtonClicked() {
         assertTrue(mDismissButton.performClick());
 
@@ -93,6 +105,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnRestartButtonClickedWithoutCheckbox() {
         mDontRepeatCheckBox.setChecked(false);
         assertTrue(mRestartButton.performClick());
@@ -101,6 +114,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnRestartButtonClickedWithCheckbox() {
         mDontRepeatCheckBox.setChecked(true);
         assertTrue(mRestartButton.performClick());
@@ -109,6 +123,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnBackgroundClickedDoesntDismiss() {
         assertFalse(mLayout.performClick());
 
@@ -116,6 +131,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnDialogContainerClicked() {
         assertTrue(mDialogContainer.performClick());
 
@@ -124,6 +140,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testSetDismissOnClickListenerNull() {
         mLayout.setDismissOnClickListener(null);
 
@@ -135,6 +152,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testSetRestartOnClickListenerNull() {
         mLayout.setRestartOnClickListener(null);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java
index 9f109a1..cfeef90 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java
@@ -16,9 +16,14 @@
 
 package com.android.wm.shell.compatui;
 
+import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK;
+
 import android.app.ActivityManager;
 import android.app.TaskInfo;
 import android.content.res.Configuration;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.util.Pair;
 
@@ -33,6 +38,7 @@
 import junit.framework.Assert;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -60,6 +66,10 @@
     private RestartDialogWindowManager mWindowManager;
     private TaskInfo mTaskInfo;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -76,6 +86,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testWhenDockedStateHasChanged_needsToBeRecreated() {
         ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
         newTaskInfo.configuration.uiMode =
@@ -86,6 +97,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testWhenDarkLightThemeHasChanged_needsToBeRecreated() {
         ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
         mTaskInfo.configuration.uiMode =
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
index 0231612..3fa21ce 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
@@ -19,6 +19,7 @@
 import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK;
 
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -28,6 +29,9 @@
 import android.app.CameraCompatTaskInfo.CameraCompatControlState;
 import android.app.TaskInfo;
 import android.content.ComponentName;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.util.Pair;
 import android.view.LayoutInflater;
@@ -47,6 +51,7 @@
 import junit.framework.Assert;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -86,6 +91,10 @@
     private UserAspectRatioSettingsLayout mLayout;
     private TaskInfo mTaskInfo;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -107,6 +116,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnClickForUserAspectRatioSettingsButton() {
         final ImageButton button = mLayout.findViewById(R.id.user_aspect_ratio_settings_button);
         button.performClick();
@@ -123,6 +133,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnLongClickForUserAspectRatioButton() {
         doNothing().when(mWindowManager).onUserAspectRatioSettingsButtonLongClicked();
 
@@ -133,6 +144,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnClickForUserAspectRatioSettingsHint() {
         mWindowManager.mHasUserAspectRatioSettingsButton = true;
         mWindowManager.createLayout(/* canShow= */ true);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
index 94e168e..9f288cc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
@@ -22,6 +22,7 @@
 import static android.view.WindowInsets.Type.navigationBars;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -39,6 +40,9 @@
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 import android.util.Pair;
@@ -61,6 +65,7 @@
 import junit.framework.Assert;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -107,6 +112,10 @@
 
     private TestShellExecutor mExecutor;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -138,6 +147,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testCreateUserAspectRatioButton() {
         // Doesn't create layout if show is false.
         mWindowManager.mHasUserAspectRatioSettingsButton = true;
@@ -178,6 +188,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testRelease() {
         mWindowManager.mHasUserAspectRatioSettingsButton = true;
         mWindowManager.createLayout(/* canShow= */ true);
@@ -190,6 +201,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateCompatInfo() {
         mWindowManager.mHasUserAspectRatioSettingsButton = true;
         mWindowManager.createLayout(/* canShow= */ true);
@@ -242,6 +254,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateCompatInfoLayoutNotInflatedYet() {
         mWindowManager.mHasUserAspectRatioSettingsButton = true;
         mWindowManager.createLayout(/* canShow= */ false);
@@ -267,6 +280,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testEligibleButtonHiddenIfLetterboxBoundsEqualToStableBounds() {
         TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
                 true, /* topActivityBoundsLetterboxed */ true, ACTION_MAIN, CATEGORY_LAUNCHER);
@@ -292,6 +306,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUserFullscreenOverrideEnabled_buttonAlwaysShown() {
         TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
                 true, /* topActivityBoundsLetterboxed */ true, ACTION_MAIN, CATEGORY_LAUNCHER);
@@ -310,6 +325,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateDisplayLayout() {
         final DisplayInfo displayInfo = new DisplayInfo();
         displayInfo.logicalWidth = 1000;
@@ -329,6 +345,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateDisplayLayoutInsets() {
         final DisplayInfo displayInfo = new DisplayInfo();
         displayInfo.logicalWidth = 1000;
@@ -353,6 +370,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testUpdateVisibility() {
         // Create button if it is not created.
         mWindowManager.removeLayout();
@@ -378,6 +396,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testLayoutHasUserAspectRatioSettingsButton() {
         clearInvocations(mWindowManager);
         spyOn(mWindowManager);
@@ -411,6 +430,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testAttachToParentSurface() {
         final SurfaceControl.Builder b = new SurfaceControl.Builder();
         mWindowManager.attachToParentSurface(b);
@@ -419,6 +439,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnUserAspectRatioButtonClicked() {
         mWindowManager.onUserAspectRatioSettingsButtonClicked();
 
@@ -433,6 +454,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testOnUserAspectRatioButtonLongClicked_showHint() {
        // Not create hint popup.
         mWindowManager.mHasUserAspectRatioSettingsButton = true;
@@ -448,6 +470,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testWhenDockedStateHasChanged_needsToBeRecreated() {
         ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
         newTaskInfo.configuration.uiMode |= Configuration.UI_MODE_TYPE_DESK;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
index 665bed0..d459639 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -52,7 +52,7 @@
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
 import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN
-import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.TransitionInfoBuilder
 import com.android.wm.shell.transition.Transitions
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index bd38d36..6cabbf9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -20,6 +20,7 @@
 import android.app.ActivityManager.RunningTaskInfo
 import android.app.KeyguardManager
 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
@@ -89,7 +90,7 @@
 import com.android.wm.shell.recents.RecentTasksController
 import com.android.wm.shell.recents.RecentsTransitionHandler
 import com.android.wm.shell.recents.RecentsTransitionStateListener
-import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellController
@@ -137,6 +138,7 @@
  */
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
+@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
 class DesktopTasksControllerTest : ShellTestCase() {
 
   @JvmField @Rule val setFlagsRule = SetFlagsRule()
@@ -193,7 +195,6 @@
             .strictness(Strictness.LENIENT)
             .spyStatic(DesktopModeStatus::class.java)
             .startMocking()
-    whenever(DesktopModeStatus.isDesktopModeFlagEnabled()).thenReturn(true)
     doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
 
     shellInit = spy(ShellInit(testExecutor))
@@ -263,7 +264,7 @@
   }
 
   @Test
-  fun instantiate_flagOff_doNotAddInitCallback() {
+  fun instantiate_canNotEnterDesktopMode_doNotAddInitCallback() {
     whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false)
     clearInvocations(shellInit)
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 4bfa96a..8d9fd91 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -33,7 +33,7 @@
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
-import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.transition.TransitionInfoBuilder
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.util.StubTransaction
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
index b2467e9..e5157c9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
@@ -45,6 +45,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
@@ -65,6 +66,8 @@
     @Mock
     private Transitions mTransitions;
     @Mock
+    private InteractionJankMonitor mInteractionJankMonitor;
+    @Mock
     IBinder mToken;
     @Mock
     Supplier<SurfaceControl.Transaction> mTransactionFactory;
@@ -94,7 +97,7 @@
                 .thenReturn(getContext().getResources().getDisplayMetrics());
 
         mExitDesktopTaskTransitionHandler = new ExitDesktopTaskTransitionHandler(mTransitions,
-                mContext);
+                mContext, mInteractionJankMonitor);
         mPoint = new Point(0, 0);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
index 582fb91..97fa8d6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
@@ -289,9 +289,9 @@
         ArrayList<Target> targets = assertExactTargetTypes(
                 mPolicy.getTargets(mInsets), TYPE_FULLSCREEN);
 
-        mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN));
+        mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), null /* hideTaskToken */);
         verify(mFullscreenStarter).startIntent(any(), anyInt(), any(),
-                eq(SPLIT_POSITION_UNDEFINED), any());
+                eq(SPLIT_POSITION_UNDEFINED), any(), any());
     }
 
     private void dragOverFullscreenApp_expectSplitScreenTargets(ClipData data) {
@@ -304,14 +304,14 @@
         ArrayList<Target> targets = assertExactTargetTypes(
                 mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
 
-        mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_LEFT));
+        mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_LEFT), null /* hideTaskToken */);
         verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
-                eq(SPLIT_POSITION_TOP_OR_LEFT), any());
+                eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any());
         reset(mSplitScreenStarter);
 
-        mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT));
+        mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), null /* hideTaskToken */);
         verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
-                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any());
+                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any());
     }
 
     private void dragOverFullscreenAppPhone_expectVerticalSplitScreenTargets(ClipData data) {
@@ -324,14 +324,15 @@
         ArrayList<Target> targets = assertExactTargetTypes(
                 mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
 
-        mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_TOP));
+        mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_TOP), null /* hideTaskToken */);
         verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
-                eq(SPLIT_POSITION_TOP_OR_LEFT), any());
+                eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any());
         reset(mSplitScreenStarter);
 
-        mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM));
+        mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM),
+                null /* hideTaskToken */);
         verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
-                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any());
+                eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any());
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index 3f3cafc..6ec6bed 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -36,7 +36,7 @@
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
-import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 5c5a1a2..a0aab2e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -68,7 +68,7 @@
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
-import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt
index 3ec65c7..00f37da 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt
@@ -22,13 +22,10 @@
 import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
-import com.android.dx.mockito.inline.extended.ExtendedMockito
-import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
 import com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION
 import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.shared.DesktopModeStatus
 import com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE
 import com.android.wm.shell.shared.desktopmode.DesktopModeFlags.ToggleOverride.OVERRIDE_OFF
 import com.android.wm.shell.shared.desktopmode.DesktopModeFlags.ToggleOverride.OVERRIDE_ON
@@ -39,8 +36,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.kotlin.whenever
-import org.mockito.quality.Strictness
 
 /**
  * Test class for [DesktopModeFlags]
@@ -188,27 +183,6 @@
   }
 
   @Test
-  @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
-  fun isEnabled_noOverride_featureFlagOnThenOff_returnsTrueAndFalse() {
-    setOverride(null)
-    // For overridableFlag, in absence of overrides, follow flag
-    assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue()
-
-    val mockitoSession: StaticMockitoSession =
-        ExtendedMockito.mockitoSession()
-            .strictness(Strictness.LENIENT)
-            .spyStatic(DesktopModeStatus::class.java)
-            .startMocking()
-    try {
-      // No caching of flags
-      whenever(DesktopModeStatus.isDesktopModeFlagEnabled()).thenReturn(false)
-      assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse()
-    } finally {
-      mockitoSession.finishMocking()
-    }
-  }
-
-  @Test
   @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
   @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
   fun isEnabled_noSystemProperty_overrideOn_featureFlagOff_returnsTrueAndStoresPropertyOn() {
@@ -453,15 +427,10 @@
   }
 
   private fun resetCache() {
-    val cachedToggleOverrideDesktopMode =
-        DESKTOP_WINDOWING_MODE::class.java.getDeclaredField("cachedToggleOverride")
-    cachedToggleOverrideDesktopMode.isAccessible = true
-    cachedToggleOverrideDesktopMode.set(DESKTOP_WINDOWING_MODE, null)
-
-    val cachedToggleOverrideWallpaperActivity =
-      WALLPAPER_ACTIVITY::class.java.getDeclaredField("cachedToggleOverride")
-    cachedToggleOverrideWallpaperActivity.isAccessible = true
-    cachedToggleOverrideWallpaperActivity.set(WALLPAPER_ACTIVITY, null)
+    val cachedToggleOverride =
+      DesktopModeFlags::class.java.getDeclaredField("cachedToggleOverride")
+    cachedToggleOverride.isAccessible = true
+    cachedToggleOverride.set(null, null)
 
     // Clear override cache stored in System property
     System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 3c387f0..5b95b15 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -36,6 +36,7 @@
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -49,6 +50,9 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.os.Bundle;
+import android.os.IBinder;
+import android.window.IWindowContainerToken;
+import android.window.WindowContainerToken;
 
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -195,10 +199,10 @@
                 PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
-                SPLIT_POSITION_TOP_OR_LEFT, null);
+                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */);
 
         verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
-                eq(SPLIT_POSITION_TOP_OR_LEFT), isNull());
+                eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull());
         assertEquals(FLAG_ACTIVITY_NO_USER_ACTION,
                 mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_NO_USER_ACTION);
     }
@@ -213,19 +217,20 @@
         ActivityManager.RunningTaskInfo topRunningTask =
                 createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
         doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask();
+        doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask(any());
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
-                SPLIT_POSITION_TOP_OR_LEFT, null);
+                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */);
 
         verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
-                eq(SPLIT_POSITION_TOP_OR_LEFT), isNull());
+                eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull());
         assertEquals(FLAG_ACTIVITY_MULTIPLE_TASK,
                 mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK);
     }
 
     @Test
     public void startIntent_multiInstancesNotSupported_startTaskInBackgroundBeforeSplitActivated() {
-        doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any());
+        doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any(), any());
         Intent startIntent = createStartIntent("startActivity");
         PendingIntent pendingIntent =
                 PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
@@ -233,15 +238,16 @@
         ActivityManager.RunningTaskInfo topRunningTask =
                 createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
         doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask();
+        doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask(any());
         // Put the same component into a task in the background
         ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo();
-        doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any(), anyInt());
+        doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any(), anyInt(), any());
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
-                SPLIT_POSITION_TOP_OR_LEFT, null);
+                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */);
 
         verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
-                isNull());
+                isNull(), isNull());
         verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any());
         verify(mStageCoordinator, never()).switchSplitPosition(any());
     }
@@ -249,7 +255,7 @@
     @Test
     public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() {
         doReturn(true).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any());
-        doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any());
+        doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any(), any());
         Intent startIntent = createStartIntent("startActivity");
         PendingIntent pendingIntent =
                 PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
@@ -261,13 +267,13 @@
                 SPLIT_POSITION_BOTTOM_OR_RIGHT);
         // Put the same component into a task in the background
         doReturn(new ActivityManager.RecentTaskInfo()).when(mRecentTasks)
-                .findTaskInBackground(any(), anyInt());
+                .findTaskInBackground(any(), anyInt(), any());
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
-                SPLIT_POSITION_TOP_OR_LEFT, null);
+                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */);
         verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any());
         verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
-                isNull());
+                isNull(), isNull());
     }
 
     @Test
@@ -284,7 +290,7 @@
                 SPLIT_POSITION_BOTTOM_OR_RIGHT);
 
         mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
-                SPLIT_POSITION_TOP_OR_LEFT, null);
+                SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */);
 
         verify(mStageCoordinator).switchSplitPosition(anyString());
     }
@@ -312,6 +318,7 @@
         info.supportsMultiWindow = true;
         info.baseIntent = strIntent;
         info.baseActivity = strIntent.getComponent();
+        info.token = new WindowContainerToken(mock(IWindowContainerToken.class));
         ActivityInfo activityInfo = new ActivityInfo();
         activityInfo.packageName = info.baseActivity.getPackageName();
         activityInfo.name = info.baseActivity.getClassName();
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 0bf5a67..b1803e9 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
@@ -70,7 +70,7 @@
 import com.android.wm.shell.desktopmode.DesktopTasksController
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
-import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.sysui.KeyguardChangeListener
 import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellController
@@ -360,7 +360,7 @@
                 isTopActivityStyleFloating = true
                 numActivities = 1
             }
-            doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+            doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
             setUpMockDecorationsForTasks(task)
 
             onTaskOpening(task)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 8165e59..b355137 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -82,7 +82,7 @@
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams;
 import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener;
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
index ac5aeec..e529711 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
@@ -34,7 +34,7 @@
 import com.android.wm.shell.R
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.DisplayLayout
-import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM
 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index f3603e1..31c6479 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -75,7 +75,7 @@
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.tests.R;
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer;
 
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java
index 102d21a..43acbb1 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java
@@ -2246,6 +2246,8 @@
         clientAttribution.uid = -1; // USE_CALLING_UID
         clientAttribution.pid = -1; // USE_CALLING_PID
         clientAttribution.deviceId = contextAttribution.deviceId;
+        clientAttribution.packageName = context.getOpPackageName();
+        clientAttribution.attributionTag = context.getAttributionTag();
         clientAttribution.next = new AttributionSourceState[0];
         return clientAttribution;
     }
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
index ad3374a..ac85ab7 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
@@ -169,10 +169,8 @@
 
             ICameraClient dummyCallbacks = new DummyCameraClient();
 
-            String clientPackageName = getContext().getPackageName();
-
             ICamera cameraUser = mUtils.getCameraService()
-                    .connect(dummyCallbacks, cameraId, clientPackageName,
+                    .connect(dummyCallbacks, cameraId,
                             getContext().getApplicationInfo().targetSdkVersion,
                             ICameraService.ROTATION_OVERRIDE_NONE,
                             /*forceSlowJpegMode*/false,
@@ -267,8 +265,6 @@
 
             ICameraDeviceCallbacks dummyCallbacks = new DummyCameraDeviceCallbacks();
 
-            String clientPackageName = getContext().getPackageName();
-            String clientAttributionTag = getContext().getAttributionTag();
             AttributionSourceState clientAttribution =
                     CameraTestUtils.getClientAttribution(mContext);
             clientAttribution.deviceId = DEVICE_ID_DEFAULT;
@@ -277,7 +273,6 @@
             ICameraDeviceUser cameraUser =
                     mUtils.getCameraService().connectDevice(
                         dummyCallbacks, String.valueOf(cameraId),
-                        clientPackageName, clientAttributionTag,
                         0 /*oomScoreOffset*/,
                         getContext().getApplicationInfo().targetSdkVersion,
                         ICameraService.ROTATION_OVERRIDE_NONE, clientAttribution,
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java
index 0ab1ee9..35ad924 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java
@@ -242,9 +242,6 @@
 
         ICameraDeviceCallbacks.Stub dummyCallbacks = new DummyCameraDeviceCallbacks();
 
-        String clientPackageName = getContext().getPackageName();
-        String clientAttributionTag = getContext().getAttributionTag();
-
         mMockCb = spy(dummyCallbacks);
 
         AttributionSourceState clientAttribution = CameraTestUtils.getClientAttribution(mContext);
@@ -252,7 +249,6 @@
         clientAttribution.uid = ICameraService.USE_CALLING_UID;
 
         mCameraUser = mUtils.getCameraService().connectDevice(mMockCb, mCameraId,
-                clientPackageName, clientAttributionTag,
                 /*oomScoreOffset*/0, getContext().getApplicationInfo().targetSdkVersion,
                 ICameraService.ROTATION_OVERRIDE_NONE, clientAttribution, DEVICE_POLICY_DEFAULT);
         assertNotNull(String.format("Camera %s was null", mCameraId), mCameraUser);
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index d6cbf2a..0cb85d8 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -64,6 +64,7 @@
     srcs: [
         "src/**/*.java",
         "src/**/*.kt",
+        "src/**/I*.aidl",
     ],
 }
 
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
index c9934ad..fb23637 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
@@ -115,7 +115,7 @@
             content: @Composable (SwitchPreferenceModel) -> Unit,
         ) {
             val context = LocalContext.current
-            val restrictedSwitchPreferenceModel = remember(restrictedMode, model.title) {
+            val restrictedSwitchPreferenceModel = remember(restrictedMode, model) {
                 RestrictedSwitchPreferenceModel(context, model, restrictedMode)
             }
             restrictedSwitchPreferenceModel.RestrictionWrapper {
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index 38c871c..403e219 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -102,3 +102,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "extreme_power_low_state_vulnerability"
+    namespace: "pixel_energizer"
+    description: "the battery saver can pause all non-essential apps and their corresponding notification when device is in locked state to introduce the security vulnerability"
+    bug: "346513692"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreference.java
new file mode 100644
index 0000000..1cbb8b4
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreference.java
@@ -0,0 +1,326 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * A data class representing an action/switch preference. The preference could be one of the four
+ * following forms: 1. Texted row with action to jump to another page 2. Texted row without action
+ * 3. Texted row with action and switch 4. Texted row with switch
+ */
+public class ActionSwitchPreference extends DeviceSettingPreference implements Parcelable {
+    private final String mTitle;
+    private final String mSummary;
+    private final Bitmap mIcon;
+    private final Intent mIntent;
+    private final boolean mHasSwitch;
+    private final boolean mChecked;
+    private final boolean mIsAllowedChangingState;
+    private final Bundle mExtras;
+
+    ActionSwitchPreference(
+            String title,
+            @Nullable String summary,
+            @Nullable Bitmap icon,
+            @Nullable Intent intent,
+            boolean hasSwitch,
+            boolean checked,
+            boolean allowChangingState,
+            @NonNull Bundle extras) {
+        super(DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH);
+        validate(title);
+        mTitle = title;
+        mSummary = summary;
+        mIcon = icon;
+        mIntent = intent;
+        mHasSwitch = hasSwitch;
+        mChecked = checked;
+        mIsAllowedChangingState = allowChangingState;
+        mExtras = extras;
+    }
+
+    private static void validate(String title) {
+        if (Objects.isNull(title)) {
+            throw new IllegalArgumentException("Title must be set");
+        }
+    }
+
+    /**
+     * Reads an {@link ActionSwitchPreference} instance from {@link Parcel}
+     * @param in The parcel to read from
+     * @return The instance read
+     */
+    @NonNull
+    public static ActionSwitchPreference readFromParcel(@NonNull Parcel in) {
+        String title = in.readString();
+        String summary = in.readString();
+        Bitmap icon = in.readParcelable(Bitmap.class.getClassLoader());
+        Intent intent = in.readParcelable(Intent.class.getClassLoader());
+        boolean hasSwitch = in.readBoolean();
+        boolean checked = in.readBoolean();
+        boolean allowChangingState = in.readBoolean();
+        Bundle extras = in.readBundle(Bundle.class.getClassLoader());
+        return new ActionSwitchPreference(
+                title, summary, icon, intent, hasSwitch, checked, allowChangingState, extras);
+    }
+
+    public static final Creator<ActionSwitchPreference> CREATOR =
+            new Creator<>() {
+                @Override
+                @NonNull
+                public ActionSwitchPreference createFromParcel(@NonNull Parcel in) {
+                    in.readInt();
+                    return readFromParcel(in);
+                }
+
+                @Override
+                @NonNull
+                public ActionSwitchPreference[] newArray(int size) {
+                    return new ActionSwitchPreference[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeString(mTitle);
+        dest.writeString(mSummary);
+        dest.writeParcelable(mIcon, flags);
+        dest.writeParcelable(mIntent, flags);
+        dest.writeBoolean(mHasSwitch);
+        dest.writeBoolean(mChecked);
+        dest.writeBoolean(mIsAllowedChangingState);
+        dest.writeBundle(mExtras);
+    }
+
+    /** Builder class for {@link ActionSwitchPreference}. */
+    public static final class Builder {
+        private String mTitle;
+        private String mSummary;
+        private Bitmap mIcon;
+        private Intent mIntent;
+        private boolean mHasSwitch;
+        private boolean mChecked;
+        private boolean mIsAllowedChangingState;
+        private Bundle mExtras = Bundle.EMPTY;
+
+        /**
+         * Sets the title of the preference.
+         *
+         * @param title The title of the preference.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setTitle(@NonNull String title) {
+            mTitle = title;
+            return this;
+        }
+
+        /**
+         * Sets the summary of the preference, optional.
+         *
+         * @param summary The preference summary.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setSummary(@Nullable String summary) {
+            mSummary = summary;
+            return this;
+        }
+
+        /**
+         * Sets the icon to be displayed on the left of the preference, optional.
+         *
+         * @param icon The icon.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setIcon(@Nullable Bitmap icon) {
+            mIcon = icon;
+            return this;
+        }
+
+        /**
+         * Sets the Intent to launch when the preference is clicked, optional.
+         *
+         * @param intent The Intent.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setIntent(@Nullable Intent intent) {
+            mIntent = intent;
+            return this;
+        }
+
+        /**
+         * Sets whether the preference will contain a switch.
+         *
+         * @param hasSwitch Whether the preference contains a switch.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setHasSwitch(boolean hasSwitch) {
+            mHasSwitch = hasSwitch;
+            return this;
+        }
+
+        /**
+         * Sets the state of the preference.
+         *
+         * @param checked Whether the switch is checked.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setChecked(boolean checked) {
+            mChecked = checked;
+            return this;
+        }
+
+        /**
+         * Sets whether state can be changed by user.
+         *
+         * @param allowChangingState Whether user is allowed to change state.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setAllowedChangingState(boolean allowChangingState) {
+            mIsAllowedChangingState = allowChangingState;
+            return this;
+        }
+
+        /**
+         * Sets the extras bundle.
+         *
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setExtras(@NonNull Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /**
+         * Builds the {@link ActionSwitchPreference} object.
+         *
+         * @return Returns the built {@link ActionSwitchPreference} object.
+         */
+        @NonNull
+        public ActionSwitchPreference build() {
+            return new ActionSwitchPreference(
+                    mTitle,
+                    mSummary,
+                    mIcon,
+                    mIntent,
+                    mHasSwitch,
+                    mChecked,
+                    mIsAllowedChangingState,
+                    mExtras);
+        }
+    }
+
+    /**
+     * Gets the title of the preference.
+     *
+     * @return Returns the title of the preference.
+     */
+    @NonNull
+    public String getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * Gets the summary of the preference.
+     *
+     * @return Returns the summary of the preference.
+     */
+    @Nullable
+    public String getSummary() {
+        return mSummary;
+    }
+
+    /**
+     * Gets the icon of the preference.
+     *
+     * @return Returns the icon of the preference.
+     */
+    @Nullable
+    public Bitmap getIcon() {
+        return mIcon;
+    }
+
+    /**
+     * Gets the Intent to launch when the preference is clicked.
+     *
+     * @return Returns the intent to launch.
+     */
+    @Nullable
+    public Intent getIntent() {
+        return mIntent;
+    }
+
+    /**
+     * Whether the preference contains a switch.
+     *
+     * @return Whether the preference contains a switch.
+     */
+    public boolean hasSwitch() {
+        return mHasSwitch;
+    }
+
+    /**
+     * Whether the switch is checked.
+     *
+     * @return Whether the switch is checked.
+     */
+    public boolean getChecked() {
+        return mChecked;
+    }
+
+    /**
+     * Gets whether the state can be changed by user.
+     *
+     * @return Whether the state can be changed by user.
+     */
+    public boolean isAllowedChangingState() {
+        return mIsAllowedChangingState;
+    }
+
+    /**
+     * Gets the extras bundle.
+     *
+     * @return The extra bundle.
+     */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceState.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceState.java
new file mode 100644
index 0000000..91c1a59
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceState.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.settingslib.bluetooth.devicesettings;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+/** A data class representing the state of an action/switch preference. */
+public class ActionSwitchPreferenceState extends DeviceSettingPreferenceState
+        implements Parcelable {
+    private final boolean mChecked;
+    private final Bundle mExtras;
+
+    ActionSwitchPreferenceState(boolean checked, @NonNull Bundle extras) {
+        super(DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH);
+        mChecked = checked;
+        mExtras = extras;
+    }
+
+    /**
+     * Reads an {@link ActionSwitchPreferenceState} instance from {@link Parcel}
+     * @param in The parcel to read from
+     * @return The instance read
+     */
+    @NonNull
+    public static ActionSwitchPreferenceState readFromParcel(@NonNull Parcel in) {
+        boolean checked = in.readBoolean();
+        Bundle extras = in.readBundle(Bundle.class.getClassLoader());
+        return new ActionSwitchPreferenceState(checked, extras);
+    }
+
+    public static final Creator<ActionSwitchPreferenceState> CREATOR =
+            new Creator<>() {
+                @Override
+                @NonNull
+                public ActionSwitchPreferenceState createFromParcel(@NonNull Parcel in) {
+                    in.readInt();
+                    return readFromParcel(in);
+                }
+
+                @Override
+                @NonNull
+                public ActionSwitchPreferenceState[] newArray(int size) {
+                    return new ActionSwitchPreferenceState[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeBoolean(mChecked);
+        dest.writeBundle(mExtras);
+    }
+
+    /** Builder class for {@link ActionSwitchPreferenceState}. */
+    public static final class Builder {
+        private boolean mChecked;
+        private Bundle mExtras = Bundle.EMPTY;
+
+        public Builder() {}
+
+        /**
+         * Sets the state of the preference.
+         *
+         * @param checked Whether the switch is checked.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setChecked(boolean checked) {
+            mChecked = checked;
+            return this;
+        }
+
+        /**
+         * Sets the extras bundle.
+         *
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setExtras(@NonNull Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /**
+         * Builds the object.
+         *
+         * @return Returns the built object.
+         */
+        @NonNull
+        public ActionSwitchPreferenceState build() {
+            return new ActionSwitchPreferenceState(mChecked, mExtras);
+        }
+    }
+
+    /**
+     * Whether the switch is checked.
+     *
+     * @return Whether the switch is checked.
+     */
+    public boolean getChecked() {
+        return mChecked;
+    }
+
+    /**
+     * Gets the extras bundle.
+     *
+     * @return The extra bundle.
+     */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfo.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfo.aidl
new file mode 100644
index 0000000..acbaf2d
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+parcelable DeviceInfo;
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfo.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfo.java
new file mode 100644
index 0000000..52e520e
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfo.java
@@ -0,0 +1,137 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import java.util.Objects;
+
+/** A data class representing a bluetooth device. */
+public class DeviceInfo implements Parcelable {
+    private final String mBluetoothAddress;
+    private final Bundle mExtras;
+
+    DeviceInfo(String bluetoothAddress, Bundle extras) {
+        validate(bluetoothAddress);
+        mBluetoothAddress = bluetoothAddress;
+        mExtras = extras;
+    }
+
+    private static void validate(String bluetoothAddress) {
+        if (Objects.isNull(bluetoothAddress)) {
+            throw new IllegalArgumentException("Bluetooth address must be set");
+        }
+    }
+
+    /** Read a {@link DeviceInfo} instance from {@link Parcel} */
+    @NonNull
+    public static DeviceInfo readFromParcel(@NonNull Parcel in) {
+        String bluetoothAddress = in.readString();
+        Bundle extras = in.readBundle(Bundle.class.getClassLoader());
+        return new DeviceInfo(bluetoothAddress, extras);
+    }
+
+    public static final Creator<DeviceInfo> CREATOR =
+            new Creator<>() {
+                @Override
+                @NonNull
+                public DeviceInfo createFromParcel(@NonNull Parcel in) {
+                    return readFromParcel(in);
+                }
+
+                @Override
+                @NonNull
+                public DeviceInfo[] newArray(int size) {
+                    return new DeviceInfo[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString(mBluetoothAddress);
+        dest.writeBundle(mExtras);
+    }
+
+    /** Builder class for {@link DeviceInfo}. */
+    public static final class Builder {
+        private String mBluetoothAddress;
+        private Bundle mExtras = Bundle.EMPTY;
+
+        /**
+         * Sets the bluetooth address of the device, from {@link
+         * android.bluetooth.BluetoothDevice#getAddress()}.
+         *
+         * @param bluetoothAddress The bluetooth address.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setBluetoothAddress(@NonNull String bluetoothAddress) {
+            mBluetoothAddress = bluetoothAddress;
+            return this;
+        }
+
+        /**
+         * Sets the extras bundle.
+         *
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setExtras(@NonNull Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /**
+         * Builds the {@link DeviceInfo} object.
+         *
+         * @return Returns the built {@link DeviceInfo} object.
+         */
+        @NonNull
+        public DeviceInfo build() {
+            return new DeviceInfo(mBluetoothAddress, mExtras);
+        }
+    }
+
+    /**
+     * Gets the bluetooth address of the device.
+     *
+     * @return The bluetooth address from {@link android.bluetooth.BluetoothDevice#getAddress()}.
+     */
+    @NonNull
+    public String getBluetoothAddress() {
+        return mBluetoothAddress;
+    }
+
+    /**
+     * Gets the extras bundle.
+     *
+     * @return The extra bundle.
+     */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSetting.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSetting.aidl
new file mode 100644
index 0000000..043cae3
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSetting.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+parcelable DeviceSetting;
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSetting.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSetting.java
new file mode 100644
index 0000000..dc219a9
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSetting.java
@@ -0,0 +1,160 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import java.util.Objects;
+
+/** A data class representing a device setting item in bluetooth device details page. */
+public final class DeviceSetting implements Parcelable {
+    @DeviceSettingId private final int mSettingId;
+    private final DeviceSettingPreference mPreference;
+    private final Bundle mExtras;
+
+    DeviceSetting(
+            int settingId, @NonNull DeviceSettingPreference preference, @NonNull Bundle extras) {
+        validate(preference);
+        mSettingId = settingId;
+        mPreference = preference;
+        mExtras = extras;
+    }
+
+    private static void validate(DeviceSettingPreference preference) {
+        if (Objects.isNull(preference)) {
+            throw new IllegalArgumentException("Preference must be set");
+        }
+    }
+
+    /** Read a {@link DeviceSetting} instance from {@link Parcel} */
+    @NonNull
+    public static DeviceSetting readFromParcel(@NonNull Parcel in) {
+        int settingId = in.readInt();
+        Bundle extras = in.readBundle(Bundle.class.getClassLoader());
+        DeviceSettingPreference settingPreference = DeviceSettingPreference.readFromParcel(in);
+        return new DeviceSetting(settingId, settingPreference, extras);
+    }
+
+    public static final Creator<DeviceSetting> CREATOR =
+            new Creator<>() {
+                @Override
+                @NonNull
+                public DeviceSetting createFromParcel(@NonNull Parcel in) {
+                    return readFromParcel(in);
+                }
+
+                @Override
+                @NonNull
+                public DeviceSetting[] newArray(int size) {
+                    return new DeviceSetting[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mSettingId);
+        dest.writeBundle(mExtras);
+        mPreference.writeToParcel(dest, flags);
+    }
+
+    /** Builder class for {@link DeviceSetting}. */
+    public static final class Builder {
+        private int mSettingId;
+        private DeviceSettingPreference mPreference;
+        private Bundle mExtras = Bundle.EMPTY;
+
+        public Builder() {}
+
+        /**
+         * Sets the setting ID, as defined by IntDef {@link DeviceSettingId}.
+         *
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setSettingId(@DeviceSettingId int settingId) {
+            mSettingId = settingId;
+            return this;
+        }
+
+        /**
+         * Sets the setting preference.
+         *
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setPreference(@NonNull DeviceSettingPreference settingPreference) {
+            mPreference = settingPreference;
+            return this;
+        }
+
+        /**
+         * Sets the extras bundle.
+         *
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setExtras(@NonNull Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /** Build the object. */
+        @NonNull
+        public DeviceSetting build() {
+            return new DeviceSetting(mSettingId, mPreference, mExtras);
+        }
+    }
+
+    /**
+     * Gets the setting ID as defined by IntDef {@link DeviceSettingId}.
+     *
+     * @return Returns the setting ID.
+     */
+    @DeviceSettingId
+    public int getSettingId() {
+        return mSettingId;
+    }
+
+    /**
+     * Gets the setting preference.
+     *
+     * @return Returns the setting preference.
+     */
+    @NonNull
+    public DeviceSettingPreference getPreference() {
+        return mPreference;
+    }
+
+    /**
+     * Gets the extras Bundle.
+     *
+     * @return Returns a Bundle object.
+     */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java
new file mode 100644
index 0000000..20a0339
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java
@@ -0,0 +1,113 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.SOURCE)
+@IntDef(
+        value = {
+            DeviceSettingId.DEVICE_SETTING_ID_UNKNOWN,
+            DeviceSettingId.DEVICE_SETTING_ID_HEADER,
+            DeviceSettingId.DEVICE_SETTING_ID_ADVANCED_HEADER,
+            DeviceSettingId.DEVICE_SETTING_ID_LE_AUDIO_HEADER,
+            DeviceSettingId.DEVICE_SETTING_ID_HEARING_AID_PAIR_OTHER_BUTTON,
+            DeviceSettingId.DEVICE_SETTING_ID_HEARING_AID_SPACE_LAYOUT,
+            DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS,
+            DeviceSettingId.DEVICE_SETTING_ID_DEVICE_STYLUS,
+            DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_EXTRA_CONTROL,
+            DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_DEVICE_SLICE_CATEGORY,
+            DeviceSettingId.DEVICE_SETTING_ID_DEVICE_COMPANION_APPS,
+            DeviceSettingId.DEVICE_SETTING_ID_HEARING_DEVICE_GROUP,
+            DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_AUDIO_DEVICE_TYPE_GROUP,
+            DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_GROUP,
+            DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
+            DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_EXTRA_OPTIONS,
+            DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_RELATED_TOOLS,
+            DeviceSettingId.DEVICE_SETTING_ID_DATA_SYNC_GROUP,
+            DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
+            DeviceSettingId.DEVICE_SETTING_ID_DEVICE_DETAILS_FOOTER,
+            DeviceSettingId.DEVICE_SETTING_ID_ANC,
+        },
+        open = true)
+public @interface DeviceSettingId {
+    /** Device setting ID is unknown. */
+    int DEVICE_SETTING_ID_UNKNOWN = 0;
+
+    /** Device setting ID for header. */
+    int DEVICE_SETTING_ID_HEADER = 1;
+
+    /** Device setting ID for advanced header. */
+    int DEVICE_SETTING_ID_ADVANCED_HEADER = 2;
+
+    /** Device setting ID for LeAudio header. */
+    int DEVICE_SETTING_ID_LE_AUDIO_HEADER = 3;
+
+    /** Device setting ID for hearing aid “pair other” button. */
+    int DEVICE_SETTING_ID_HEARING_AID_PAIR_OTHER_BUTTON = 4;
+
+    /** Device setting ID for hearing aid space layout. */
+    int DEVICE_SETTING_ID_HEARING_AID_SPACE_LAYOUT = 5;
+
+    /** Device setting ID for action buttons(Forget, Connect/Disconnect). */
+    int DEVICE_SETTING_ID_ACTION_BUTTONS = 6;
+
+    /** Device setting ID for stylus device. */
+    int DEVICE_SETTING_ID_DEVICE_STYLUS = 7;
+
+    /** Device setting ID for bluetooth extra control. */
+    int DEVICE_SETTING_ID_BLUETOOTH_EXTRA_CONTROL = 8;
+
+    /** Device setting ID for bluetooth device slice category. */
+    int DEVICE_SETTING_ID_BLUETOOTH_DEVICE_SLICE_CATEGORY = 9;
+
+    /** Device setting ID for device companion apps. */
+    int DEVICE_SETTING_ID_DEVICE_COMPANION_APPS = 10;
+
+    /** Device setting ID for hearing device group. */
+    int DEVICE_SETTING_ID_HEARING_DEVICE_GROUP = 11;
+
+    /** Device setting ID for bluetooth audio device type group. */
+    int DEVICE_SETTING_ID_BLUETOOTH_AUDIO_DEVICE_TYPE_GROUP = 12;
+
+    /** Device setting ID for spatial audio group. */
+    int DEVICE_SETTING_ID_SPATIAL_AUDIO_GROUP = 13;
+
+    /** Device setting ID for bluetooth profiles. */
+    int DEVICE_SETTING_ID_BLUETOOTH_PROFILES = 14;
+
+    /** Device setting ID for bluetooth extra options. */
+    int DEVICE_SETTING_ID_BLUETOOTH_EXTRA_OPTIONS = 15;
+
+    /** Device setting ID for bluetooth related tools. */
+    int DEVICE_SETTING_ID_BLUETOOTH_RELATED_TOOLS = 16;
+
+    /** Device setting ID for data sync group. */
+    int DEVICE_SETTING_ID_DATA_SYNC_GROUP = 17;
+
+    /** Device setting ID for keyboard settings. */
+    int DEVICE_SETTING_ID_KEYBOARD_SETTINGS = 18;
+
+    /** Device setting ID for device details footer. */
+    int DEVICE_SETTING_ID_DEVICE_DETAILS_FOOTER = 19;
+
+    /** Device setting ID for ANC. */
+    int DEVICE_SETTING_ID_ANC = 1001;
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
new file mode 100644
index 0000000..9ee33b0
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings
+
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+
+/**
+ * A data class representing a device settings item in bluetooth device details config.
+ *
+ * @property settingId The setting ID of the item, as defined by IntDef [DeviceSettingId].
+ * @property packageName The package name for service binding.
+ * @property className The class name for service binding.
+ * @property intentAction The intent action for service binding.
+ * @property extras Extra bundle
+ */
+data class DeviceSettingItem(
+    @DeviceSettingId val settingId: Int,
+    val packageName: String,
+    val className: String,
+    val intentAction: String,
+    val extras: Bundle = Bundle.EMPTY,
+) : Parcelable {
+
+    override fun describeContents(): Int = 0
+
+    override fun writeToParcel(parcel: Parcel, flags: Int) {
+        parcel.run {
+            writeInt(settingId)
+            writeString(packageName)
+            writeString(className)
+            writeString(intentAction)
+            writeBundle(extras)
+        }
+    }
+
+    companion object {
+        @JvmField
+        val CREATOR: Parcelable.Creator<DeviceSettingItem> =
+            object : Parcelable.Creator<DeviceSettingItem> {
+                override fun createFromParcel(parcel: Parcel) =
+                    parcel.run {
+                        DeviceSettingItem(
+                            settingId = readInt(),
+                            packageName = readString() ?: "",
+                            className = readString() ?: "",
+                            intentAction = readString() ?: "",
+                            extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY,
+                        )
+                    }
+
+                override fun newArray(size: Int): Array<DeviceSettingItem?> {
+                    return arrayOfNulls(size)
+                }
+            }
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java
new file mode 100644
index 0000000..790939a
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java
@@ -0,0 +1,62 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+
+/** An abstract class representing a device setting preference. */
+public abstract class DeviceSettingPreference {
+    @DeviceSettingType private final int mSettingType;
+
+    public static final DeviceSettingPreference UNKNOWN =
+            new DeviceSettingPreference(DeviceSettingType.DEVICE_SETTING_TYPE_UNKNOWN) {};
+
+    protected DeviceSettingPreference(@DeviceSettingType int settingType) {
+        mSettingType = settingType;
+    }
+
+    /** Read a {@link DeviceSettingPreference} instance from {@link Parcel} */
+    @NonNull
+    public static DeviceSettingPreference readFromParcel(@NonNull Parcel in) {
+        int type = in.readInt();
+        switch (type) {
+            case DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH:
+                return ActionSwitchPreference.readFromParcel(in);
+            case DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE:
+                return MultiTogglePreference.readFromParcel(in);
+            default:
+                return UNKNOWN;
+        }
+    }
+
+    /** Writes the instance to {@link Parcel}. */
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mSettingType);
+    }
+
+    /**
+     * Gets the setting type, as defined by IntDef {@link DeviceSettingType}.
+     *
+     * @return the setting type.
+     */
+    @DeviceSettingType
+    public int getSettingType() {
+        return mSettingType;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreferenceState.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreferenceState.java
new file mode 100644
index 0000000..a982af7
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreferenceState.java
@@ -0,0 +1,62 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+
+/** An abstract class representing a device setting preference state. */
+public abstract class DeviceSettingPreferenceState {
+    @DeviceSettingType private final int mSettingType;
+
+    public static final DeviceSettingPreferenceState UNKNOWN =
+            new DeviceSettingPreferenceState(DeviceSettingType.DEVICE_SETTING_TYPE_UNKNOWN) {};
+
+    protected DeviceSettingPreferenceState(@DeviceSettingType int settingType) {
+        mSettingType = settingType;
+    }
+
+    /** Reads a {@link DeviceSettingPreferenceState} from {@link Parcel}. */
+    @NonNull
+    public static DeviceSettingPreferenceState readFromParcel(@NonNull Parcel in) {
+        int type = in.readInt();
+        switch (type) {
+            case DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH:
+                return ActionSwitchPreferenceState.readFromParcel(in);
+            case DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE:
+                return MultiTogglePreferenceState.readFromParcel(in);
+            default:
+                return UNKNOWN;
+        }
+    }
+
+    /** Writes the object to parcel. */
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mSettingType);
+    }
+
+    /**
+     * Gets the setting type, as defined by IntDef {@link DeviceSettingType}.
+     *
+     * @return The setting type.
+     */
+    @DeviceSettingType
+    public int getSettingType() {
+        return mSettingType;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingState.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingState.aidl
new file mode 100644
index 0000000..61429a6
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingState.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+parcelable DeviceSettingState;
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingState.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingState.java
new file mode 100644
index 0000000..63fd4eb
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingState.java
@@ -0,0 +1,165 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import java.util.Objects;
+
+/** A data class representing a device setting state. */
+public class DeviceSettingState implements Parcelable {
+    @DeviceSettingId private final int mSettingId;
+    private final DeviceSettingPreferenceState mPreferenceState;
+    private final Bundle mExtras;
+
+    DeviceSettingState(
+            @DeviceSettingId int settingId,
+            @NonNull DeviceSettingPreferenceState preferenceState,
+            @NonNull Bundle extras) {
+        validate(preferenceState);
+        mSettingId = settingId;
+        mPreferenceState = preferenceState;
+        mExtras = extras;
+    }
+
+    private static void validate(DeviceSettingPreferenceState preferenceState) {
+        if (Objects.isNull(preferenceState)) {
+            throw new IllegalArgumentException("PreferenceState must be set");
+        }
+    }
+
+    /** Reads a {@link DeviceSettingState} from {@link Parcel}. */
+    @NonNull
+    public static DeviceSettingState readFromParcel(@NonNull Parcel in) {
+        int settingId = in.readInt();
+        Bundle extra = in.readBundle(Bundle.class.getClassLoader());
+        DeviceSettingPreferenceState preferenceState =
+                DeviceSettingPreferenceState.readFromParcel(in);
+        return new DeviceSettingState(settingId, preferenceState, extra);
+    }
+
+    public static final Creator<DeviceSettingState> CREATOR =
+            new Creator<>() {
+                @Override
+                @NonNull
+                public DeviceSettingState createFromParcel(@NonNull Parcel in) {
+                    return readFromParcel(in);
+                }
+
+                @Override
+                @NonNull
+                public DeviceSettingState[] newArray(int size) {
+                    return new DeviceSettingState[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Writes the instance to {@link Parcel}. */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mSettingId);
+        dest.writeBundle(mExtras);
+        mPreferenceState.writeToParcel(dest, flags);
+    }
+
+    /** Builder class for {@link DeviceSettingState}. */
+    public static final class Builder {
+        private int mSettingId;
+        private DeviceSettingPreferenceState mSettingPreferenceState;
+        private Bundle mExtras = Bundle.EMPTY;
+
+        public Builder() {}
+
+        /**
+         * Sets the setting ID, as defined by IntDef {@link DeviceSettingId}.
+         *
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setSettingId(@DeviceSettingId int settingId) {
+            mSettingId = settingId;
+            return this;
+        }
+
+        /**
+         * Sets the setting preference state.
+         *
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setPreferenceState(
+                @NonNull DeviceSettingPreferenceState settingPreferenceState) {
+            mSettingPreferenceState = settingPreferenceState;
+            return this;
+        }
+
+        /**
+         * Sets the extras bundle.
+         *
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setExtras(@NonNull Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /** Build the object. */
+        @NonNull
+        public DeviceSettingState build() {
+            return new DeviceSettingState(mSettingId, mSettingPreferenceState, mExtras);
+        }
+    }
+
+    /**
+     * Gets the setting ID, as defined by IntDef {@link DeviceSettingId}.
+     *
+     * @return the setting ID.
+     */
+    @DeviceSettingId
+    public int getSettingId() {
+        return mSettingId;
+    }
+
+    /**
+     * Gets the preference state of the setting.
+     *
+     * @return the setting preference state.
+     */
+    @NonNull
+    public DeviceSettingPreferenceState getPreferenceState() {
+        return mPreferenceState;
+    }
+
+    /**
+     * Gets the extras Bundle.
+     *
+     * @return Returns a Bundle object.
+     */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingType.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingType.java
new file mode 100644
index 0000000..ee4d90f
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingType.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 com.android.settingslib.bluetooth.devicesettings;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.SOURCE)
+@IntDef(
+        value = {
+            DeviceSettingType.DEVICE_SETTING_TYPE_UNKNOWN,
+            DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH,
+            DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE,
+        },
+        open = true)
+public @interface DeviceSettingType {
+    /** Device setting type is unknown. */
+    int DEVICE_SETTING_TYPE_UNKNOWN = 0;
+
+    /** Device setting type is action/switch preference. */
+    int DEVICE_SETTING_TYPE_ACTION_SWITCH = 1;
+
+    /** Device setting type is multi-toggle preference. */
+    int DEVICE_SETTING_TYPE_MULTI_TOGGLE = 2;
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.aidl
new file mode 100644
index 0000000..3201d13
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+parcelable DeviceSettingsConfig;
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt
new file mode 100644
index 0000000..c8a2e9c
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings
+
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+
+/**
+ * A data class representing a bluetooth device details config.
+ *
+ * @property mainContentItems The setting items to be shown in main page.
+ * @property moreSettingsItems The setting items to be shown in more settings page.
+ * @property moreSettingsFooter The footer in more settings page.
+ * @property extras Extra bundle
+ */
+data class DeviceSettingsConfig(
+    val mainContentItems: List<DeviceSettingItem>,
+    val moreSettingsItems: List<DeviceSettingItem>,
+    val moreSettingsFooter: String,
+    val extras: Bundle = Bundle.EMPTY,
+) : Parcelable {
+
+    override fun describeContents(): Int = 0
+
+    override fun writeToParcel(parcel: Parcel, flags: Int) {
+        parcel.run {
+            writeTypedList(mainContentItems)
+            writeTypedList(moreSettingsItems)
+            writeString(moreSettingsFooter)
+            writeBundle(extras)
+        }
+    }
+
+    companion object {
+        @JvmField
+        val CREATOR: Parcelable.Creator<DeviceSettingsConfig> =
+            object : Parcelable.Creator<DeviceSettingsConfig> {
+                override fun createFromParcel(parcel: Parcel): DeviceSettingsConfig =
+                    parcel.run {
+                        DeviceSettingsConfig(
+                            mainContentItems =
+                                arrayListOf<DeviceSettingItem>().also {
+                                    readTypedList(it, DeviceSettingItem.CREATOR)
+                                },
+                            moreSettingsItems =
+                                arrayListOf<DeviceSettingItem>().also {
+                                    readTypedList(it, DeviceSettingItem.CREATOR)
+                                },
+                            moreSettingsFooter = readString()!!,
+                            extras = readBundle((Bundle::class.java.classLoader))!!,
+                        )
+                    }
+
+                override fun newArray(size: Int): Array<DeviceSettingsConfig?> {
+                    return arrayOfNulls(size)
+                }
+            }
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsListener.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsListener.aidl
new file mode 100644
index 0000000..385a780
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsListener.aidl
@@ -0,0 +1,23 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import com.android.settingslib.bluetooth.devicesettings.DeviceSetting;
+
+interface IDeviceSettingsListener {
+   oneway void onDeviceSettingsChanged(in List<DeviceSetting> settings) = 0;
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl
new file mode 100644
index 0000000..d5efac9
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl
@@ -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.settingslib.bluetooth.devicesettings;
+
+import com.android.settingslib.bluetooth.devicesettings.DeviceInfo;
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState;
+import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener;
+
+oneway interface IDeviceSettingsProviderService {
+   void registerDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback);
+   void unregisterDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback);
+   void updateDeviceSettings(in DeviceInfo device, in DeviceSettingState params);
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java
new file mode 100644
index 0000000..01bb6f0
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java
@@ -0,0 +1,232 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** A data class representing a multi-toggle preference. */
+public class MultiTogglePreference extends DeviceSettingPreference implements Parcelable {
+    private final String mTitle;
+    private final ImmutableList<ToggleInfo> mToggleInfos;
+    private final int mState;
+    private final boolean mIsAllowedChangingState;
+    private final Bundle mExtras;
+
+    MultiTogglePreference(
+            @NonNull String title,
+            List<ToggleInfo> toggleInfos,
+            int state,
+            boolean allowChangingState,
+            Bundle extras) {
+        super(DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE);
+        validate(title, state);
+        mTitle = title;
+        mToggleInfos = ImmutableList.copyOf(toggleInfos);
+        mState = state;
+        mIsAllowedChangingState = allowChangingState;
+        mExtras = extras;
+    }
+
+    private static void validate(String title, int state) {
+        if (Objects.isNull(title)) {
+            throw new IllegalArgumentException("Title must be set");
+        }
+        if (state < 0) {
+            throw new IllegalArgumentException("State must be a non-negative integer");
+        }
+    }
+
+    /** Read a {@link MultiTogglePreference} from {@link Parcel}. */
+    @NonNull
+    public static MultiTogglePreference readFromParcel(@NonNull Parcel in) {
+        String title = in.readString();
+        List<ToggleInfo> toggleInfos = new ArrayList<>();
+        in.readTypedList(toggleInfos, ToggleInfo.CREATOR);
+        int state = in.readInt();
+        boolean allowChangingState = in.readBoolean();
+        Bundle extras = in.readBundle(Bundle.class.getClassLoader());
+        return new MultiTogglePreference(title, toggleInfos, state, allowChangingState, extras);
+    }
+
+    public static final Creator<MultiTogglePreference> CREATOR =
+            new Creator<>() {
+                @Override
+                @NonNull
+                public MultiTogglePreference createFromParcel(@NonNull Parcel in) {
+                    in.readInt();
+                    return readFromParcel(in);
+                }
+
+                @Override
+                @NonNull
+                public MultiTogglePreference[] newArray(int size) {
+                    return new MultiTogglePreference[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeString(mTitle);
+        dest.writeTypedList(mToggleInfos, flags);
+        dest.writeInt(mState);
+        dest.writeBoolean(mIsAllowedChangingState);
+        dest.writeBundle(mExtras);
+    }
+
+    /** Builder class for {@link MultiTogglePreference}. */
+    public static final class Builder {
+        private String mTitle;
+        private ImmutableList.Builder<ToggleInfo> mToggleInfos = new ImmutableList.Builder<>();
+        private int mState;
+        private boolean mAllowChangingState;
+        private Bundle mExtras = Bundle.EMPTY;
+
+        /**
+         * Sets the title of the preference.
+         *
+         * @param title The title of the preference.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setTitle(@NonNull String title) {
+            mTitle = title;
+            return this;
+        }
+
+        /**
+         * Adds a toggle in the preference.
+         *
+         * @param toggleInfo The toggle to add.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder addToggleInfo(@NonNull ToggleInfo toggleInfo) {
+            mToggleInfos.add(toggleInfo);
+            return this;
+        }
+
+        /**
+         * Sets the state of the preference.
+         *
+         * @param state The index of the enabled toggle.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setState(int state) {
+            mState = state;
+            return this;
+        }
+
+        /**
+         * Sets whether state can be changed by user.
+         *
+         * @param allowChangingState Whether user is allowed to change state.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setAllowChangingState(boolean allowChangingState) {
+            mAllowChangingState = allowChangingState;
+            return this;
+        }
+
+        /**
+         * Sets the extras bundle.
+         *
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setExtras(@NonNull Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /**
+         * Builds the {@link ToggleInfo} object.
+         *
+         * @return Returns the built {@link ToggleInfo} object.
+         */
+        @NonNull
+        public MultiTogglePreference build() {
+            return new MultiTogglePreference(
+                    mTitle, mToggleInfos.build(), mState, mAllowChangingState, mExtras);
+        }
+    }
+
+    /**
+     * Gets the title of the preference.
+     *
+     * @return The title.
+     */
+    @NonNull
+    public String getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * Gets the state of the {@link MultiTogglePreference}.
+     *
+     * @return Returns the index of the enabled toggle.
+     */
+    public int getState() {
+        return mState;
+    }
+
+    /**
+     * Gets the toggle list in the preference.
+     *
+     * @return the toggle list.
+     */
+    @NonNull
+    public List<ToggleInfo> getToggleInfos() {
+        return mToggleInfos;
+    }
+
+    /**
+     * Gets whether the state can be changed by user.
+     *
+     * @return Whether the state can be changed by user.
+     */
+    public boolean isAllowedChangingState() {
+        return mIsAllowedChangingState;
+    }
+
+    /**
+     * Gets the extras Bundle.
+     *
+     * @return Returns a Bundle object.
+     */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceState.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceState.java
new file mode 100644
index 0000000..239df0b
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceState.java
@@ -0,0 +1,126 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+/** A data class representing a multi-toggle preference state. */
+public class MultiTogglePreferenceState extends DeviceSettingPreferenceState implements Parcelable {
+    private final int mState;
+    private final Bundle mExtras;
+
+    MultiTogglePreferenceState(int state, @NonNull Bundle extras) {
+        super(DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE);
+        mState = state;
+        mExtras = extras;
+    }
+
+    /** Reads a {@link MultiTogglePreferenceState} from {@link Parcel}. */
+    @NonNull
+    public static MultiTogglePreferenceState readFromParcel(@NonNull Parcel in) {
+        int state = in.readInt();
+        Bundle extras = in.readBundle(Bundle.class.getClassLoader());
+        return new MultiTogglePreferenceState(state, extras);
+    }
+
+    public static final Creator<MultiTogglePreferenceState> CREATOR =
+            new Creator<>() {
+                @Override
+                @NonNull
+                public MultiTogglePreferenceState createFromParcel(@NonNull Parcel in) {
+                    in.readInt();
+                    return readFromParcel(in);
+                }
+
+                @Override
+                @NonNull
+                public MultiTogglePreferenceState[] newArray(int size) {
+                    return new MultiTogglePreferenceState[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeInt(mState);
+        dest.writeBundle(mExtras);
+    }
+
+    /** Builder class for {@link MultiTogglePreferenceState}. */
+    public static final class Builder {
+        private int mState;
+        private Bundle mExtras = Bundle.EMPTY;
+
+        public Builder() {}
+
+        /**
+         * Sets the state of {@link MultiTogglePreference}.
+         *
+         * @return Returns the index of enabled toggle.
+         */
+        @NonNull
+        public Builder setState(int state) {
+            mState = state;
+            return this;
+        }
+
+        /**
+         * Sets the extras bundle.
+         *
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setExtras(@NonNull Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /** Builds the object. */
+        @NonNull
+        public MultiTogglePreferenceState build() {
+            return new MultiTogglePreferenceState(mState, mExtras);
+        }
+    }
+
+    /**
+     * Gets the state of the {@link MultiTogglePreference}.
+     *
+     * @return Returns the index of the enabled toggle.
+     */
+    public int getState() {
+        return mState;
+    }
+
+    /**
+     * Gets the extras Bundle.
+     *
+     * @return Returns a Bundle object.
+     */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ToggleInfo.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ToggleInfo.java
new file mode 100644
index 0000000..7dcf3aa
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ToggleInfo.java
@@ -0,0 +1,167 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import java.util.Objects;
+
+/** A data class representing a toggle in {@link MultiTogglePreference}. */
+public class ToggleInfo implements Parcelable {
+    private final String mLabel;
+    private final Bitmap mIcon;
+    private final Bundle mExtras;
+
+    ToggleInfo(@NonNull String label, @NonNull Bitmap icon, @NonNull Bundle extras) {
+        validate(label, icon);
+        mLabel = label;
+        mIcon = icon;
+        mExtras = extras;
+    }
+
+    private static void validate(String label, Bitmap icon) {
+        if (Objects.isNull(label)) {
+            throw new IllegalArgumentException("Label must be set");
+        }
+        if (Objects.isNull(icon)) {
+            throw new IllegalArgumentException("Icon must be set");
+        }
+    }
+
+    /** Read a {@link ToggleInfo} instance from {@link Parcel}. */
+    @NonNull
+    public static ToggleInfo readFromParcel(@NonNull Parcel in) {
+        String label = in.readString();
+        Bitmap icon = in.readParcelable(Bitmap.class.getClassLoader());
+        Bundle extras = in.readBundle(Bundle.class.getClassLoader());
+        return new ToggleInfo(label, icon, extras);
+    }
+
+    public static final Creator<ToggleInfo> CREATOR =
+            new Creator<>() {
+                @Override
+                @NonNull
+                public ToggleInfo createFromParcel(@NonNull Parcel in) {
+                    return readFromParcel(in);
+                }
+
+                @Override
+                @NonNull
+                public ToggleInfo[] newArray(int size) {
+                    return new ToggleInfo[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString(mLabel);
+        dest.writeParcelable(mIcon, flags);
+        dest.writeBundle(mExtras);
+    }
+
+    /** Builder class for {@link ToggleInfo}. */
+    public static final class Builder {
+        private Bitmap mIcon;
+        private String mLabel;
+        private Bundle mExtras = Bundle.EMPTY;
+
+        /**
+         * Sets the label of the toggle.
+         *
+         * @param label The label of the toggle.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setLabel(@NonNull String label) {
+            mLabel = label;
+            return this;
+        }
+
+        /**
+         * Sets the icon of the toggle.
+         *
+         * @param icon The icon of the toggle.
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setIcon(@NonNull Bitmap icon) {
+            mIcon = icon;
+            return this;
+        }
+
+        /**
+         * Sets the extras bundle.
+         *
+         * @return Returns the Builder object.
+         */
+        @NonNull
+        public Builder setExtras(@NonNull Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /**
+         * Builds the {@link ToggleInfo} object.
+         *
+         * @return Returns the built {@link ToggleInfo} object.
+         */
+        @NonNull
+        public ToggleInfo build() {
+            return new ToggleInfo(mLabel, mIcon, mExtras);
+        }
+    }
+
+    /**
+     * Gets the label of the toggle.
+     *
+     * @return the label to be shown under the toggle
+     */
+    @NonNull
+    public String getLabel() {
+        return mLabel;
+    }
+
+    /**
+     * Gets the icon of the toggle.
+     *
+     * @return the icon in toggle
+     */
+    @NonNull
+    public Bitmap getIcon() {
+        return mIcon;
+    }
+
+    /**
+     * Gets the extras Bundle.
+     *
+     * @return Returns a Bundle object.
+     */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
index 8bdbee3..9be3159 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
@@ -16,11 +16,15 @@
 
 package com.android.settingslib.fuelgauge;
 
+import static android.provider.Settings.Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED;
+import static android.provider.Settings.Secure.LOW_POWER_WARNING_ACKNOWLEDGED;
+
 import static com.android.settingslib.fuelgauge.BatterySaverLogging.ACTION_SAVER_STATE_MANUAL_UPDATE;
 import static com.android.settingslib.fuelgauge.BatterySaverLogging.EXTRA_POWER_SAVE_MODE_MANUAL_ENABLED;
 import static com.android.settingslib.fuelgauge.BatterySaverLogging.EXTRA_POWER_SAVE_MODE_MANUAL_ENABLED_REASON;
 import static com.android.settingslib.fuelgauge.BatterySaverLogging.SaverManualEnabledReason;
 
+import android.app.KeyguardManager;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
@@ -33,6 +37,10 @@
 import android.util.Log;
 import android.util.Slog;
 
+import androidx.core.util.Function;
+
+import com.android.settingslib.flags.Flags;
+
 /**
  * Utilities related to battery saver.
  */
@@ -125,6 +133,19 @@
             Log.d(TAG, "Battery saver turning " + (enable ? "ON" : "OFF") + ", reason: " + reason);
         }
         final ContentResolver cr = context.getContentResolver();
+        final PowerManager powerManager = context.getSystemService(PowerManager.class);
+
+        if (Flags.extremePowerLowStateVulnerability()) {
+            var keyguardManager = context.getSystemService(KeyguardManager.class);
+            if (enable
+                    && needFirstTimeWarning
+                    && keyguardManager != null
+                    && keyguardManager.isDeviceLocked()) {
+                var result = powerManager.setPowerSaveModeEnabled(true);
+                Log.d(TAG, "Device is locked, setPowerSaveModeEnabled by default. " + result);
+                return result;
+            }
+        }
 
         final Bundle confirmationExtras = new Bundle(1);
         confirmationExtras.putBoolean(EXTRA_CONFIRM_TEXT_ONLY, false);
@@ -136,7 +157,7 @@
             setBatterySaverConfirmationAcknowledged(context);
         }
 
-        if (context.getSystemService(PowerManager.class).setPowerSaveModeEnabled(enable)) {
+        if (powerManager.setPowerSaveModeEnabled(enable)) {
             if (enable) {
                 final int count =
                         Secure.getInt(cr, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, 0) + 1;
@@ -173,10 +194,7 @@
      * @see #EXTRA_POWER_SAVE_MODE_TRIGGER_LEVEL
      */
     public static boolean maybeShowBatterySaverConfirmation(Context context, Bundle extras) {
-        if (Secure.getInt(context.getContentResolver(),
-                Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 0) != 0
-                && Secure.getInt(context.getContentResolver(),
-                Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED, 0) != 0) {
+        if (isBatterySaverConfirmationHasBeenShowedBefore(context)) {
             // Already shown.
             return false;
         }
@@ -184,6 +202,17 @@
         return true;
     }
 
+    /**
+     * Returns {@code true} if the battery saver confirmation warning has been acknowledged by the
+     * user in the past before.
+     */
+    public static boolean isBatterySaverConfirmationHasBeenShowedBefore(Context context) {
+        Function<String, Integer> secureGetInt =
+                key -> Secure.getInt(context.getContentResolver(), key, /* def= */ 0);
+        return secureGetInt.apply(LOW_POWER_WARNING_ACKNOWLEDGED) != 0
+                && secureGetInt.apply(EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED) != 0;
+    }
+
     private static void recordBatterySaverEnabledReason(Context context, boolean enable,
             @SaverManualEnabledReason int reason) {
         final Bundle enabledReasonExtras = new Bundle(2);
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExt.kt
new file mode 100644
index 0000000..02d684d
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExt.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.settingslib.media.data.repository
+
+import android.media.AudioManager
+import android.media.IVolumeController
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.launch
+
+/** Returns [AudioManager.setVolumeController] events as a [Flow] */
+fun AudioManager.volumeControllerEvents(): Flow<VolumeControllerEvent> =
+    callbackFlow {
+            volumeController =
+                object : IVolumeController.Stub() {
+                    override fun displaySafeVolumeWarning(flags: Int) {
+                        launch { send(VolumeControllerEvent.DisplaySafeVolumeWarning(flags)) }
+                    }
+
+                    override fun volumeChanged(streamType: Int, flags: Int) {
+                        launch { send(VolumeControllerEvent.VolumeChanged(streamType, flags)) }
+                    }
+
+                    override fun masterMuteChanged(flags: Int) {
+                        launch { send(VolumeControllerEvent.MasterMuteChanged(flags)) }
+                    }
+
+                    override fun setLayoutDirection(layoutDirection: Int) {
+                        launch { send(VolumeControllerEvent.SetLayoutDirection(layoutDirection)) }
+                    }
+
+                    override fun dismiss() {
+                        launch { send(VolumeControllerEvent.Dismiss) }
+                    }
+
+                    override fun setA11yMode(mode: Int) {
+                        launch { send(VolumeControllerEvent.SetA11yMode(mode)) }
+                    }
+
+                    override fun displayCsdWarning(
+                        csdWarning: Int,
+                        displayDurationMs: Int,
+                    ) {
+                        launch {
+                            send(
+                                VolumeControllerEvent.DisplayCsdWarning(
+                                    csdWarning,
+                                    displayDurationMs,
+                                )
+                            )
+                        }
+                    }
+                }
+            awaitClose { volumeController = null }
+        }
+        .buffer()
+
+/** Models events received via [IVolumeController] */
+sealed interface VolumeControllerEvent {
+
+    /** @see [IVolumeController.displaySafeVolumeWarning] */
+    data class DisplaySafeVolumeWarning(val flags: Int) : VolumeControllerEvent
+
+    /** @see [IVolumeController.volumeChanged] */
+    data class VolumeChanged(val streamType: Int, val flags: Int) : VolumeControllerEvent
+
+    /** @see [IVolumeController.masterMuteChanged] */
+    data class MasterMuteChanged(val flags: Int) : VolumeControllerEvent
+
+    /** @see [IVolumeController.setLayoutDirection] */
+    data class SetLayoutDirection(val layoutDirection: Int) : VolumeControllerEvent
+
+    /** @see [IVolumeController.setA11yMode] */
+    data class SetA11yMode(val mode: Int) : VolumeControllerEvent
+
+    /** @see [IVolumeController.displayCsdWarning] */
+    data class DisplayCsdWarning(
+        val csdWarning: Int,
+        val displayDurationMs: Int,
+    ) : VolumeControllerEvent
+
+    /** @see [IVolumeController.dismiss] */
+    data object Dismiss : VolumeControllerEvent
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExtTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExtTest.kt
new file mode 100644
index 0000000..83b612d
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/data/repository/AudioManagerVolumeControllerExtTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.settingslib.media.data.repository
+
+import android.media.AudioManager
+import android.media.IVolumeController
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AudioManagerVolumeControllerExtTest {
+
+    private val testScope = TestScope()
+
+    @Captor private lateinit var volumeControllerCaptor: ArgumentCaptor<IVolumeController>
+    @Mock private lateinit var audioManager: AudioManager
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun displaySafeVolumeWarning_emitsEvent() =
+        testEvent(VolumeControllerEvent.DisplaySafeVolumeWarning(1)) { displaySafeVolumeWarning(1) }
+
+    @Test
+    fun volumeChanged_emitsEvent() =
+        testEvent(VolumeControllerEvent.VolumeChanged(1, 2)) { volumeChanged(1, 2) }
+
+    @Test
+    fun masterMuteChanged_emitsEvent() =
+        testEvent(VolumeControllerEvent.MasterMuteChanged(1)) { masterMuteChanged(1) }
+
+    @Test
+    fun setLayoutDirection_emitsEvent() =
+        testEvent(VolumeControllerEvent.SetLayoutDirection(1)) { setLayoutDirection(1) }
+
+    @Test
+    fun setA11yMode_emitsEvent() =
+        testEvent(VolumeControllerEvent.SetA11yMode(1)) { setA11yMode(1) }
+
+    @Test
+    fun displayCsdWarning_emitsEvent() =
+        testEvent(VolumeControllerEvent.DisplayCsdWarning(1, 2)) { displayCsdWarning(1, 2) }
+
+    @Test fun dismiss_emitsEvent() = testEvent(VolumeControllerEvent.Dismiss) { dismiss() }
+
+    private fun testEvent(
+        expectedEvent: VolumeControllerEvent,
+        emit: IVolumeController.() -> Unit,
+    ) =
+        testScope.runTest {
+            var event: VolumeControllerEvent? = null
+            audioManager.volumeControllerEvents().onEach { event = it }.launchIn(backgroundScope)
+            runCurrent()
+            verify(audioManager).volumeController = volumeControllerCaptor.capture()
+
+            volumeControllerCaptor.value.emit()
+            runCurrent()
+
+            assertThat(event).isEqualTo(expectedEvent)
+        }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceStateTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceStateTest.java
new file mode 100644
index 0000000..e8e1556
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceStateTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+import android.os.Parcel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class ActionSwitchPreferenceStateTest {
+
+    @Test
+    public void getMethods() {
+        ActionSwitchPreferenceState state1 =
+                new ActionSwitchPreferenceState.Builder()
+                        .setChecked(true)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+        ActionSwitchPreferenceState state2 =
+                new ActionSwitchPreferenceState.Builder()
+                        .setChecked(false)
+                        .setExtras(buildBundle("key2", "value2"))
+                        .build();
+
+        assertThat(state1.getChecked()).isTrue();
+        assertThat(state2.getChecked()).isFalse();
+        assertThat(state1.getExtras().getString("key1")).isEqualTo("value1");
+        assertThat(state2.getExtras().getString("key2")).isEqualTo("value2");
+    }
+
+    @Test
+    public void parcelOperation_notChecked() {
+        ActionSwitchPreferenceState state =
+                new ActionSwitchPreferenceState.Builder()
+                        .setChecked(false)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        ActionSwitchPreferenceState fromParcel = writeAndRead(state);
+
+        assertThat(fromParcel.getChecked()).isEqualTo(state.getChecked());
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(state.getExtras().getString("key1"));
+    }
+
+    @Test
+    public void parcelOperation_checked() {
+        ActionSwitchPreferenceState state =
+                new ActionSwitchPreferenceState.Builder()
+                        .setChecked(true)
+                        .setExtras(buildBundle("key2", "value2"))
+                        .build();
+
+        ActionSwitchPreferenceState fromParcel = writeAndRead(state);
+
+        assertThat(fromParcel.getChecked()).isEqualTo(state.getChecked());
+        assertThat(fromParcel.getExtras().getString("key2"))
+                .isEqualTo(state.getExtras().getString("key2"));
+    }
+
+    private Bundle buildBundle(String key, String value) {
+        Bundle bundle = new Bundle();
+        bundle.putString(key, value);
+        return bundle;
+    }
+
+    private ActionSwitchPreferenceState writeAndRead(ActionSwitchPreferenceState state) {
+        Parcel parcel = Parcel.obtain();
+        state.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        ActionSwitchPreferenceState fromParcel =
+                ActionSwitchPreferenceState.CREATOR.createFromParcel(parcel);
+        return fromParcel;
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceTest.java
new file mode 100644
index 0000000..354d0f6
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Parcel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class ActionSwitchPreferenceTest {
+
+    @Test
+    public void build_withoutTitle_fail() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    ActionSwitchPreference unused =
+                            new ActionSwitchPreference.Builder().setSummary("summary").build();
+                });
+    }
+
+    @Test
+    public void build_withTitle_successfully() {
+        ActionSwitchPreference unused =
+                new ActionSwitchPreference.Builder().setTitle("title").build();
+    }
+
+    @Test
+    public void build_withAllFields_successfully() {
+        ActionSwitchPreference unused =
+                new ActionSwitchPreference.Builder()
+                        .setTitle("title")
+                        .setSummary("summary")
+                        .setIntent(new Intent("intent_action"))
+                        .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
+                        .setHasSwitch(true)
+                        .setChecked(true)
+                        .setAllowedChangingState(true)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+    }
+
+    @Test
+    public void getMethods() {
+        Intent intent = new Intent("intent_action");
+        Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+        ActionSwitchPreference preference = builder().setIcon(icon).setIntent(intent).build();
+
+        assertThat(preference.getTitle()).isEqualTo("title");
+        assertThat(preference.getSummary()).isEqualTo("summary");
+        assertThat(preference.getIcon()).isSameInstanceAs(icon);
+        assertThat(preference.getIntent()).isSameInstanceAs(intent);
+        assertThat(preference.hasSwitch()).isTrue();
+        assertThat(preference.getChecked()).isTrue();
+        assertThat(preference.isAllowedChangingState()).isTrue();
+        assertThat(preference.getExtras().getString("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    public void parcelOperation() {
+        Intent intent = new Intent("intent_action");
+        Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+        ActionSwitchPreference preference = builder().setIcon(icon).setIntent(intent).build();
+
+        ActionSwitchPreference fromParcel = writeAndRead(preference);
+
+        assertThat(fromParcel.getTitle()).isEqualTo(preference.getTitle());
+        assertThat(fromParcel.getSummary()).isEqualTo(preference.getSummary());
+        assertThat(fromParcel.getIcon().sameAs(preference.getIcon())).isTrue();
+        assertThat(fromParcel.getIntent().getAction()).isSameInstanceAs("intent_action");
+        assertThat(fromParcel.hasSwitch()).isEqualTo(preference.hasSwitch());
+        assertThat(fromParcel.getChecked()).isEqualTo(preference.getChecked());
+        assertThat(fromParcel.isAllowedChangingState())
+                .isEqualTo(preference.isAllowedChangingState());
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(preference.getExtras().getString("key1"));
+    }
+
+    @Test
+    public void parcelOperation_noIntent() {
+        Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+        ActionSwitchPreference preference = builder().setIcon(icon).setIntent(null).build();
+
+        ActionSwitchPreference fromParcel = writeAndRead(preference);
+
+        assertThat(fromParcel.getTitle()).isEqualTo(preference.getTitle());
+        assertThat(fromParcel.getSummary()).isEqualTo(preference.getSummary());
+        assertThat(fromParcel.getIcon().sameAs(preference.getIcon())).isTrue();
+        assertThat(preference.getIntent()).isNull();
+        assertThat(fromParcel.hasSwitch()).isEqualTo(preference.hasSwitch());
+        assertThat(fromParcel.getChecked()).isEqualTo(preference.getChecked());
+        assertThat(fromParcel.isAllowedChangingState())
+                .isEqualTo(preference.isAllowedChangingState());
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(preference.getExtras().getString("key1"));
+    }
+
+    @Test
+    public void parcelOperation_noIcon() {
+        Intent intent = new Intent("intent_action");
+        ActionSwitchPreference preference = builder().setIcon(null).setIntent(intent).build();
+
+        ActionSwitchPreference fromParcel = writeAndRead(preference);
+
+        assertThat(fromParcel.getTitle()).isEqualTo(preference.getTitle());
+        assertThat(fromParcel.getSummary()).isEqualTo(preference.getSummary());
+        assertThat(fromParcel.getIcon()).isNull();
+        assertThat(fromParcel.getIntent().getAction()).isSameInstanceAs("intent_action");
+        assertThat(fromParcel.hasSwitch()).isEqualTo(preference.hasSwitch());
+        assertThat(fromParcel.getChecked()).isEqualTo(preference.getChecked());
+        assertThat(fromParcel.isAllowedChangingState())
+                .isEqualTo(preference.isAllowedChangingState());
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(preference.getExtras().getString("key1"));
+    }
+
+    private Bundle buildBundle(String key, String value) {
+        Bundle bundle = new Bundle();
+        bundle.putString(key, value);
+        return bundle;
+    }
+
+    private ActionSwitchPreference writeAndRead(ActionSwitchPreference preference) {
+        Parcel parcel = Parcel.obtain();
+        preference.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        ActionSwitchPreference fromParcel = ActionSwitchPreference.CREATOR.createFromParcel(parcel);
+        return fromParcel;
+    }
+
+    private ActionSwitchPreference.Builder builder() {
+        return new ActionSwitchPreference.Builder()
+                .setTitle("title")
+                .setSummary("summary")
+                .setHasSwitch(true)
+                .setChecked(true)
+                .setAllowedChangingState(true)
+                .setExtras(buildBundle("key1", "value1"));
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfoTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfoTest.java
new file mode 100644
index 0000000..fd5b075
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfoTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Bundle;
+import android.os.Parcel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class DeviceInfoTest {
+    @Test
+    public void build_withoutBluetoothAddress_fail() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    DeviceInfo unused =
+                            new DeviceInfo.Builder()
+                                    .setExtras(buildBundle("key1", "value1"))
+                                    .build();
+                });
+    }
+
+    @Test
+    public void build_withoutExtra_successfully() {
+        DeviceInfo unused = new DeviceInfo.Builder().setBluetoothAddress("12:34:56:78").build();
+    }
+
+    @Test
+    public void build_withAllFields_successfully() {
+        DeviceInfo unused =
+                new DeviceInfo.Builder()
+                        .setBluetoothAddress("12:34:56:78")
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+    }
+
+    @Test
+    public void getMethods() {
+        DeviceInfo info =
+                new DeviceInfo.Builder()
+                        .setBluetoothAddress("12:34:56:78")
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        assertThat(info.getBluetoothAddress()).isEqualTo("12:34:56:78");
+        assertThat(info.getExtras().getString("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    public void parcelOperation() {
+        DeviceInfo info =
+                new DeviceInfo.Builder()
+                        .setBluetoothAddress("12:34:56:78")
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        DeviceInfo fromParcel = writeAndRead(info);
+
+        assertThat(fromParcel.getBluetoothAddress()).isEqualTo(info.getBluetoothAddress());
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(info.getExtras().getString("key1"));
+    }
+
+    private Bundle buildBundle(String key, String value) {
+        Bundle bundle = new Bundle();
+        bundle.putString(key, value);
+        return bundle;
+    }
+
+    private DeviceInfo writeAndRead(DeviceInfo state) {
+        Parcel parcel = Parcel.obtain();
+        state.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        DeviceInfo fromParcel = DeviceInfo.CREATOR.createFromParcel(parcel);
+        return fromParcel;
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItemTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItemTest.kt
new file mode 100644
index 0000000..56e9b6c
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItemTest.kt
@@ -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.settingslib.bluetooth.devicesettings
+
+import android.os.Bundle
+import android.os.Parcel
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class DeviceSettingItemTest {
+
+    @Test
+    fun parcelOperation() {
+        val item =
+            DeviceSettingItem(
+                settingId = 1,
+                packageName = "package_name",
+                className = "class_name",
+                intentAction = "intent_action",
+                extras = Bundle().apply { putString("key1", "value1") },
+            )
+
+        val fromParcel = writeAndRead(item)
+
+        assertThat(fromParcel.settingId).isEqualTo(item.settingId)
+        assertThat(fromParcel.packageName).isEqualTo(item.packageName)
+        assertThat(fromParcel.className).isEqualTo(item.className)
+        assertThat(fromParcel.intentAction).isEqualTo(item.intentAction)
+        assertThat(fromParcel.extras.getString("key1")).isEqualTo(item.extras.getString("key1"))
+    }
+
+    private fun writeAndRead(item: DeviceSettingItem): DeviceSettingItem {
+        val parcel = Parcel.obtain()
+        item.writeToParcel(parcel, 0)
+        parcel.setDataPosition(0)
+        return DeviceSettingItem.CREATOR.createFromParcel(parcel)
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingStateTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingStateTest.java
new file mode 100644
index 0000000..12b7a0f
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingStateTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Bundle;
+import android.os.Parcel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class DeviceSettingStateTest {
+    private static final ActionSwitchPreferenceState ACTION_SWITCH_PREFERENCE_STATE =
+            new ActionSwitchPreferenceState.Builder().setChecked(true).build();
+    private static final MultiTogglePreferenceState MULTI_TOGGLE_PREFERENCE_STATE =
+            new MultiTogglePreferenceState.Builder().setState(123).build();
+
+    @Test
+    public void build_withoutPreferenceState_fail() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    DeviceSettingState unused =
+                            new DeviceSettingState.Builder()
+                                    .setSettingId(123)
+                                    .setExtras(buildBundle("key1", "value1"))
+                                    .build();
+                });
+    }
+
+    @Test
+    public void build_withoutExtra_successfully() {
+        DeviceSettingState unused =
+                new DeviceSettingState.Builder()
+                        .setSettingId(123)
+                        .setPreferenceState(ACTION_SWITCH_PREFERENCE_STATE)
+                        .build();
+    }
+
+    @Test
+    public void build_withAllFields_successfully() {
+        DeviceSettingState unused =
+                new DeviceSettingState.Builder()
+                        .setSettingId(123)
+                        .setPreferenceState(ACTION_SWITCH_PREFERENCE_STATE)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+    }
+
+    @Test
+    public void getMethods_actionSwitchPreferenceState() {
+        DeviceSettingState state =
+                new DeviceSettingState.Builder()
+                        .setSettingId(123)
+                        .setPreferenceState(ACTION_SWITCH_PREFERENCE_STATE)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        assertThat(state.getSettingId()).isEqualTo(123);
+        assertThat(state.getPreferenceState()).isInstanceOf(ActionSwitchPreferenceState.class);
+        assertThat(state.getExtras().getString("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    public void getMethods_multiTogglePreference() {
+        DeviceSettingState state =
+                new DeviceSettingState.Builder()
+                        .setSettingId(123)
+                        .setPreferenceState(MULTI_TOGGLE_PREFERENCE_STATE)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        assertThat(state.getSettingId()).isEqualTo(123);
+        assertThat(state.getPreferenceState()).isInstanceOf(MultiTogglePreferenceState.class);
+        assertThat(state.getExtras().getString("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    public void parcelOperation_actionSwitchPreferenceState() {
+        DeviceSettingState state =
+                new DeviceSettingState.Builder()
+                        .setSettingId(123)
+                        .setPreferenceState(ACTION_SWITCH_PREFERENCE_STATE)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        DeviceSettingState fromParcel = writeAndRead(state);
+
+        assertThat(fromParcel.getSettingId()).isEqualTo(state.getSettingId());
+        assertThat(fromParcel.getPreferenceState()).isInstanceOf(ActionSwitchPreferenceState.class);
+        assertThat(fromParcel.getPreferenceState().getSettingType())
+                .isEqualTo(DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH);
+        assertThat(((ActionSwitchPreferenceState) fromParcel.getPreferenceState()).getChecked())
+                .isTrue();
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(state.getExtras().getString("key1"));
+    }
+
+    @Test
+    public void parcelOperation_multiTogglePreferenceState() {
+        DeviceSettingState state =
+                new DeviceSettingState.Builder()
+                        .setSettingId(123)
+                        .setPreferenceState(MULTI_TOGGLE_PREFERENCE_STATE)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        DeviceSettingState fromParcel = writeAndRead(state);
+
+        assertThat(fromParcel.getSettingId()).isEqualTo(state.getSettingId());
+        assertThat(fromParcel.getPreferenceState()).isInstanceOf(MultiTogglePreferenceState.class);
+        assertThat(fromParcel.getPreferenceState().getSettingType())
+                .isEqualTo(DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE);
+        assertThat(((MultiTogglePreferenceState) fromParcel.getPreferenceState()).getState())
+                .isEqualTo(123);
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(state.getExtras().getString("key1"));
+    }
+
+    @Test
+    public void parcelOperation_unknownPreferenceState() {
+        DeviceSettingState state =
+                new DeviceSettingState.Builder()
+                        .setSettingId(123)
+                        .setPreferenceState(new DeviceSettingPreferenceState(123) {})
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        DeviceSettingState fromParcel = writeAndRead(state);
+
+        assertThat(fromParcel.getSettingId()).isEqualTo(state.getSettingId());
+        assertThat(fromParcel.getPreferenceState())
+                .isSameInstanceAs(DeviceSettingPreferenceState.UNKNOWN);
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(state.getExtras().getString("key1"));
+    }
+
+    private Bundle buildBundle(String key, String value) {
+        Bundle bundle = new Bundle();
+        bundle.putString(key, value);
+        return bundle;
+    }
+
+    private DeviceSettingState writeAndRead(DeviceSettingState state) {
+        Parcel parcel = Parcel.obtain();
+        state.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        DeviceSettingState fromParcel = DeviceSettingState.CREATOR.createFromParcel(parcel);
+        return fromParcel;
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingTest.java
new file mode 100644
index 0000000..98dc54b
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Bundle;
+import android.os.Parcel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class DeviceSettingTest {
+    private static final ActionSwitchPreference ACTION_SWITCH_PREFERENCE =
+            new ActionSwitchPreference.Builder().setTitle("action_switch_preference").build();
+    private static final MultiTogglePreference MULTI_TOGGLE_PREFERENCE =
+            new MultiTogglePreference.Builder().setTitle("multi_toggle_preference").build();
+
+    @Test
+    public void build_withoutPreference_fail() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    DeviceSetting unused =
+                            new DeviceSetting.Builder()
+                                    .setSettingId(123)
+                                    .setExtras(buildBundle("key1", "value1"))
+                                    .build();
+                });
+    }
+
+    @Test
+    public void build_withoutExtra_successfully() {
+        DeviceSetting unused =
+                new DeviceSetting.Builder()
+                        .setSettingId(123)
+                        .setPreference(ACTION_SWITCH_PREFERENCE)
+                        .build();
+    }
+
+    @Test
+    public void build_withAllFields_successfully() {
+        DeviceSetting unused =
+                new DeviceSetting.Builder()
+                        .setSettingId(123)
+                        .setPreference(ACTION_SWITCH_PREFERENCE)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+    }
+
+    @Test
+    public void getMethods_actionSwitchPreference() {
+        DeviceSetting setting =
+                new DeviceSetting.Builder()
+                        .setSettingId(123)
+                        .setPreference(ACTION_SWITCH_PREFERENCE)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        assertThat(setting.getSettingId()).isEqualTo(123);
+        assertThat(setting.getPreference()).isInstanceOf(ActionSwitchPreference.class);
+        assertThat(setting.getExtras().getString("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    public void getMethods_multiTogglePreference() {
+        DeviceSetting setting =
+                new DeviceSetting.Builder()
+                        .setSettingId(123)
+                        .setPreference(MULTI_TOGGLE_PREFERENCE)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        assertThat(setting.getSettingId()).isEqualTo(123);
+        assertThat(setting.getPreference()).isInstanceOf(MultiTogglePreference.class);
+        assertThat(setting.getExtras().getString("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    public void parcelOperation_actionSwitchPreference() {
+        DeviceSetting setting =
+                new DeviceSetting.Builder()
+                        .setSettingId(123)
+                        .setPreference(ACTION_SWITCH_PREFERENCE)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        DeviceSetting fromParcel = writeAndRead(setting);
+
+        assertThat(fromParcel.getSettingId()).isEqualTo(setting.getSettingId());
+        assertThat(fromParcel.getPreference()).isInstanceOf(ActionSwitchPreference.class);
+        assertThat(fromParcel.getPreference().getSettingType())
+                .isEqualTo(DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH);
+        assertThat(((ActionSwitchPreference) fromParcel.getPreference()).getTitle())
+                .isEqualTo("action_switch_preference");
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(setting.getExtras().getString("key1"));
+    }
+
+    @Test
+    public void parcelOperation_multiTogglePreference() {
+        DeviceSetting setting =
+                new DeviceSetting.Builder()
+                        .setSettingId(123)
+                        .setPreference(MULTI_TOGGLE_PREFERENCE)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        DeviceSetting fromParcel = writeAndRead(setting);
+
+        assertThat(fromParcel.getSettingId()).isEqualTo(setting.getSettingId());
+        assertThat(fromParcel.getPreference()).isInstanceOf(MultiTogglePreference.class);
+        assertThat(fromParcel.getPreference().getSettingType())
+                .isEqualTo(DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE);
+        assertThat(((MultiTogglePreference) fromParcel.getPreference()).getTitle())
+                .isEqualTo("multi_toggle_preference");
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(setting.getExtras().getString("key1"));
+    }
+
+    @Test
+    public void parcelOperation_unknownPreference() {
+        DeviceSetting setting =
+                new DeviceSetting.Builder()
+                        .setSettingId(123)
+                        .setPreference(new DeviceSettingPreference(123) {})
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        DeviceSetting fromParcel = writeAndRead(setting);
+
+        assertThat(fromParcel.getSettingId()).isEqualTo(setting.getSettingId());
+        assertThat(fromParcel.getPreference()).isSameInstanceAs(DeviceSettingPreference.UNKNOWN);
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(setting.getExtras().getString("key1"));
+    }
+
+    private Bundle buildBundle(String key, String value) {
+        Bundle bundle = new Bundle();
+        bundle.putString(key, value);
+        return bundle;
+    }
+
+    private DeviceSetting writeAndRead(DeviceSetting state) {
+        Parcel parcel = Parcel.obtain();
+        state.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        DeviceSetting fromParcel = DeviceSetting.CREATOR.createFromParcel(parcel);
+        return fromParcel;
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt
new file mode 100644
index 0000000..2b29a6e
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings
+
+import android.os.Bundle
+import android.os.Parcel
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class DeviceSettingsConfigTest {
+
+    @Test
+    fun parcelOperation() {
+        val config =
+            DeviceSettingsConfig(
+                mainContentItems =
+                    listOf(
+                        DeviceSettingItem(
+                            1,
+                            "package_name_1",
+                            "class_name_1",
+                            "intent_action_1",
+                            Bundle()
+                        )
+                    ),
+                moreSettingsItems =
+                    listOf(
+                        DeviceSettingItem(
+                            2,
+                            "package_name_2",
+                            "class_name_2",
+                            "intent_action_2",
+                            Bundle()
+                        )
+                    ),
+                moreSettingsFooter = "footer",
+                extras = Bundle().apply { putString("key1", "value1") },
+            )
+
+        val fromParcel = writeAndRead(config)
+
+        assertThat(fromParcel.mainContentItems.stream().map { it.settingId }.toList())
+            .containsExactly(1)
+        assertThat(fromParcel.mainContentItems.stream().map { it.packageName }.toList())
+            .containsExactly("package_name_1")
+        assertThat(fromParcel.mainContentItems.stream().map { it.className }.toList())
+            .containsExactly("class_name_1")
+        assertThat(fromParcel.mainContentItems.stream().map { it.intentAction }.toList())
+            .containsExactly("intent_action_1")
+        assertThat(fromParcel.moreSettingsItems.stream().map { it.settingId }.toList())
+            .containsExactly(2)
+        assertThat(fromParcel.moreSettingsItems.stream().map { it.packageName }.toList())
+            .containsExactly("package_name_2")
+        assertThat(fromParcel.moreSettingsItems.stream().map { it.className }.toList())
+            .containsExactly("class_name_2")
+        assertThat(fromParcel.moreSettingsItems.stream().map { it.intentAction }.toList())
+            .containsExactly("intent_action_2")
+        assertThat(fromParcel.moreSettingsFooter).isEqualTo(config.moreSettingsFooter)
+    }
+
+    private fun writeAndRead(item: DeviceSettingsConfig): DeviceSettingsConfig {
+        val parcel = Parcel.obtain()
+        item.writeToParcel(parcel, 0)
+        parcel.setDataPosition(0)
+        return DeviceSettingsConfig.CREATOR.createFromParcel(parcel)
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceStateTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceStateTest.java
new file mode 100644
index 0000000..2645fc5
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceStateTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+import android.os.Parcel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class MultiTogglePreferenceStateTest {
+
+    @Test
+    public void getMethods() {
+        MultiTogglePreferenceState state1 =
+                new MultiTogglePreferenceState.Builder()
+                        .setState(1)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+        MultiTogglePreferenceState state2 =
+                new MultiTogglePreferenceState.Builder()
+                        .setState(2)
+                        .setExtras(buildBundle("key2", "value2"))
+                        .build();
+
+        assertThat(state1.getState()).isEqualTo(1);
+        assertThat(state2.getState()).isEqualTo(2);
+        assertThat(state1.getExtras().getString("key1")).isEqualTo("value1");
+        assertThat(state2.getExtras().getString("key2")).isEqualTo("value2");
+    }
+
+    @Test
+    public void parcelOperation() {
+        MultiTogglePreferenceState state =
+                new MultiTogglePreferenceState.Builder()
+                        .setState(123)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        MultiTogglePreferenceState fromParcel = writeAndRead(state);
+
+        assertThat(fromParcel.getState()).isEqualTo(state.getState());
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(state.getExtras().getString("key1"));
+    }
+
+    private Bundle buildBundle(String key, String value) {
+        Bundle bundle = new Bundle();
+        bundle.putString(key, value);
+        return bundle;
+    }
+
+    private MultiTogglePreferenceState writeAndRead(MultiTogglePreferenceState state) {
+        Parcel parcel = Parcel.obtain();
+        state.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        MultiTogglePreferenceState fromParcel =
+                MultiTogglePreferenceState.CREATOR.createFromParcel(parcel);
+        return fromParcel;
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java
new file mode 100644
index 0000000..62fcb5e
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Parcel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class MultiTogglePreferenceTest {
+    private static final Bitmap ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+    private static final ToggleInfo TOGGLE_INFO_1 =
+            new ToggleInfo.Builder().setLabel("label1").setIcon(ICON).build();
+    private static final ToggleInfo TOGGLE_INFO_2 =
+            new ToggleInfo.Builder().setLabel("label2").setIcon(ICON).build();
+
+    @Test
+    public void build_withoutTitle_fail() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    MultiTogglePreference unused =
+                            new MultiTogglePreference.Builder()
+                                    .addToggleInfo(TOGGLE_INFO_1)
+                                    .setState(0)
+                                    .setAllowChangingState(true)
+                                    .setExtras(buildBundle("key1", "value1"))
+                                    .build();
+                });
+    }
+
+    @Test
+    public void build_withNegativeState_fail() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    MultiTogglePreference unused =
+                            new MultiTogglePreference.Builder()
+                                    .setTitle("title")
+                                    .addToggleInfo(TOGGLE_INFO_1)
+                                    .setState(-1)
+                                    .setAllowChangingState(true)
+                                    .setExtras(buildBundle("key1", "value1"))
+                                    .build();
+                });
+    }
+
+    @Test
+    public void build_withoutExtra_successfully() {
+        MultiTogglePreference unused =
+                new MultiTogglePreference.Builder()
+                        .setTitle("title")
+                        .addToggleInfo(TOGGLE_INFO_1)
+                        .addToggleInfo(TOGGLE_INFO_2)
+                        .setState(123)
+                        .setAllowChangingState(true)
+                        .build();
+    }
+
+    @Test
+    public void build_withAllFields_successfully() {
+        MultiTogglePreference unused =
+                new MultiTogglePreference.Builder()
+                        .setTitle("title")
+                        .addToggleInfo(TOGGLE_INFO_1)
+                        .addToggleInfo(TOGGLE_INFO_2)
+                        .setState(123)
+                        .setAllowChangingState(true)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+    }
+
+    @Test
+    public void getMethods() {
+        MultiTogglePreference preference =
+                new MultiTogglePreference.Builder()
+                        .setTitle("title")
+                        .addToggleInfo(TOGGLE_INFO_1)
+                        .addToggleInfo(TOGGLE_INFO_2)
+                        .setState(123)
+                        .setAllowChangingState(true)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        assertThat(preference.getTitle()).isEqualTo("title");
+        assertThat(preference.getToggleInfos().stream().map(ToggleInfo::getLabel).toList())
+                .containsExactly("label1", "label2");
+        assertThat(preference.getState()).isEqualTo(123);
+        assertThat(preference.isAllowedChangingState()).isTrue();
+        assertThat(preference.getExtras().getString("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    public void parcelOperation() {
+        MultiTogglePreference preference =
+                new MultiTogglePreference.Builder()
+                        .setTitle("title")
+                        .addToggleInfo(TOGGLE_INFO_1)
+                        .addToggleInfo(TOGGLE_INFO_2)
+                        .setState(123)
+                        .setAllowChangingState(true)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        MultiTogglePreference fromParcel = writeAndRead(preference);
+
+        assertThat(fromParcel.getTitle()).isEqualTo(preference.getTitle());
+        assertThat(fromParcel.getToggleInfos().stream().map(ToggleInfo::getLabel).toList())
+                .containsExactly("label1", "label2");
+        assertThat(fromParcel.getState()).isEqualTo(preference.getState());
+        assertThat(fromParcel.isAllowedChangingState())
+                .isEqualTo(preference.isAllowedChangingState());
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(preference.getExtras().getString("key1"));
+    }
+
+    private Bundle buildBundle(String key, String value) {
+        Bundle bundle = new Bundle();
+        bundle.putString(key, value);
+        return bundle;
+    }
+
+    private MultiTogglePreference writeAndRead(MultiTogglePreference preference) {
+        Parcel parcel = Parcel.obtain();
+        preference.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        MultiTogglePreference fromParcel = MultiTogglePreference.CREATOR.createFromParcel(parcel);
+        return fromParcel;
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ToggleInfoTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ToggleInfoTest.java
new file mode 100644
index 0000000..439749a
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ToggleInfoTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.settingslib.bluetooth.devicesettings;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Parcel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class ToggleInfoTest {
+
+    @Test
+    public void build_withoutIcon_fail() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    ToggleInfo unused =
+                            new ToggleInfo.Builder()
+                                    .setLabel("label")
+                                    .setExtras(buildBundle("key1", "value1"))
+                                    .build();
+                });
+    }
+
+    @Test
+    public void build_withoutLabel_fail() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    ToggleInfo unused =
+                            new ToggleInfo.Builder()
+                                    .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
+                                    .setExtras(buildBundle("key1", "value1"))
+                                    .build();
+                });
+    }
+
+    @Test
+    public void build_withoutExtra_successfully() {
+        ToggleInfo unused =
+                new ToggleInfo.Builder()
+                        .setLabel("label")
+                        .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
+                        .build();
+    }
+
+    @Test
+    public void build_withAllFields_successfully() {
+        ToggleInfo unused =
+                new ToggleInfo.Builder()
+                        .setLabel("label")
+                        .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+    }
+
+    @Test
+    public void getMethods() {
+        Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+        ToggleInfo info =
+                new ToggleInfo.Builder()
+                        .setLabel("label")
+                        .setIcon(icon)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        assertThat(info.getLabel()).isEqualTo("label");
+        assertThat(info.getIcon()).isSameInstanceAs(icon);
+        assertThat(info.getExtras().getString("key1")).isEqualTo("value1");
+    }
+
+    @Test
+    public void parcelOperation() {
+        Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+        ToggleInfo info =
+                new ToggleInfo.Builder()
+                        .setLabel("label")
+                        .setIcon(icon)
+                        .setExtras(buildBundle("key1", "value1"))
+                        .build();
+
+        ToggleInfo fromParcel = writeAndRead(info);
+
+        assertThat(fromParcel.getLabel()).isEqualTo(info.getLabel());
+        assertThat(fromParcel.getIcon().sameAs(info.getIcon())).isTrue();
+        assertThat(fromParcel.getExtras().getString("key1"))
+                .isEqualTo(info.getExtras().getString("key1"));
+    }
+
+    private Bundle buildBundle(String key, String value) {
+        Bundle bundle = new Bundle();
+        bundle.putString(key, value);
+        return bundle;
+    }
+
+    private ToggleInfo writeAndRead(ToggleInfo state) {
+        Parcel parcel = Parcel.obtain();
+        state.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        ToggleInfo fromParcel = ToggleInfo.CREATOR.createFromParcel(parcel);
+        return fromParcel;
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java
index 80301c0..b143b22 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java
@@ -28,23 +28,31 @@
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.KeyguardManager;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.os.PowerManager;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings.Global;
 import android.provider.Settings.Secure;
 
+import com.android.settingslib.flags.Flags;
+
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 import org.robolectric.RobolectricTestRunner;
 
 import java.util.List;
@@ -54,26 +62,22 @@
     private static final int BATTERY_SAVER_THRESHOLD_1 = 15;
     private static final int BATTERY_SAVER_THRESHOLD_2 = 20;
 
-    @Mock
-    private Context mMockContext;
+    @Rule(order = 0) public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Rule(order = 1) public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 
-    @Mock
-    private ContentResolver mMockResolver;
-
-    @Mock
-    private PowerManager mMockPowerManager;
+    @Mock private Context mMockContext;
+    @Mock private ContentResolver mMockResolver;
+    @Mock private PowerManager mMockPowerManager;
 
     @Before
     public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
         when(mMockContext.getContentResolver()).thenReturn(mMockResolver);
         when(mMockContext.getSystemService(eq(PowerManager.class))).thenReturn(mMockPowerManager);
         when(mMockPowerManager.setPowerSaveModeEnabled(anyBoolean())).thenReturn(true);
     }
 
     @Test
-    public void testSetPowerSaveMode_enableWithWarning_firstCall_needConfirmationWarning() {
+    public void setPowerSaveMode_enableWithWarning_firstCall_needConfirmationWarning() {
         Secure.putString(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, "null");
         Secure.putString(mMockResolver, Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED, "null");
         Secure.putString(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, "null");
@@ -96,7 +100,7 @@
     }
 
     @Test
-    public void testSetPowerSaveMode_enableWithWarning_secondCall_expectUpdateIntent() {
+    public void setPowerSaveMode_enableWithWarning_secondCall_expectUpdateIntent() {
         // Already acked.
         Secure.putInt(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 1);
         Secure.putInt(mMockResolver, Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED, 1);
@@ -119,7 +123,7 @@
     }
 
     @Test
-    public void testSetPowerSaveMode_enableWithWarning_thirdCall_expectUpdateIntent() {
+    public void setPowerSaveMode_enableWithWarning_thirdCall_expectUpdateIntent() {
         // Already acked.
         Secure.putInt(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 1);
         Secure.putInt(mMockResolver, Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED, 1);
@@ -142,7 +146,7 @@
     }
 
     @Test
-    public void testSetPowerSaveMode_enableWithWarning_5thCall_needAutoSuggestionWarning() {
+    public void setPowerSaveMode_enableWithWarning_5thCall_needAutoSuggestionWarning() {
         // Already acked.
         Secure.putInt(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 1);
         Secure.putInt(mMockResolver, Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED, 1);
@@ -166,7 +170,7 @@
     }
 
     @Test
-    public void testSetPowerSaveMode_enableWithoutWarning_expectUpdateIntent() {
+    public void setPowerSaveMode_enableWithoutWarning_expectUpdateIntent() {
         Secure.putString(mMockResolver, Secure.LOW_POWER_WARNING_ACKNOWLEDGED, "null");
         Secure.putString(mMockResolver, Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED, "null");
         Secure.putString(mMockResolver, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, "null");
@@ -187,17 +191,17 @@
     }
 
     @Test
-    public void testSetPowerSaveMode_disableWithoutWarning_expectUpdateIntent() {
+    public void setPowerSaveMode_disableWithoutWarning_expectUpdateIntent() {
         verifyDisablePowerSaveMode(/* needFirstTimeWarning= */ false);
     }
 
     @Test
-    public void testSetPowerSaveMode_disableWithWarning_expectUpdateIntent() {
+    public void setPowerSaveMode_disableWithWarning_expectUpdateIntent() {
         verifyDisablePowerSaveMode(/* needFirstTimeWarning= */ true);
     }
 
     @Test
-    public void testEnsureAutoBatterysaver_setNewPositiveValue_doNotOverwrite() {
+    public void ensureAutoBatterysaver_setNewPositiveValue_doNotOverwrite() {
         Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
 
         BatterySaverUtils.ensureAutoBatterySaver(mMockContext, BATTERY_SAVER_THRESHOLD_1);
@@ -212,7 +216,7 @@
     }
 
     @Test
-    public void testSetAutoBatterySaverTriggerLevel_setSuppressSuggestion() {
+    public void setAutoBatterySaverTriggerLevel_setSuppressSuggestion() {
         Global.putString(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, "null");
         Secure.putString(mMockResolver, Secure.SUPPRESS_AUTO_BATTERY_SAVER_SUGGESTION, "null");
 
@@ -230,7 +234,7 @@
     }
 
     @Test
-    public void testGetBatterySaverScheduleKey_returnExpectedKey() {
+    public void getBatterySaverScheduleKey_returnExpectedKey() {
         Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
         Global.putInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE,
                 PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
@@ -253,8 +257,25 @@
                 KEY_NO_SCHEDULE);
     }
 
+    @EnableFlags(Flags.FLAG_EXTREME_POWER_LOW_STATE_VULNERABILITY)
     @Test
-    public void testSetBatterySaverScheduleMode_setSchedule() {
+    public void setPowerSaveMode_1stTimeAndDeviceLocked_enableBatterySaver() {
+        var keyguardManager = mock(KeyguardManager.class);
+        when(mMockContext.getSystemService(KeyguardManager.class)).thenReturn(keyguardManager);
+        when(keyguardManager.isDeviceLocked()).thenReturn(true);
+        when(mMockPowerManager.setPowerSaveModeEnabled(true)).thenReturn(true);
+
+        var enableResult = BatterySaverUtils.setPowerSaveMode(
+                mMockContext,
+                /* enable= */ true,
+                /* needFirstTimeWarning= */ true,
+                /* reason= */ 0);
+
+        assertThat(enableResult).isTrue();
+    }
+
+    @Test
+    public void setBatterySaverScheduleMode_setSchedule() {
         BatterySaverUtils.setBatterySaverScheduleMode(mMockContext, KEY_NO_SCHEDULE, -1);
 
         assertThat(Global.getInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE, -1))
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 861c405..fd4fc20 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -740,8 +740,6 @@
     // The settings provider must hold its lock when calling here.
     @GuardedBy("mLock")
     public void removeSettingsForPackageLocked(String packageName) {
-        boolean removedSomething = false;
-
         final int settingCount = mSettings.size();
         for (int i = settingCount - 1; i >= 0; i--) {
             String name = mSettings.keyAt(i);
@@ -752,14 +750,9 @@
             }
             Setting setting = mSettings.valueAt(i);
             if (packageName.equals(setting.packageName)) {
-                mSettings.removeAt(i);
-                removedSomething = true;
+                deleteSettingLocked(setting.name);
             }
         }
-
-        if (removedSomething) {
-            scheduleWriteIfNeededLocked();
-        }
     }
 
     // The settings provider must hold its lock when calling here.
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index 4b4ced3..48ce49d 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -988,7 +988,40 @@
     }
 
     @Test
-   public void testGetFlagOverrideToSync() {
+    public void testMemoryUsagePerPackage_StatsUpdatedOnAppDataCleared() {
+        SettingsState settingsState =
+                new SettingsState(
+                        InstrumentationRegistry.getContext(), mLock, mSettingsFile, 1,
+                        SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED, Looper.getMainLooper());
+        final String testKey1 = SETTING_NAME;
+        final String testKey2 = SETTING_NAME + "_2";
+        final String testValue1 = Strings.repeat("A", 9000);
+        final String testValue2 = Strings.repeat("A", 9001);
+        final String packageName = "p";
+        // Inserting the first setting should be okay
+        settingsState.insertSettingLocked(testKey1, testValue1, null, true, packageName);
+        int expectedMemUsageForPackage = (testKey1.length() + testValue1.length()
+                + testValue1.length() /* size for default */) * Character.BYTES;
+        assertEquals(expectedMemUsageForPackage, settingsState.getMemoryUsage(packageName));
+        // Inserting the second setting should fail
+        try {
+            settingsState.insertSettingLocked(testKey2, testValue2, null, true, packageName);
+            fail("Should throw because it exceeded max memory usage per package");
+        } catch (IllegalStateException ex) {
+            assertTrue(ex.getMessage().startsWith("You are adding too many system settings."));
+        }
+        // Now clear app data and check that the memory usage is cleared
+        settingsState.removeSettingsForPackageLocked(packageName);
+        assertEquals(0, settingsState.getMemoryUsage(packageName));
+        // Try inserting the second setting again and it should go through
+        settingsState.insertSettingLocked(testKey2, testValue2, null, true, packageName);
+        expectedMemUsageForPackage = (testKey2.length() + testValue2.length()
+                + testValue2.length() /* size for default */) * Character.BYTES;
+        assertEquals(expectedMemUsageForPackage, settingsState.getMemoryUsage(packageName));
+    }
+
+    @Test
+    public void testGetFlagOverrideToSync() {
         int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
         Object lock = new Object();
         SettingsState settingsState =
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 3f165a3..a1f1a08 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -824,13 +824,6 @@
 }
 
 flag {
-    name: "media_controls_refactor"
-    namespace: "systemui"
-    description: "Refactors media code to follow the recommended architecture"
-    bug: "326408371"
-}
-
-flag {
     name: "qs_tile_focus_state"
     namespace: "systemui"
     description: "enables new focus outline for qs tiles when focused on with physical keyboard"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 97ed74f..b7d6e09 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -658,14 +658,18 @@
                 )
                 .onSizeChanged { setToolbarSize(it) },
     ) {
+        val addWidgetText = stringResource(R.string.hub_mode_add_widget_button_text)
         ToolbarButton(
             isPrimary = !removeEnabled,
-            modifier = Modifier.align(Alignment.CenterStart),
+            modifier =
+                Modifier.align(Alignment.CenterStart).semantics {
+                    contentDescription = addWidgetText
+                },
             onClick = onOpenWidgetPicker,
         ) {
-            Icon(Icons.Default.Add, stringResource(R.string.hub_mode_add_widget_button_text))
+            Icon(Icons.Default.Add, null)
             Text(
-                text = stringResource(R.string.hub_mode_add_widget_button_text),
+                text = addWidgetText,
             )
         }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
index 887e349..25e91be 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.composable
 
+import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.getValue
@@ -48,6 +49,16 @@
     fun SceneScope.Content(
         modifier: Modifier = Modifier,
     ) {
+        val isContentVisible: Boolean by viewModel.isContentVisible.collectAsStateWithLifecycle()
+        if (!isContentVisible) {
+            // If the content isn't supposed to be visible, show a large empty box as it's needed
+            // for scene transition animations (can't just skip rendering everything or shared
+            // elements won't have correct final/initial bounds from animating in and out of the
+            // lockscreen scene).
+            Box(modifier)
+            return
+        }
+
         val coroutineScope = rememberCoroutineScope()
         val blueprintId by viewModel.blueprintId(coroutineScope).collectAsStateWithLifecycle()
         val view = LocalView.current
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java
deleted file mode 100644
index 07d8890..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.ambient.touch;
-
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.DreamManager;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.Flags;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.shade.ShadeViewController;
-import com.android.systemui.shared.system.InputChannelCompat;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Optional;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class ShadeTouchHandlerTest extends SysuiTestCase {
-    @Mock
-    CentralSurfaces mCentralSurfaces;
-
-    @Mock
-    ShadeViewController mShadeViewController;
-
-    @Mock
-    DreamManager mDreamManager;
-
-    @Mock
-    TouchHandler.TouchSession mTouchSession;
-
-    ShadeTouchHandler mTouchHandler;
-
-    @Captor
-    ArgumentCaptor<GestureDetector.OnGestureListener> mGestureListenerCaptor;
-    @Captor
-    ArgumentCaptor<InputChannelCompat.InputEventListener> mInputListenerCaptor;
-
-    private static final int TOUCH_HEIGHT = 20;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-
-        mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), mShadeViewController,
-                mDreamManager, TOUCH_HEIGHT);
-    }
-
-    // Verifies that a swipe down in the gesture region is captured by the shade touch handler.
-    @Test
-    public void testSwipeDown_captured() {
-        final boolean captured = swipe(Direction.DOWN);
-
-        assertThat(captured).isTrue();
-    }
-
-    // Verifies that a swipe in the upward direction is not captured.
-    @Test
-    public void testSwipeUp_notCaptured() {
-        final boolean captured = swipe(Direction.UP);
-
-        // Motion events not captured as the swipe is going in the wrong direction.
-        assertThat(captured).isFalse();
-    }
-
-    // Verifies that a swipe down forwards captured touches to central surfaces for handling.
-    @Test
-    @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
-    public void testSwipeDown_communalEnabled_sentToCentralSurfaces() {
-        swipe(Direction.DOWN);
-
-        // Both motion events are sent for central surfaces to process.
-        verify(mCentralSurfaces, times(2)).handleExternalShadeWindowTouch(any());
-    }
-
-    // Verifies that a swipe down forwards captured touches to the shade view for handling.
-    @Test
-    @DisableFlags(Flags.FLAG_COMMUNAL_HUB)
-    public void testSwipeDown_communalDisabled_sentToShadeView() {
-        swipe(Direction.DOWN);
-
-        // Both motion events are sent for the shade view to process.
-        verify(mShadeViewController, times(2)).handleExternalTouch(any());
-    }
-
-    // Verifies that a swipe down while dreaming forwards captured touches to the shade view for
-    // handling.
-    @Test
-    public void testSwipeDown_dreaming_sentToShadeView() {
-        when(mDreamManager.isDreaming()).thenReturn(true);
-
-        swipe(Direction.DOWN);
-
-        // Both motion events are sent for the shade view to process.
-        verify(mShadeViewController, times(2)).handleExternalTouch(any());
-    }
-
-    // Verifies that a swipe up is not forwarded to central surfaces.
-    @Test
-    @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
-    public void testSwipeUp_communalEnabled_touchesNotSent() {
-        swipe(Direction.UP);
-
-        // Motion events are not sent for central surfaces to process as the swipe is going in the
-        // wrong direction.
-        verify(mCentralSurfaces, never()).handleExternalShadeWindowTouch(any());
-    }
-
-    // Verifies that a swipe up is not forwarded to the shade view.
-    @Test
-    @DisableFlags(Flags.FLAG_COMMUNAL_HUB)
-    public void testSwipeUp_communalDisabled_touchesNotSent() {
-        swipe(Direction.UP);
-
-        // Motion events are not sent for the shade view to process as the swipe is going in the
-        // wrong direction.
-        verify(mShadeViewController, never()).handleExternalTouch(any());
-    }
-
-    /**
-     * Simulates a swipe in the given direction and returns true if the touch was intercepted by the
-     * touch handler's gesture listener.
-     * <p>
-     * Swipe down starts from a Y coordinate of 0 and goes downward. Swipe up starts from the edge
-     * of the gesture region, {@link #TOUCH_HEIGHT}, and goes upward to 0.
-     */
-    private boolean swipe(Direction direction) {
-        Mockito.clearInvocations(mTouchSession);
-        mTouchHandler.onSessionStart(mTouchSession);
-
-        verify(mTouchSession).registerGestureListener(mGestureListenerCaptor.capture());
-        verify(mTouchSession).registerInputListener(mInputListenerCaptor.capture());
-
-        final float startY = direction == Direction.UP ? TOUCH_HEIGHT : 0;
-        final float endY = direction == Direction.UP ? 0 : TOUCH_HEIGHT;
-
-        // Send touches to the input and gesture listener.
-        final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, startY, 0);
-        final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, endY, 0);
-        mInputListenerCaptor.getValue().onInputEvent(event1);
-        mInputListenerCaptor.getValue().onInputEvent(event2);
-        final boolean captured = mGestureListenerCaptor.getValue().onScroll(event1, event2, 0,
-                startY - endY);
-
-        return captured;
-    }
-
-    private enum Direction {
-        DOWN, UP,
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
new file mode 100644
index 0000000..4314676
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.ambient.touch
+
+import android.app.DreamManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.GestureDetector
+import android.view.MotionEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.ambient.touch.TouchHandler.TouchSession
+import com.android.systemui.shade.ShadeViewController
+import com.android.systemui.shared.system.InputChannelCompat
+import com.android.systemui.statusbar.phone.CentralSurfaces
+import com.google.common.truth.Truth
+import java.util.Optional
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShadeTouchHandlerTest : SysuiTestCase() {
+    private var mCentralSurfaces = mock<CentralSurfaces>()
+    private var mShadeViewController = mock<ShadeViewController>()
+    private var mDreamManager = mock<DreamManager>()
+    private var mTouchSession = mock<TouchSession>()
+
+    private lateinit var mTouchHandler: ShadeTouchHandler
+
+    private var mGestureListenerCaptor = argumentCaptor<GestureDetector.OnGestureListener>()
+    private var mInputListenerCaptor = argumentCaptor<InputChannelCompat.InputEventListener>()
+
+    @Before
+    fun setup() {
+        mTouchHandler =
+            ShadeTouchHandler(
+                Optional.of(mCentralSurfaces),
+                mShadeViewController,
+                mDreamManager,
+                TOUCH_HEIGHT
+            )
+    }
+
+    // Verifies that a swipe down in the gesture region is captured by the shade touch handler.
+    @Test
+    fun testSwipeDown_captured() {
+        val captured = swipe(Direction.DOWN)
+        Truth.assertThat(captured).isTrue()
+    }
+
+    // Verifies that a swipe in the upward direction is not captured.
+    @Test
+    fun testSwipeUp_notCaptured() {
+        val captured = swipe(Direction.UP)
+
+        // Motion events not captured as the swipe is going in the wrong direction.
+        Truth.assertThat(captured).isFalse()
+    }
+
+    // Verifies that a swipe down forwards captured touches to central surfaces for handling.
+    @Test
+    @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
+    fun testSwipeDown_communalEnabled_sentToCentralSurfaces() {
+        swipe(Direction.DOWN)
+
+        // Both motion events are sent for central surfaces to process.
+        verify(mCentralSurfaces, times(2)).handleExternalShadeWindowTouch(any())
+    }
+
+    // Verifies that a swipe down forwards captured touches to the shade view for handling.
+    @Test
+    @DisableFlags(Flags.FLAG_COMMUNAL_HUB)
+    fun testSwipeDown_communalDisabled_sentToShadeView() {
+        swipe(Direction.DOWN)
+
+        // Both motion events are sent for the shade view to process.
+        verify(mShadeViewController, times(2)).handleExternalTouch(any())
+    }
+
+    // Verifies that a swipe down while dreaming forwards captured touches to the shade view for
+    // handling.
+    @Test
+    fun testSwipeDown_dreaming_sentToShadeView() {
+        whenever(mDreamManager.isDreaming).thenReturn(true)
+        swipe(Direction.DOWN)
+
+        // Both motion events are sent for the shade view to process.
+        verify(mShadeViewController, times(2)).handleExternalTouch(any())
+    }
+
+    // Verifies that a swipe up is not forwarded to central surfaces.
+    @Test
+    @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
+    fun testSwipeUp_communalEnabled_touchesNotSent() {
+        swipe(Direction.UP)
+
+        // Motion events are not sent for central surfaces to process as the swipe is going in the
+        // wrong direction.
+        verify(mCentralSurfaces, never()).handleExternalShadeWindowTouch(any())
+    }
+
+    // Verifies that a swipe up is not forwarded to the shade view.
+    @Test
+    @DisableFlags(Flags.FLAG_COMMUNAL_HUB)
+    fun testSwipeUp_communalDisabled_touchesNotSent() {
+        swipe(Direction.UP)
+
+        // Motion events are not sent for the shade view to process as the swipe is going in the
+        // wrong direction.
+        verify(mShadeViewController, never()).handleExternalTouch(any())
+    }
+
+    /**
+     * Simulates a swipe in the given direction and returns true if the touch was intercepted by the
+     * touch handler's gesture listener.
+     *
+     * Swipe down starts from a Y coordinate of 0 and goes downward. Swipe up starts from the edge
+     * of the gesture region, [.TOUCH_HEIGHT], and goes upward to 0.
+     */
+    private fun swipe(direction: Direction): Boolean {
+        clearInvocations(mTouchSession)
+        mTouchHandler.onSessionStart(mTouchSession)
+        verify(mTouchSession).registerGestureListener(mGestureListenerCaptor.capture())
+        verify(mTouchSession).registerInputListener(mInputListenerCaptor.capture())
+        val startY = (if (direction == Direction.UP) TOUCH_HEIGHT else 0).toFloat()
+        val endY = (if (direction == Direction.UP) 0 else TOUCH_HEIGHT).toFloat()
+
+        // Send touches to the input and gesture listener.
+        val event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0f, startY, 0)
+        val event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0f, endY, 0)
+        mInputListenerCaptor.lastValue.onInputEvent(event1)
+        mInputListenerCaptor.lastValue.onInputEvent(event2)
+        return mGestureListenerCaptor.lastValue.onScroll(event1, event2, 0f, startY - endY)
+    }
+
+    private enum class Direction {
+        DOWN,
+        UP
+    }
+
+    companion object {
+        private const val TOUCH_HEIGHT = 20
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
index de4b999..875e9e0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.platform.test.flag.junit.FlagsParameterization
@@ -27,10 +29,13 @@
 import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
+import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
 import com.android.systemui.keyguard.shared.model.ClockSize
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.testKosmos
@@ -38,6 +43,8 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import java.util.Locale
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -201,6 +208,44 @@
             }
         }
 
+    @Test
+    fun isContentVisible_whenNotOccluded_visible() =
+        with(kosmos) {
+            testScope.runTest {
+                val isContentVisible by collectLastValue(underTest.isContentVisible)
+
+                keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false, null)
+                runCurrent()
+                assertThat(isContentVisible).isTrue()
+            }
+        }
+
+    @Test
+    fun isContentVisible_whenOccluded_notVisible() =
+        with(kosmos) {
+            testScope.runTest {
+                val isContentVisible by collectLastValue(underTest.isContentVisible)
+
+                keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null)
+                runCurrent()
+                assertThat(isContentVisible).isFalse()
+            }
+        }
+
+    @Test
+    fun isContentVisible_whenOccluded_notVisible_evenIfShadeShown() =
+        with(kosmos) {
+            testScope.runTest {
+                val isContentVisible by collectLastValue(underTest.isContentVisible)
+                keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null)
+                runCurrent()
+
+                sceneInteractor.snapToScene(Scenes.Shade, "")
+                runCurrent()
+                assertThat(isContentVisible).isFalse()
+            }
+        }
+
     private fun prepareConfiguration(): Int {
         val configuration = context.resources.configuration
         configuration.setLayoutDirection(Locale.US)
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index ca5fc12..0bc2c82 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -341,13 +341,17 @@
     <string name="share_to_app_stop_dialog_button">Stop sharing</string>
 
     <!-- Content description for the status bar chip shown to the user when they're casting their screen to a different device [CHAR LIMIT=NONE] -->
-    <string name="cast_to_other_device_chip_accessibility_label">Casting screen</string>
+    <string name="cast_screen_to_other_device_chip_accessibility_label">Casting screen</string>
     <!-- Title for a dialog shown to the user that will let them stop casting their screen to a different device [CHAR LIMIT=50] -->
-    <string name="cast_to_other_device_stop_dialog_title">Stop casting screen?</string>
+    <string name="cast_screen_to_other_device_stop_dialog_title">Stop casting screen?</string>
+    <!-- Title for a dialog shown to the user that will let them stop casting to a different device [CHAR LIMIT=50] -->
+    <string name="cast_to_other_device_stop_dialog_title">Stop casting?</string>
     <!-- Text telling a user that they will stop casting their screen to a different device if they click the "Stop casting" button [CHAR LIMIT=100] -->
-    <string name="cast_to_other_device_stop_dialog_message">You will stop casting your screen</string>
+    <string name="cast_screen_to_other_device_stop_dialog_message">You will stop casting your screen</string>
     <!-- Text telling a user that they will stop casting the contents of the specified [app_name] to a different device if they click the "Stop casting" button. Note that the app name will appear in bold.  [CHAR LIMIT=100] -->
-    <string name="cast_to_other_device_stop_dialog_message_specific_app">You will stop casting &lt;b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g>&lt;/b></string>
+    <string name="cast_screen_to_other_device_stop_dialog_message_specific_app">You will stop casting &lt;b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g>&lt;/b></string>
+    <!-- Text telling a user that they're currently casting to a different device [CHAR LIMIT=100] -->
+    <string name="cast_to_other_device_stop_dialog_message">You\'re currently casting</string>
     <!-- Button to stop screen casting to a different device [CHAR LIMIT=35] -->
     <string name="cast_to_other_device_stop_dialog_button">Stop casting</string>
 
@@ -1231,6 +1235,10 @@
     <string name="accessibility_action_label_remove_widget">remove widget</string>
     <!-- Label for accessibility action to place a widget in edit mode after selecting move widget. [CHAR LIMIT=NONE] -->
     <string name="accessibility_action_label_place_widget">place selected widget</string>
+    <!-- Title in the communal widget picker. [CHAR LIMIT=50] -->
+    <string name="communal_widget_picker_title">Lock screen widgets</string>
+    <!-- Text displayed below the title in the communal widget picker providing additional details about the communal surface. [CHAR LIMIT=80] -->
+    <string name="communal_widget_picker_description">Anyone can view widgets on your lock screen, even if your tablet\'s locked.</string>
     <!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] -->
     <string name="communal_widgets_disclaimer_title">Lock screen widgets</string>
     <!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index d30f33f..a67dcdb 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -515,7 +515,7 @@
     }
 
     @Nullable
-    private ComponentName getAssistInfo() {
+    public ComponentName getAssistInfo() {
         return getAssistInfoForUser(mSelectedUserInteractor.getSelectedUserId());
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 2e92438..4f54fee 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -187,6 +187,11 @@
                 CommunalWidgetCategories.defaultCategories
             )
             putExtra(EXTRA_UI_SURFACE_KEY, EXTRA_UI_SURFACE_VALUE)
+            putExtra(EXTRA_PICKER_TITLE, resources.getString(R.string.communal_widget_picker_title))
+            putExtra(
+                EXTRA_PICKER_DESCRIPTION,
+                resources.getString(R.string.communal_widget_picker_description)
+            )
             putParcelableArrayListExtra(EXTRA_ADDED_APP_WIDGETS_KEY, excludeList)
         }
     }
@@ -214,6 +219,8 @@
 
         private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"
         private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"
+        private const val EXTRA_PICKER_TITLE = "picker_title"
+        private const val EXTRA_PICKER_DESCRIPTION = "picker_description"
         private const val EXTRA_UI_SURFACE_KEY = "ui_surface"
         private const val EXTRA_UI_SURFACE_VALUE = "widgets_hub"
         const val EXTRA_ADDED_APP_WIDGETS_KEY = "added_app_widgets"
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 4b2fb44..572283a 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -74,6 +74,7 @@
 import com.android.systemui.mediaprojection.MediaProjectionModule;
 import com.android.systemui.mediaprojection.appselector.MediaProjectionActivitiesModule;
 import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherModule;
+import com.android.systemui.mediarouter.MediaRouterModule;
 import com.android.systemui.model.SceneContainerPlugin;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.motiontool.MotionToolModule;
@@ -220,6 +221,7 @@
         MediaProjectionActivitiesModule.class,
         MediaProjectionModule.class,
         MediaProjectionTaskSwitcherModule.class,
+        MediaRouterModule.class,
         MotionToolModule.class,
         NotificationIconAreaControllerModule.class,
         PeopleHubModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index 5b9d30e..b703892 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -227,7 +227,7 @@
     get() =
         when (type) {
             ShortcutCategoryType.SYSTEM -> R.string.shortcut_helper_category_system
-            ShortcutCategoryType.MULTI_TASKING -> R.string.shortcut_helper_category_system
+            ShortcutCategoryType.MULTI_TASKING -> R.string.shortcut_helper_category_multitasking
             ShortcutCategoryType.IME -> R.string.shortcut_helper_category_input
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
index 646db40..d0e3ab4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
@@ -34,7 +34,6 @@
 import androidx.lifecycle.lifecycleScope
 import com.android.compose.theme.PlatformTheme
 import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper
-import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
 import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
 import com.android.systemui.res.R
 import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -81,10 +80,7 @@
         requireViewById<ComposeView>(R.id.shortcut_helper_compose_container).apply {
             setContent {
                 PlatformTheme {
-                    val shortcutsUiState by
-                        viewModel.shortcutsUiState.collectAsStateWithLifecycle(
-                            initialValue = ShortcutsUiState.Inactive
-                        )
+                    val shortcutsUiState by viewModel.shortcutsUiState.collectAsStateWithLifecycle()
                     ShortcutHelper(
                         shortcutsUiState = shortcutsUiState,
                         onKeyboardSettingsClicked = ::onKeyboardSettingsClicked,
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
index 3759b0c..e602cad 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
@@ -23,13 +23,17 @@
 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 
 class ShortcutHelperViewModel
 @Inject
 constructor(
+    @Background private val backgroundScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val stateInteractor: ShortcutHelperStateInteractor,
     categoriesInteractor: ShortcutHelperCategoriesInteractor,
@@ -42,16 +46,22 @@
             .flowOn(backgroundDispatcher)
 
     val shortcutsUiState =
-        categoriesInteractor.shortcutCategories.map {
-            if (it.isEmpty()) {
-                ShortcutsUiState.Inactive
-            } else {
-                ShortcutsUiState.Active(
-                    shortcutCategories = it,
-                    defaultSelectedCategory = it.first().type,
-                )
+        categoriesInteractor.shortcutCategories
+            .map {
+                if (it.isEmpty()) {
+                    ShortcutsUiState.Inactive
+                } else {
+                    ShortcutsUiState.Active(
+                        shortcutCategories = it,
+                        defaultSelectedCategory = it.first().type,
+                    )
+                }
             }
-        }
+            .stateIn(
+                scope = backgroundScope,
+                started = SharingStarted.Lazily,
+                initialValue = ShortcutsUiState.Inactive
+            )
 
     fun onViewClosed() {
         stateInteractor.onViewClosed()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index b32d0950..d1a8463 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -103,14 +103,14 @@
  * swiped away via a touch gesture, or when it's flinging expanded/collapsed after a swipe.
  */
 const val LEGACY_UNLOCK_ANIMATION_DURATION_MS = 200L
-const val UNLOCK_ANIMATION_DURATION_MS = 167L
+const val UNLOCK_ANIMATION_DURATION_MS = 300L
 
 /**
  * If there are two different wallpapers on home and lock screen, duration and delay of the lock
  * wallpaper fade out.
  */
-const val LOCK_WALLPAPER_FADE_OUT_DURATION = 140L
-const val LOCK_WALLPAPER_FADE_OUT_START_DELAY = 0L
+const val LOCK_WALLPAPER_FADE_OUT_DURATION = 150L
+const val LOCK_WALLPAPER_FADE_OUT_START_DELAY = 150L
 
 /**
  * How long the in-window launcher icon animation takes. This is used if the launcher is underneath
@@ -167,7 +167,7 @@
         private val statusBarStateController: SysuiStatusBarStateController,
         private val notificationShadeWindowController: NotificationShadeWindowController,
         private val powerManager: PowerManager,
-        private val wallpaperManager: WallpaperManager
+        private val wallpaperManager: WallpaperManager,
 ) : KeyguardStateController.Callback, ISysuiUnlockAnimationController.Stub() {
 
     interface KeyguardUnlockAnimationListener {
@@ -380,7 +380,6 @@
                     else LAUNCHER_ICONS_ANIMATION_DURATION_MS
             interpolator = if (fasterUnlockTransition()) Interpolators.LINEAR
                     else Interpolators.ALPHA_OUT
-            if (fasterUnlockTransition()) startDelay = CANNED_UNLOCK_START_DELAY
             addUpdateListener { valueAnimator: ValueAnimator ->
                 setWallpaperAppearAmount(
                         valueAnimator.animatedValue as Float, openingWallpaperTargets)
@@ -647,14 +646,12 @@
         val isWakeAndUnlockNotFromDream = biometricUnlockControllerLazy.get().isWakeAndUnlock &&
             biometricUnlockControllerLazy.get().mode != MODE_WAKE_AND_UNLOCK_FROM_DREAM
 
-        val duration = if (fasterUnlockTransition()) UNLOCK_ANIMATION_DURATION_MS
-                else LAUNCHER_ICONS_ANIMATION_DURATION_MS
         listeners.forEach {
             it.onUnlockAnimationStarted(
                 playingCannedUnlockAnimation /* playingCannedAnimation */,
                 isWakeAndUnlockNotFromDream /* isWakeAndUnlockNotFromDream */,
                 cannedUnlockStartDelayMs() /* unlockStartDelay */,
-                duration /* unlockAnimationDuration */) }
+                LAUNCHER_ICONS_ANIMATION_DURATION_MS /* unlockAnimationDuration */) }
 
         // Finish the keyguard remote animation if the dismiss amount has crossed the threshold.
         // Check it here in case there is no more change to the dismiss amount after the last change
@@ -746,6 +743,10 @@
         // As soon as the shade starts animating out of the way, start the canned unlock animation,
         // which will finish keyguard exit when it completes. The in-window animations in the
         // Launcher window will end on their own.
+        if (fasterUnlockTransition() && openingWallpaperTargets?.isNotEmpty() == true) {
+            fadeOutWallpaper()
+        }
+
         handler.postDelayed({
             if (keyguardViewMediator.get().isShowingAndNotOccluded &&
                 !keyguardStateController.isKeyguardGoingAway) {
@@ -754,9 +755,8 @@
                 return@postDelayed
             }
 
-            if ((openingWallpaperTargets?.isNotEmpty() == true)) {
+            if (openingWallpaperTargets?.isNotEmpty() == true) {
                 fadeInWallpaper()
-                if (fasterUnlockTransition()) fadeOutWallpaper()
                 hideKeyguardViewAfterRemoteAnimation()
             } else {
                 keyguardViewMediator.get().exitKeyguardAndFinishSurfaceBehindRemoteAnimation(
@@ -1038,6 +1038,7 @@
         surfaceBehindAlphaAnimator.cancel()
         surfaceBehindEntryAnimator.cancel()
         wallpaperCannedUnlockAnimator.cancel()
+        if (fasterUnlockTransition()) wallpaperFadeOutUnlockAnimator.cancel()
 
         // That target is no longer valid since the animation finished, null it out.
         surfaceBehindRemoteAnimationTargets = null
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 1de0abe..8a29f96 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.model.ClockSize
 import com.android.systemui.res.R
+import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
@@ -48,6 +49,7 @@
     val shadeInteractor: ShadeInteractor,
     @Application private val applicationScope: CoroutineScope,
     unfoldTransitionInteractor: UnfoldTransitionInteractor,
+    occlusionInteractor: SceneContainerOcclusionInteractor,
 ) {
     @VisibleForTesting val clockSize = clockInteractor.clockSize
 
@@ -93,6 +95,16 @@
                 initialValue = UnfoldTranslations(),
             )
 
+    /** Whether the content of the scene UI should be shown. */
+    val isContentVisible: StateFlow<Boolean> =
+        occlusionInteractor.isOccludingActivityShown
+            .map { !it }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = true,
+            )
+
     fun getSmartSpacePaddingTop(resources: Resources): Int {
         return if (clockSize.value == ClockSize.LARGE) {
             resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) +
diff --git a/packages/SystemUI/src/com/android/systemui/mediarouter/MediaRouterModule.kt b/packages/SystemUI/src/com/android/systemui/mediarouter/MediaRouterModule.kt
new file mode 100644
index 0000000..c07e3a0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediarouter/MediaRouterModule.kt
@@ -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.systemui.mediarouter
+
+import com.android.systemui.mediarouter.data.repository.MediaRouterRepository
+import com.android.systemui.mediarouter.data.repository.MediaRouterRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface MediaRouterModule {
+    @Binds fun mediaRouterRepository(impl: MediaRouterRepositoryImpl): MediaRouterRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepository.kt b/packages/SystemUI/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepository.kt
new file mode 100644
index 0000000..998d76c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepository.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.mediarouter.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.policy.CastController
+import com.android.systemui.statusbar.policy.CastDevice
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+/** A repository for data coming from MediaRouter APIs. */
+interface MediaRouterRepository {
+    /** A list of the cast devices that MediaRouter is currently aware of. */
+    val castDevices: StateFlow<List<CastDevice>>
+
+    /** Stops the cast to the given device. */
+    fun stopCasting(device: CastDevice)
+}
+
+@SysUISingleton
+class MediaRouterRepositoryImpl
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val castController: CastController,
+) : MediaRouterRepository {
+    override val castDevices: StateFlow<List<CastDevice>> =
+        conflatedCallbackFlow {
+                val callback =
+                    CastController.Callback {
+                        val mediaRouterCastDevices =
+                            castController.castDevices.filter {
+                                it.origin == CastDevice.CastOrigin.MediaRouter
+                            }
+                        trySend(mediaRouterCastDevices)
+                    }
+                castController.addCallback(callback)
+                awaitClose { castController.removeCallback(callback) }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
+
+    override fun stopCasting(device: CastDevice) {
+        castController.stopCasting(device)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 4e2e227..b3624ad 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -115,7 +115,7 @@
 import com.android.systemui.statusbar.phone.StatusBarWindowCallback;
 import com.android.systemui.statusbar.policy.CallbackController;
 import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder;
-import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.sysui.ShellInterface;
 
 import dagger.Lazy;
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
index 4a4c73b..98a61df 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
@@ -74,7 +74,7 @@
         when (intent?.action) {
             ACTION_START -> {
                 bgExecutor.execute {
-                    traceurMessageSender.startTracing(issueRecordingState.traceType)
+                    traceurMessageSender.startTracing(issueRecordingState.traceConfig)
                 }
                 issueRecordingState.isRecording = true
                 if (!issueRecordingState.recordScreen) {
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
index b077349..7612900 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
@@ -22,7 +22,8 @@
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
-import com.android.traceur.TraceUtils.PresetTraceType
+import com.android.traceur.PresetTraceConfigs
+import com.android.traceur.TraceConfig
 import java.util.concurrent.CopyOnWriteArrayList
 import javax.inject.Inject
 
@@ -53,8 +54,8 @@
         get() = prefs.getInt(KEY_ISSUE_TYPE_RES, ISSUE_TYPE_NOT_SET)
         set(value) = prefs.edit().putInt(KEY_ISSUE_TYPE_RES, value).apply()
 
-    val traceType: PresetTraceType
-        get() = ALL_ISSUE_TYPES[issueTypeRes] ?: PresetTraceType.UNSET
+    val traceConfig: TraceConfig
+        get() = ALL_ISSUE_TYPES[issueTypeRes] ?: PresetTraceConfigs.getDefaultConfig()
 
     private val listeners = CopyOnWriteArrayList<Runnable>()
 
@@ -83,12 +84,12 @@
         const val KEY_ISSUE_TYPE_RES = "key_issueTypeRes"
         const val ISSUE_TYPE_NOT_SET = -1
 
-        val ALL_ISSUE_TYPES: Map<Int, PresetTraceType> =
+        val ALL_ISSUE_TYPES: Map<Int, TraceConfig?> =
             hashMapOf(
-                Pair(R.string.performance, PresetTraceType.PERFORMANCE),
-                Pair(R.string.user_interface, PresetTraceType.UI),
-                Pair(R.string.battery, PresetTraceType.BATTERY),
-                Pair(R.string.thermal, PresetTraceType.THERMAL)
+                Pair(R.string.performance, PresetTraceConfigs.getPerformanceConfig()),
+                Pair(R.string.user_interface, PresetTraceConfigs.getUiConfig()),
+                Pair(R.string.battery, PresetTraceConfigs.getBatteryConfig()),
+                Pair(R.string.thermal, PresetTraceConfigs.getThermalConfig()),
             )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt
index 51744aa..903d662 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt
@@ -35,7 +35,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.traceur.FileSender
 import com.android.traceur.MessageConstants
-import com.android.traceur.TraceUtils.PresetTraceType
+import com.android.traceur.TraceConfig
 import javax.inject.Inject
 
 private const val TAG = "TraceurMessageSender"
@@ -93,9 +93,9 @@
     }
 
     @WorkerThread
-    fun startTracing(traceType: PresetTraceType) {
+    fun startTracing(traceType: TraceConfig) {
         val data =
-            Bundle().apply { putSerializable(MessageConstants.INTENT_EXTRA_TRACE_TYPE, traceType) }
+            Bundle().apply { putParcelable(MessageConstants.INTENT_EXTRA_TRACE_TYPE, traceType) }
         notifyTraceur(MessageConstants.START_WHAT, data)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
index 233e9b5..2d510e1 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
@@ -43,8 +43,14 @@
     sceneInteractor: SceneInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
 ) {
-    /** Whether a show-when-locked activity is at the top of the current activity stack. */
-    private val isOccludingActivityShown: StateFlow<Boolean> =
+    /**
+     * Whether a show-when-locked activity is at the top of the current activity stack.
+     *
+     * Note: this isn't enough to figure out whether the scene container UI should be invisible as
+     * that also depends on the things like the state of AOD and the current scene. If the code
+     * needs that, [invisibleDueToOcclusion] should be collected instead.
+     */
+    val isOccludingActivityShown: StateFlow<Boolean> =
         keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop.stateIn(
             scope = applicationScope,
             started = SharingStarted.WhileSubscribed(),
@@ -69,6 +75,9 @@
     /**
      * Whether the scene container should become invisible due to "occlusion" by an in-foreground
      * "show when locked" activity.
+     *
+     * Note: this returns `false` when an overlaid scene (like shade or QS) is shown above the
+     * occluding activity.
      */
     val invisibleDueToOcclusion: StateFlow<Boolean> =
         combine(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt
new file mode 100644
index 0000000..21f301c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt
@@ -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.systemui.statusbar.chips.casttootherdevice.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.mediarouter.data.repository.MediaRouterRepository
+import com.android.systemui.statusbar.chips.casttootherdevice.domain.model.MediaRouterCastModel
+import com.android.systemui.statusbar.policy.CastDevice
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Interactor for MediaRouter events, used to show the cast-audio-to-other-device chip in the status
+ * bar.
+ */
+@SysUISingleton
+class MediaRouterChipInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val mediaRouterRepository: MediaRouterRepository,
+) {
+    private val activeCastDevice: StateFlow<CastDevice?> =
+        mediaRouterRepository.castDevices
+            .map { allDevices -> allDevices.firstOrNull { it.isCasting } }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+    /** The current casting state, according to MediaRouter APIs. */
+    val mediaRouterCastingState: StateFlow<MediaRouterCastModel> =
+        activeCastDevice
+            .map {
+                if (it != null) {
+                    MediaRouterCastModel.Casting
+                } else {
+                    MediaRouterCastModel.DoingNothing
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), MediaRouterCastModel.DoingNothing)
+
+    /** Stops the currently active MediaRouter cast. */
+    fun stopCasting() {
+        activeCastDevice.value?.let { mediaRouterRepository.stopCasting(it) }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/model/MediaRouterCastModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/model/MediaRouterCastModel.kt
new file mode 100644
index 0000000..b228922
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/model/MediaRouterCastModel.kt
@@ -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.systemui.statusbar.chips.casttootherdevice.domain.model
+
+/** Represents the current casting state, according to MediaRouter APIs. */
+sealed interface MediaRouterCastModel {
+    /** MediaRouter isn't aware of any active cast. */
+    data object DoingNothing : MediaRouterCastModel
+
+    /** MediaRouter has an active cast. */
+    data object Casting : MediaRouterCastModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastToOtherDeviceDialogDelegate.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
index bc0f492..ffb20a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastToOtherDeviceDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
@@ -24,7 +24,7 @@
 import com.android.systemui.statusbar.phone.SystemUIDialog
 
 /** A dialog that lets the user stop an ongoing cast-screen-to-other-device event. */
-class EndCastToOtherDeviceDialogDelegate(
+class EndCastScreenToOtherDeviceDialogDelegate(
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
     private val stopAction: () -> Unit,
     private val state: ProjectionChipModel.Projecting,
@@ -36,13 +36,14 @@
     override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
         with(dialog) {
             setIcon(CAST_TO_OTHER_DEVICE_ICON)
-            setTitle(R.string.cast_to_other_device_stop_dialog_title)
+            setTitle(R.string.cast_screen_to_other_device_stop_dialog_title)
+            // TODO(b/332662551): Include device name in this string.
             setMessage(
                 endMediaProjectionDialogHelper.getDialogMessage(
                     state.projectionState,
-                    genericMessageResId = R.string.cast_to_other_device_stop_dialog_message,
+                    genericMessageResId = R.string.cast_screen_to_other_device_stop_dialog_message,
                     specificAppMessageResId =
-                        R.string.cast_to_other_device_stop_dialog_message_specific_app,
+                        R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
                 )
             )
             // No custom on-click, because the dialog will automatically be dismissed when the
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
similarity index 74%
copy from packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastToOtherDeviceDialogDelegate.kt
copy to packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
index bc0f492..afe67b4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastToOtherDeviceDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
@@ -19,15 +19,17 @@
 import android.os.Bundle
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel.Companion.CAST_TO_OTHER_DEVICE_ICON
-import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel
 import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
 import com.android.systemui.statusbar.phone.SystemUIDialog
 
-/** A dialog that lets the user stop an ongoing cast-screen-to-other-device event. */
-class EndCastToOtherDeviceDialogDelegate(
+/**
+ * A dialog that lets the user stop an ongoing cast-to-other-device event. The user could be casting
+ * their screen, or just casting their audio. This dialog uses generic strings to handle both cases
+ * well.
+ */
+class EndGenericCastToOtherDeviceDialogDelegate(
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
     private val stopAction: () -> Unit,
-    private val state: ProjectionChipModel.Projecting,
 ) : SystemUIDialog.Delegate {
     override fun createDialog(): SystemUIDialog {
         return endMediaProjectionDialogHelper.createDialog(this)
@@ -37,14 +39,8 @@
         with(dialog) {
             setIcon(CAST_TO_OTHER_DEVICE_ICON)
             setTitle(R.string.cast_to_other_device_stop_dialog_title)
-            setMessage(
-                endMediaProjectionDialogHelper.getDialogMessage(
-                    state.projectionState,
-                    genericMessageResId = R.string.cast_to_other_device_stop_dialog_message,
-                    specificAppMessageResId =
-                        R.string.cast_to_other_device_stop_dialog_message_specific_app,
-                )
-            )
+            // TODO(b/332662551): Include device name in this string.
+            setMessage(R.string.cast_to_other_device_stop_dialog_message)
             // No custom on-click, because the dialog will automatically be dismissed when the
             // button is clicked anyway.
             setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
index 73ccaab..2eff336 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
@@ -23,7 +23,10 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.res.R
-import com.android.systemui.statusbar.chips.casttootherdevice.ui.view.EndCastToOtherDeviceDialogDelegate
+import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor.MediaRouterChipInteractor
+import com.android.systemui.statusbar.chips.casttootherdevice.domain.model.MediaRouterCastModel
+import com.android.systemui.statusbar.chips.casttootherdevice.ui.view.EndCastScreenToOtherDeviceDialogDelegate
+import com.android.systemui.statusbar.chips.casttootherdevice.ui.view.EndGenericCastToOtherDeviceDialogDelegate
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
 import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel
 import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
@@ -36,12 +39,14 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
 /**
- * View model for the cast-to-other-device chip, shown when sharing your phone screen content to a
- * different device. (Triggered from the Quick Settings Cast tile or from the Settings app.)
+ * View model for the cast-to-other-device chip, shown when a user is sharing content to a different
+ * device. (Triggered from the Quick Settings Cast tile or from the Settings app.) The content could
+ * either be the user's screen, or just the user's audio.
  */
 @SysUISingleton
 class CastToOtherDeviceChipViewModel
@@ -49,14 +54,19 @@
 constructor(
     @Application private val scope: CoroutineScope,
     private val mediaProjectionChipInteractor: MediaProjectionChipInteractor,
+    private val mediaRouterChipInteractor: MediaRouterChipInteractor,
     private val systemClock: SystemClock,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
 ) : OngoingActivityChipViewModel {
-    override val chip: StateFlow<OngoingActivityChipModel> =
-        // TODO(b/342169876): The MediaProjection APIs are not invoked for certain
-        // cast-to-other-device events, like audio-only casting. We should also listen to
-        // MediaRouter APIs to cover all cast events.
+    /**
+     * The cast chip to show, based only on MediaProjection API events.
+     *
+     * This chip will only be [OngoingActivityChipModel.Shown] when the user is casting their
+     * *screen*. If the user is only casting audio, this chip will be
+     * [OngoingActivityChipModel.Hidden].
+     */
+    private val projectionChip: StateFlow<OngoingActivityChipModel> =
         mediaProjectionChipInteractor.projection
             .map { projectionModel ->
                 when (projectionModel) {
@@ -65,7 +75,7 @@
                         if (projectionModel.type != ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE) {
                             OngoingActivityChipModel.Hidden
                         } else {
-                            createCastToOtherDeviceChip(projectionModel)
+                            createCastScreenToOtherDeviceChip(projectionModel)
                         }
                     }
                 }
@@ -73,43 +83,132 @@
             // See b/347726238.
             .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
 
+    /**
+     * The cast chip to show, based only on MediaRouter API events.
+     *
+     * This chip will be [OngoingActivityChipModel.Shown] when the user is casting their screen *or*
+     * their audio.
+     *
+     * The MediaProjection APIs are not invoked for casting *only audio* to another device because
+     * MediaProjection is only concerned with *screen* sharing (see b/342169876). We listen to
+     * MediaRouter APIs here to cover audio-only casting.
+     *
+     * Note that this means we will start showing the cast chip before the casting actually starts,
+     * for **both** audio-only casting and screen casting. MediaRouter is aware of all
+     * cast-to-other-device events, and MediaRouter immediately marks a device as "connecting" once
+     * a user selects what device they'd like to cast to, even if they haven't hit "Start casting"
+     * yet. All of SysUI considers "connecting" devices to be casting (see
+     * [com.android.systemui.statusbar.policy.CastDevice.isCasting]), so the chip will follow the
+     * same convention and start showing once a device is selected. See b/269975671.
+     */
+    private val routerChip =
+        mediaRouterChipInteractor.mediaRouterCastingState
+            .map { routerModel ->
+                when (routerModel) {
+                    is MediaRouterCastModel.DoingNothing -> OngoingActivityChipModel.Hidden
+                    is MediaRouterCastModel.Casting -> {
+                        // A consequence of b/269975671 is that MediaRouter will mark a device as
+                        // casting before casting has actually started. To alleviate this bug a bit,
+                        // we won't show a timer for MediaRouter events. That way, we won't show a
+                        // timer if cast hasn't actually started.
+                        //
+                        // This does mean that the audio-only casting chip will *never* show a
+                        // timer, because audio-only casting never activates the MediaProjection
+                        // APIs and those are the only cast APIs that show a timer.
+                        createIconOnlyCastChip()
+                    }
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+
+    override val chip: StateFlow<OngoingActivityChipModel> =
+        combine(projectionChip, routerChip) { projection, router ->
+                // A consequence of b/269975671 is that MediaRouter and MediaProjection APIs fire at
+                // different times when *screen* casting:
+                //
+                // 1. When the user chooses what device to cast to, the MediaRouter APIs mark the
+                // device as casting (even though casting hasn't actually started yet). At this
+                // point, `routerChip` is [OngoingActivityChipModel.Shown] but `projectionChip` is
+                // [OngoingActivityChipModel.Hidden], and we'll show the router chip.
+                //
+                // 2. Once casting has actually started, the MediaProjection APIs become aware of
+                // the device. At this point, both `routerChip` and `projectionChip` are
+                // [OngoingActivityChipModel.Shown].
+                //
+                // Because the MediaProjection APIs have activated, we know that the user is screen
+                // casting (not audio casting). We need to switch to using `projectionChip` because
+                // that chip will show information specific to screen casting. The `projectionChip`
+                // will also show a timer, as opposed to `routerChip`'s icon-only display.
+                if (projection is OngoingActivityChipModel.Shown) {
+                    projection
+                } else {
+                    router
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+
     /** Stops the currently active projection. */
     private fun stopProjecting() {
         mediaProjectionChipInteractor.stopProjecting()
     }
 
-    private fun createCastToOtherDeviceChip(
+    private fun stopMediaRouterCasting() {
+        mediaRouterChipInteractor.stopCasting()
+    }
+
+    private fun createCastScreenToOtherDeviceChip(
         state: ProjectionChipModel.Projecting,
     ): OngoingActivityChipModel.Shown {
         return OngoingActivityChipModel.Shown.Timer(
             icon =
                 Icon.Resource(
                     CAST_TO_OTHER_DEVICE_ICON,
-                    // Note: This string is "Casting screen", which is okay right now because this
-                    // chip does not currently support audio-only casting. If the chip starts
-                    // supporting audio-only casting (see b/342169876), update the content
-                    // description to just "Casting".
+                    // This string is "Casting screen"
                     ContentDescription.Resource(
-                        R.string.cast_to_other_device_chip_accessibility_label,
+                        R.string.cast_screen_to_other_device_chip_accessibility_label,
                     ),
                 ),
             colors = ColorsModel.Red,
             // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
             startTimeMs = systemClock.elapsedRealtime(),
             createDialogLaunchOnClickListener(
-                createCastToOtherDeviceDialogDelegate(state),
+                createCastScreenToOtherDeviceDialogDelegate(state),
                 dialogTransitionAnimator,
             ),
         )
     }
 
-    private fun createCastToOtherDeviceDialogDelegate(state: ProjectionChipModel.Projecting) =
-        EndCastToOtherDeviceDialogDelegate(
+    private fun createIconOnlyCastChip(): OngoingActivityChipModel.Shown {
+        return OngoingActivityChipModel.Shown.IconOnly(
+            icon =
+                Icon.Resource(
+                    CAST_TO_OTHER_DEVICE_ICON,
+                    // This string is just "Casting"
+                    ContentDescription.Resource(R.string.accessibility_casting),
+                ),
+            colors = ColorsModel.Red,
+            createDialogLaunchOnClickListener(
+                createGenericCastToOtherDeviceDialogDelegate(),
+                dialogTransitionAnimator,
+            ),
+        )
+    }
+
+    private fun createCastScreenToOtherDeviceDialogDelegate(
+        state: ProjectionChipModel.Projecting,
+    ) =
+        EndCastScreenToOtherDeviceDialogDelegate(
             endMediaProjectionDialogHelper,
             stopAction = this::stopProjecting,
             state,
         )
 
+    private fun createGenericCastToOtherDeviceDialogDelegate() =
+        EndGenericCastToOtherDeviceDialogDelegate(
+            endMediaProjectionDialogHelper,
+            stopAction = this::stopMediaRouterCasting,
+        )
+
     companion object {
         @DrawableRes val CAST_TO_OTHER_DEVICE_ICON = R.drawable.ic_cast_connected
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
index 5a1146d..1434dc0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
 package com.android.systemui.statusbar.notification.collection.coordinator
 
 import android.app.NotificationManager
@@ -27,9 +25,10 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
-import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.expansionChanges
 import com.android.systemui.statusbar.notification.collection.GroupEntry
@@ -58,7 +57,6 @@
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.delay
@@ -89,7 +87,7 @@
     private val headsUpManager: HeadsUpManager,
     private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
     private val keyguardRepository: KeyguardRepository,
-    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val logger: KeyguardCoordinatorLogger,
     @Application private val scope: CoroutineScope,
     private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider,
@@ -126,8 +124,9 @@
     private suspend fun trackSeenNotifications() {
         // Whether or not keyguard is visible (or occluded).
         val isKeyguardPresent: Flow<Boolean> =
-            keyguardTransitionRepository.transitions
-                .map { step -> step.to != KeyguardState.GONE }
+            keyguardTransitionInteractor
+                .transitionValue(Scenes.Gone, stateWithoutSceneContainer = KeyguardState.GONE)
+                .map { it == 0f }
                 .distinctUntilChanged()
                 .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 534d9d2..900201f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -186,6 +186,7 @@
         interactor.configurationBasedDimensions
             .map {
                 when {
+                    !it.useSplitShade -> 0
                     it.useLargeScreenHeader -> it.marginTopLargeScreen
                     else -> it.marginTop
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index 6012ecd..775f34d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -250,6 +250,7 @@
             mLevel = (int) (100f
                     * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)
                     / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100));
+            int previousPluggedChargingSource = mPluggedChargingSource;
             mPluggedChargingSource = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
             mPluggedIn = mPluggedChargingSource != 0;
             final int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS,
@@ -276,7 +277,9 @@
                 mIsBatteryDefender = isBatteryDefender;
                 fireIsBatteryDefenderChanged();
             }
-
+            if (mPluggedChargingSource != previousPluggedChargingSource) {
+                updatePowerSave();
+            }
             fireBatteryLevelChanged();
         } else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)) {
             updatePowerSave();
diff --git a/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java b/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
index 9c8a481..501fee6 100644
--- a/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
@@ -20,15 +20,15 @@
 import com.android.systemui.util.RingerModeTrackerImpl;
 import com.android.systemui.util.animation.data.repository.AnimationStatusRepository;
 import com.android.systemui.util.animation.data.repository.AnimationStatusRepositoryImpl;
+import com.android.systemui.util.icons.AppCategoryIconProvider;
+import com.android.systemui.util.icons.AppCategoryIconProviderImpl;
 import com.android.systemui.util.wrapper.UtilWrapperModule;
 
 import dagger.Binds;
 import dagger.Module;
 
 /** Dagger Module for code in the util package. */
-@Module(includes = {
-                UtilWrapperModule.class
-        })
+@Module(includes = {UtilWrapperModule.class})
 public interface UtilModule {
     /** */
     @Binds
@@ -37,4 +37,8 @@
     @Binds
     AnimationStatusRepository provideAnimationStatus(
             AnimationStatusRepositoryImpl ringerModeTrackerImpl);
+
+    /** */
+    @Binds
+    AppCategoryIconProvider appCategoryIconProvider(AppCategoryIconProviderImpl impl);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/icons/AppCategoryIconProvider.kt b/packages/SystemUI/src/com/android/systemui/util/icons/AppCategoryIconProvider.kt
new file mode 100644
index 0000000..6e3f8f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/icons/AppCategoryIconProvider.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.icons
+
+import android.content.Intent
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Icon
+import android.os.RemoteException
+import android.util.Log
+import com.android.systemui.assist.AssistManager
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.shared.system.PackageManagerWrapper
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+interface AppCategoryIconProvider {
+    /** Returns the [Icon] of the default assistant app, if it exists. */
+    suspend fun assistantAppIcon(): Icon?
+
+    /**
+     * Returns the [Icon] of the default app of [category], if it exists. Category can be for
+     * example [Intent.CATEGORY_APP_EMAIL] or [Intent.CATEGORY_APP_CALCULATOR].
+     */
+    suspend fun categoryAppIcon(category: String): Icon?
+}
+
+class AppCategoryIconProviderImpl
+@Inject
+constructor(
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val assistManager: AssistManager,
+    private val packageManager: PackageManager,
+    private val packageManagerWrapper: PackageManagerWrapper,
+) : AppCategoryIconProvider {
+
+    override suspend fun assistantAppIcon(): Icon? {
+        val assistInfo = assistManager.assistInfo ?: return null
+        return getPackageIcon(assistInfo.packageName)
+    }
+
+    override suspend fun categoryAppIcon(category: String): Icon? {
+        val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(category) }
+        val packageInfo = getPackageInfo(intent) ?: return null
+        return getPackageIcon(packageInfo.packageName)
+    }
+
+    private suspend fun getPackageInfo(intent: Intent): PackageInfo? =
+        withContext(backgroundDispatcher) {
+            val packageName =
+                packageManagerWrapper
+                    .resolveActivity(/* intent= */ intent, /* flags= */ 0)
+                    ?.activityInfo
+                    ?.packageName ?: return@withContext null
+            return@withContext getPackageInfo(packageName)
+        }
+
+    private suspend fun getPackageIcon(packageName: String): Icon? {
+        val appInfo = getPackageInfo(packageName)?.applicationInfo ?: return null
+        return if (appInfo.icon != 0) {
+            Icon.createWithResource(appInfo.packageName, appInfo.icon)
+        } else {
+            null
+        }
+    }
+
+    private suspend fun getPackageInfo(packageName: String): PackageInfo? =
+        withContext(backgroundDispatcher) {
+            try {
+                return@withContext packageManager.getPackageInfo(packageName, /* flags= */ 0)
+            } catch (e: RemoteException) {
+                Log.e(TAG, "Failed to retrieve package info for $packageName")
+                return@withContext null
+            }
+        }
+
+    companion object {
+        private const val TAG = "DefaultAppsIconProvider"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
index 160ae86..fe54044 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
@@ -19,6 +19,7 @@
 import android.database.ContentObserver
 import android.net.Uri
 import android.provider.Settings.SettingNotFoundException
+import androidx.annotation.WorkerThread
 import com.android.app.tracing.TraceUtils.trace
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -59,10 +60,13 @@
     fun getUriFor(name: String): Uri
 
     /**
-     * Convenience wrapper around [ContentResolver.registerContentObserver].'
-     *
+     * Registers listener for a given content observer <b>while blocking the current thread</b>.
      * Implicitly calls [getUriFor] on the passed in name.
+     *
+     * This should not be called from the main thread, use [registerContentObserver] or
+     * [registerContentObserverAsync] instead.
      */
+    @WorkerThread
     fun registerContentObserverSync(name: String, settingsObserver: ContentObserver) {
         registerContentObserverSync(getUriFor(name), settingsObserver)
     }
@@ -90,7 +94,13 @@
             registerContentObserverSync(getUriFor(name), settingsObserver)
         }
 
-    /** Convenience wrapper around [ContentResolver.registerContentObserver].' */
+    /**
+     * Registers listener for a given content observer <b>while blocking the current thread</b>.
+     *
+     * This should not be called from the main thread, use [registerContentObserver] or
+     * [registerContentObserverAsync] instead.
+     */
+    @WorkerThread
     fun registerContentObserverSync(uri: Uri, settingsObserver: ContentObserver) =
         registerContentObserverSync(uri, false, settingsObserver)
 
@@ -157,7 +167,13 @@
             registerContentObserverSync(getUriFor(name), notifyForDescendants, settingsObserver)
         }
 
-    /** Convenience wrapper around [ContentResolver.registerContentObserver].' */
+    /**
+     * Registers listener for a given content observer <b>while blocking the current thread</b>.
+     *
+     * This should not be called from the main thread, use [registerContentObserver] or
+     * [registerContentObserverAsync] instead.
+     */
+    @WorkerThread
     fun registerContentObserverSync(
         uri: Uri,
         notifyForDescendants: Boolean,
@@ -200,7 +216,13 @@
             registerContentObserverSync(uri, notifyForDescendants, settingsObserver)
         }
 
-    /** See [ContentResolver.unregisterContentObserver]. */
+    /**
+     * Unregisters the given content observer <b>while blocking the current thread</b>.
+     *
+     * This should not be called from the main thread, use [unregisterContentObserver] or
+     * [unregisterContentObserverAsync] instead.
+     */
+    @WorkerThread
     fun unregisterContentObserverSync(settingsObserver: ContentObserver) {
         trace({ "SP#unregisterObserver" }) {
             getContentResolver().unregisterContentObserver(settingsObserver)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerCollector.kt b/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerCollector.kt
new file mode 100644
index 0000000..6859191
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeControllerCollector.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume
+
+import android.media.IVolumeController
+import com.android.settingslib.media.data.repository.VolumeControllerEvent
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+
+/**
+ * This class is a bridge between
+ * [com.android.settingslib.volume.data.repository.AudioRepository.volumeControllerEvents] and the
+ * old code that uses [IVolumeController] interface directly.
+ */
+class VolumeControllerCollector
+@Inject
+constructor(@Application private val coroutineScope: CoroutineScope) {
+
+    /** Collects [Flow] of [VolumeControllerEvent] into [IVolumeController]. */
+    fun collectToController(
+        eventsFlow: Flow<VolumeControllerEvent>,
+        controller: IVolumeController
+    ) =
+        coroutineScope.launch {
+            eventsFlow.collect { event ->
+                when (event) {
+                    is VolumeControllerEvent.VolumeChanged ->
+                        controller.volumeChanged(event.streamType, event.flags)
+                    VolumeControllerEvent.Dismiss -> controller.dismiss()
+                    is VolumeControllerEvent.DisplayCsdWarning ->
+                        controller.displayCsdWarning(event.csdWarning, event.displayDurationMs)
+                    is VolumeControllerEvent.DisplaySafeVolumeWarning ->
+                        controller.displaySafeVolumeWarning(event.flags)
+                    is VolumeControllerEvent.MasterMuteChanged ->
+                        controller.masterMuteChanged(event.flags)
+                    is VolumeControllerEvent.SetA11yMode -> controller.setA11yMode(event.mode)
+                    is VolumeControllerEvent.SetLayoutDirection ->
+                        controller.setLayoutDirection(event.layoutDirection)
+                }
+            }
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 97f5efc..677d1fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -1799,8 +1799,10 @@
             when {
                 fingerprint != null && face != null -> "coex"
                 fingerprint != null && fingerprint.isAnySidefpsType -> "fingerprint only, sideFps"
-                fingerprint != null && !fingerprint.isAnySidefpsType ->
-                    "fingerprint only, non-sideFps"
+                fingerprint != null && fingerprint.isAnyUdfpsType -> "fingerprint only, udfps"
+                fingerprint != null &&
+                    fingerprint.sensorType == FingerprintSensorProperties.TYPE_REAR ->
+                    "fingerprint only, rearFps"
                 face != null -> "face only"
                 else -> "?"
             }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt
new file mode 100644
index 0000000..84ec1a5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt
@@ -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.
+ */
+
+package com.android.systemui.mediarouter.data.repository
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.policy.CastDevice
+import com.android.systemui.statusbar.policy.fakeCastController
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class MediaRouterRepositoryTest : SysuiTestCase() {
+    val kosmos = Kosmos()
+    val testScope = kosmos.testScope
+    val castController = kosmos.fakeCastController
+
+    val underTest = kosmos.realMediaRouterRepository
+
+    @Test
+    fun castDevices_empty_isEmpty() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.castDevices)
+            // Required to let the listener attach before the devices get set
+            runCurrent()
+
+            castController.castDevices = emptyList()
+
+            assertThat(latest).isEmpty()
+        }
+
+    @Test
+    fun castDevices_onlyIncludesMediaRouterOriginDevices() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.castDevices)
+            // Required to let the listener attach before the devices get set
+            runCurrent()
+
+            val projectionDevice =
+                CastDevice(
+                    id = "idProjection",
+                    name = "name",
+                    description = "desc",
+                    state = CastDevice.CastState.Connected,
+                    origin = CastDevice.CastOrigin.MediaProjection,
+                )
+            val routerDevice1 =
+                CastDevice(
+                    id = "idRouter1",
+                    name = "name",
+                    description = "desc",
+                    state = CastDevice.CastState.Connected,
+                    origin = CastDevice.CastOrigin.MediaRouter,
+                )
+
+            val routerDevice2 =
+                CastDevice(
+                    id = "idRouter2",
+                    name = "name",
+                    description = "desc",
+                    state = CastDevice.CastState.Connected,
+                    origin = CastDevice.CastOrigin.MediaRouter,
+                )
+            castController.setCastDevices(listOf(projectionDevice, routerDevice1, routerDevice2))
+
+            assertThat(latest).containsExactly(routerDevice1, routerDevice2).inOrder()
+        }
+
+    @Test
+    fun stopCasting_notifiesCastController() {
+        val device =
+            CastDevice(
+                id = "id",
+                name = "name",
+                description = "desc",
+                state = CastDevice.CastState.Connected,
+                origin = CastDevice.CastOrigin.MediaRouter,
+            )
+
+        underTest.stopCasting(device)
+
+        assertThat(castController.lastStoppedDevice).isEqualTo(device)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
new file mode 100644
index 0000000..8a6a50d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
@@ -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.systemui.statusbar.chips.casttootherdevice.domian.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediarouter.data.repository.fakeMediaRouterRepository
+import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor.mediaRouterChipInteractor
+import com.android.systemui.statusbar.chips.casttootherdevice.domain.model.MediaRouterCastModel
+import com.android.systemui.statusbar.policy.CastDevice
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class MediaRouterChipInteractorTest : SysuiTestCase() {
+    val kosmos = Kosmos()
+    val testScope = kosmos.testScope
+    val mediaRouterRepository = kosmos.fakeMediaRouterRepository
+
+    val underTest = kosmos.mediaRouterChipInteractor
+
+    @Test
+    fun mediaRouterCastingState_noDevices_doingNothing() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.mediaRouterCastingState)
+
+            mediaRouterRepository.castDevices.value = emptyList()
+
+            assertThat(latest).isEqualTo(MediaRouterCastModel.DoingNothing)
+        }
+
+    @Test
+    fun mediaRouterCastingState_disconnectedDevice_doingNothing() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.mediaRouterCastingState)
+
+            mediaRouterRepository.castDevices.value =
+                listOf(
+                    CastDevice(
+                        state = CastDevice.CastState.Disconnected,
+                        id = "id",
+                        name = "name",
+                        description = "desc",
+                        origin = CastDevice.CastOrigin.MediaRouter,
+                    )
+                )
+
+            assertThat(latest).isEqualTo(MediaRouterCastModel.DoingNothing)
+        }
+
+    @Test
+    fun mediaRouterCastingState_connectingDevice_casting() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.mediaRouterCastingState)
+
+            mediaRouterRepository.castDevices.value =
+                listOf(
+                    CastDevice(
+                        state = CastDevice.CastState.Connecting,
+                        id = "id",
+                        name = "name",
+                        description = "desc",
+                        origin = CastDevice.CastOrigin.MediaRouter,
+                    )
+                )
+
+            assertThat(latest).isEqualTo(MediaRouterCastModel.Casting)
+        }
+
+    @Test
+    fun mediaRouterCastingState_connectedDevice_casting() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.mediaRouterCastingState)
+
+            mediaRouterRepository.castDevices.value =
+                listOf(
+                    CastDevice(
+                        state = CastDevice.CastState.Connected,
+                        id = "id",
+                        name = "name",
+                        description = "desc",
+                        origin = CastDevice.CastOrigin.MediaRouter,
+                    )
+                )
+
+            assertThat(latest).isEqualTo(MediaRouterCastModel.Casting)
+        }
+
+    @Test
+    fun stopCasting_noDevices_doesNothing() =
+        testScope.runTest {
+            collectLastValue(underTest.mediaRouterCastingState)
+
+            mediaRouterRepository.castDevices.value = emptyList()
+            // Let the interactor catch up to the repo value
+            runCurrent()
+
+            underTest.stopCasting()
+
+            assertThat(mediaRouterRepository.lastStoppedDevice).isNull()
+        }
+
+    @Test
+    fun stopCasting_disconnectedDevice_doesNothing() =
+        testScope.runTest {
+            collectLastValue(underTest.mediaRouterCastingState)
+
+            mediaRouterRepository.castDevices.value =
+                listOf(
+                    CastDevice(
+                        state = CastDevice.CastState.Disconnected,
+                        id = "id",
+                        name = "name",
+                        description = "desc",
+                        origin = CastDevice.CastOrigin.MediaRouter,
+                    )
+                )
+            // Let the interactor catch up to the repo value
+            runCurrent()
+
+            underTest.stopCasting()
+
+            assertThat(mediaRouterRepository.lastStoppedDevice).isNull()
+        }
+
+    @Test
+    fun stopCasting_connectingDevice_notifiesRepo() =
+        testScope.runTest {
+            collectLastValue(underTest.mediaRouterCastingState)
+
+            val device =
+                CastDevice(
+                    state = CastDevice.CastState.Connecting,
+                    id = "id",
+                    name = "name",
+                    description = "desc",
+                    origin = CastDevice.CastOrigin.MediaRouter,
+                )
+            mediaRouterRepository.castDevices.value = listOf(device)
+            // Let the interactor catch up to the repo value
+            runCurrent()
+
+            underTest.stopCasting()
+
+            assertThat(mediaRouterRepository.lastStoppedDevice).isEqualTo(device)
+        }
+
+    @Test
+    fun stopCasting_connectedDevice_notifiesRepo() =
+        testScope.runTest {
+            collectLastValue(underTest.mediaRouterCastingState)
+
+            val device =
+                CastDevice(
+                    state = CastDevice.CastState.Connected,
+                    id = "id",
+                    name = "name",
+                    description = "desc",
+                    origin = CastDevice.CastOrigin.MediaRouter,
+                )
+            mediaRouterRepository.castDevices.value = listOf(device)
+            // Let the interactor catch up to the repo value
+            runCurrent()
+
+            underTest.stopCasting()
+
+            assertThat(mediaRouterRepository.lastStoppedDevice).isEqualTo(device)
+        }
+
+    @Test
+    fun stopCasting_multipleConnectedDevices_notifiesRepoOfFirst() =
+        testScope.runTest {
+            collectLastValue(underTest.mediaRouterCastingState)
+
+            val device1 =
+                CastDevice(
+                    state = CastDevice.CastState.Connected,
+                    id = "id1",
+                    name = "name",
+                    description = "desc",
+                    origin = CastDevice.CastOrigin.MediaRouter,
+                )
+            val device2 =
+                CastDevice(
+                    state = CastDevice.CastState.Connected,
+                    id = "id2",
+                    name = "name",
+                    description = "desc",
+                    origin = CastDevice.CastOrigin.MediaRouter,
+                )
+            mediaRouterRepository.castDevices.value = listOf(device1, device2)
+            // Let the interactor catch up to the repo value
+            runCurrent()
+
+            underTest.stopCasting()
+
+            assertThat(mediaRouterRepository.lastStoppedDevice).isEqualTo(device1)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
similarity index 92%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
index c6fb481..e9d6f0e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
@@ -48,10 +48,10 @@
 
 @SmallTest
 @OptIn(ExperimentalCoroutinesApi::class)
-class EndCastToOtherDeviceDialogDelegateTest : SysuiTestCase() {
+class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() {
     private val kosmos = Kosmos().also { it.testCase = this }
     private val sysuiDialog = mock<SystemUIDialog>()
-    private lateinit var underTest: EndCastToOtherDeviceDialogDelegate
+    private lateinit var underTest: EndCastScreenToOtherDeviceDialogDelegate
 
     @Test
     fun icon() {
@@ -68,7 +68,7 @@
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
-        verify(sysuiDialog).setTitle(R.string.cast_to_other_device_stop_dialog_title)
+        verify(sysuiDialog).setTitle(R.string.cast_screen_to_other_device_stop_dialog_title)
     }
 
     @Test
@@ -78,7 +78,7 @@
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
         verify(sysuiDialog)
-            .setMessage(context.getString(R.string.cast_to_other_device_stop_dialog_message))
+            .setMessage(context.getString(R.string.cast_screen_to_other_device_stop_dialog_message))
     }
 
     @Test
@@ -99,7 +99,7 @@
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
-        // It'd be nice to use R.string.cast_to_other_device_stop_dialog_message_specific_app
+        // It'd be nice to use R.string.cast_screen_to_other_device_stop_dialog_message_specific_app
         // directly, but it includes the <b> tags which aren't in the returned string.
         val result = argumentCaptor<CharSequence>()
         verify(sysuiDialog).setMessage(result.capture())
@@ -142,7 +142,7 @@
 
     private fun createAndSetDelegate(state: MediaProjectionState.Projecting) {
         underTest =
-            EndCastToOtherDeviceDialogDelegate(
+            EndCastScreenToOtherDeviceDialogDelegate(
                 kosmos.endMediaProjectionDialogHelper,
                 stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
                 ProjectionChipModel.Projecting(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
new file mode 100644
index 0000000..0af423d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.casttootherdevice.ui.view
+
+import android.content.DialogInterface
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediarouter.data.repository.fakeMediaRouterRepository
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor.mediaRouterChipInteractor
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.policy.CastDevice
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class EndGenericCastToOtherDeviceDialogDelegateTest : SysuiTestCase() {
+    private val kosmos = Kosmos().also { it.testCase = this }
+    private val sysuiDialog = mock<SystemUIDialog>()
+    private lateinit var underTest: EndGenericCastToOtherDeviceDialogDelegate
+
+    @Test
+    fun icon() {
+        createAndSetDelegate()
+
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setIcon(R.drawable.ic_cast_connected)
+    }
+
+    @Test
+    fun title() {
+        createAndSetDelegate()
+
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setTitle(R.string.cast_to_other_device_stop_dialog_title)
+    }
+
+    @Test
+    fun message() {
+        createAndSetDelegate()
+
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setMessage(R.string.cast_to_other_device_stop_dialog_message)
+    }
+
+    @Test
+    fun negativeButton() {
+        createAndSetDelegate()
+
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setNegativeButton(R.string.close_dialog_button, null)
+    }
+
+    @Test
+    fun positiveButton() =
+        kosmos.testScope.runTest {
+            createAndSetDelegate()
+
+            // Set up a real device so the stop works correctly
+            collectLastValue(kosmos.mediaRouterChipInteractor.mediaRouterCastingState)
+            val device =
+                CastDevice(
+                    state = CastDevice.CastState.Connected,
+                    id = "id",
+                    name = "name",
+                    description = "desc",
+                    origin = CastDevice.CastOrigin.MediaRouter,
+                )
+            kosmos.fakeMediaRouterRepository.castDevices.value = listOf(device)
+            // Let everything catch up to the repo value
+            runCurrent()
+            runCurrent()
+
+            underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+            val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
+
+            // Verify the button has the right text
+            verify(sysuiDialog)
+                .setPositiveButton(
+                    eq(R.string.cast_to_other_device_stop_dialog_button),
+                    clickListener.capture()
+                )
+
+            // Verify that clicking the button stops the recording
+            assertThat(kosmos.fakeMediaRouterRepository.lastStoppedDevice).isNull()
+
+            clickListener.firstValue.onClick(mock<DialogInterface>(), 0)
+            runCurrent()
+
+            assertThat(kosmos.fakeMediaRouterRepository.lastStoppedDevice).isEqualTo(device)
+        }
+
+    private fun createAndSetDelegate() {
+        underTest =
+            EndGenericCastToOtherDeviceDialogDelegate(
+                kosmos.endMediaProjectionDialogHelper,
+                stopAction = kosmos.mediaRouterChipInteractor::stopCasting,
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
index 74b6ae2..fe29140 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
@@ -20,6 +20,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.mockDialogTransitionAnimator
+import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.Kosmos
@@ -28,8 +29,10 @@
 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.mediarouter.data.repository.fakeMediaRouterRepository
 import com.android.systemui.res.R
-import com.android.systemui.statusbar.chips.casttootherdevice.ui.view.EndCastToOtherDeviceDialogDelegate
+import com.android.systemui.statusbar.chips.casttootherdevice.ui.view.EndCastScreenToOtherDeviceDialogDelegate
+import com.android.systemui.statusbar.chips.casttootherdevice.ui.view.EndGenericCastToOtherDeviceDialogDelegate
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.CAST_TO_OTHER_DEVICES_PACKAGE
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
@@ -38,6 +41,7 @@
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+import com.android.systemui.statusbar.policy.CastDevice
 import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
@@ -55,9 +59,11 @@
     private val kosmos = Kosmos().also { it.testCase = this }
     private val testScope = kosmos.testScope
     private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository
+    private val mediaRouterRepo = kosmos.fakeMediaRouterRepository
     private val systemClock = kosmos.fakeSystemClock
 
-    private val mockCastDialog = mock<SystemUIDialog>()
+    private val mockScreenCastDialog = mock<SystemUIDialog>()
+    private val mockGenericCastDialog = mock<SystemUIDialog>()
 
     private val chipBackgroundView = mock<ChipBackgroundContainer>()
     private val chipView =
@@ -76,14 +82,25 @@
     fun setUp() {
         setUpPackageManagerForMediaProjection(kosmos)
 
-        whenever(kosmos.mockSystemUIDialogFactory.create(any<EndCastToOtherDeviceDialogDelegate>()))
-            .thenReturn(mockCastDialog)
+        whenever(
+                kosmos.mockSystemUIDialogFactory.create(
+                    any<EndCastScreenToOtherDeviceDialogDelegate>()
+                )
+            )
+            .thenReturn(mockScreenCastDialog)
+        whenever(
+                kosmos.mockSystemUIDialogFactory.create(
+                    any<EndGenericCastToOtherDeviceDialogDelegate>()
+                )
+            )
+            .thenReturn(mockGenericCastDialog)
     }
 
     @Test
     fun chip_notProjectingState_isHidden() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
+            mediaRouterRepo.castDevices.value = emptyList()
 
             mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
 
@@ -91,9 +108,10 @@
         }
 
     @Test
-    fun chip_singleTaskState_otherDevicesPackage_isShownAsTimer() =
+    fun chip_projectionIsSingleTaskState_otherDevicesPackage_isShownAsTimer_forScreen() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
+            mediaRouterRepo.castDevices.value = emptyList()
 
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.SingleTask(
@@ -104,13 +122,15 @@
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
             assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
-            assertThat(icon.contentDescription).isNotNull()
+            assertThat((icon.contentDescription as ContentDescription.Resource).res)
+                .isEqualTo(R.string.cast_screen_to_other_device_chip_accessibility_label)
         }
 
     @Test
-    fun chip_entireScreenState_otherDevicesPackage_isShownAsTimer() =
+    fun chip_projectionIsEntireScreenState_otherDevicesPackage_isShownAsTimer_forScreen() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
+            mediaRouterRepo.castDevices.value = emptyList()
 
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
@@ -118,7 +138,72 @@
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
             assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
-            assertThat(icon.contentDescription).isNotNull()
+            assertThat((icon.contentDescription as ContentDescription.Resource).res)
+                .isEqualTo(R.string.cast_screen_to_other_device_chip_accessibility_label)
+        }
+
+    @Test
+    fun chip_routerStateDoingNothing_isHidden() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+            mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
+
+            mediaRouterRepo.castDevices.value = emptyList()
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+        }
+
+    @Test
+    fun chip_routerStateCasting_isShownAsGenericIconOnly() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+            mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
+
+            mediaRouterRepo.castDevices.value =
+                listOf(
+                    CastDevice(
+                        state = CastDevice.CastState.Connected,
+                        id = "id",
+                        name = "name",
+                        description = "desc",
+                        origin = CastDevice.CastOrigin.MediaRouter,
+                    )
+                )
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
+            val icon = (latest as OngoingActivityChipModel.Shown).icon
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
+            // This content description is just generic "Casting", not "Casting screen"
+            assertThat((icon.contentDescription as ContentDescription.Resource).res)
+                .isEqualTo(R.string.accessibility_casting)
+        }
+
+    @Test
+    fun chip_projectingAndRouterCasting_projectionInfoShown() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
+            mediaRouterRepo.castDevices.value =
+                listOf(
+                    CastDevice(
+                        state = CastDevice.CastState.Connected,
+                        id = "id",
+                        name = "name",
+                        description = "desc",
+                        origin = CastDevice.CastOrigin.MediaRouter,
+                    )
+                )
+
+            // Only the projection info will show a timer
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
+            val icon = (latest as OngoingActivityChipModel.Shown).icon
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
+            // MediaProjection == screen casting, so this content description reflects that we're
+            // using the MediaProjection information.
+            assertThat((icon.contentDescription as ContentDescription.Resource).res)
+                .isEqualTo(R.string.cast_screen_to_other_device_chip_accessibility_label)
         }
 
     @Test
@@ -133,7 +218,7 @@
         }
 
     @Test
-    fun chip_singleTaskState_normalPackage_isHidden() =
+    fun chip_projectionIsSingleTaskState_normalPackage_isHidden() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
@@ -144,7 +229,7 @@
         }
 
     @Test
-    fun chip_entireScreenState_normalPackage_isHidden() =
+    fun chip_projectionIsEntireScreenState_normalPackage_isHidden() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
@@ -155,7 +240,7 @@
         }
 
     @Test
-    fun chip_timeResetsOnEachNewShare() =
+    fun chip_projectionOnly_timeResetsOnEachNewShare() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
@@ -181,7 +266,38 @@
         }
 
     @Test
-    fun chip_entireScreen_clickListenerShowsCastDialog() =
+    fun chip_routerInfoThenProjectionInfo_switchesToTimer() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            // First, set only MediaRouter to have information and verify we just show the icon
+            systemClock.setElapsedRealtime(1234)
+            mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
+            mediaRouterRepo.castDevices.value =
+                listOf(
+                    CastDevice(
+                        state = CastDevice.CastState.Connected,
+                        id = "id",
+                        name = "name",
+                        description = "desc",
+                        origin = CastDevice.CastOrigin.MediaRouter,
+                    )
+                )
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
+
+            // Later, set MediaProjection to also have information
+            systemClock.setElapsedRealtime(5678)
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
+
+            // Verify the new time is used
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(5678)
+        }
+
+    @Test
+    fun chip_projectionStateEntireScreen_clickListenerShowsScreenCastDialog() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
             mediaProjectionRepo.mediaProjectionState.value =
@@ -193,7 +309,7 @@
             clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
-                    eq(mockCastDialog),
+                    eq(mockScreenCastDialog),
                     eq(chipBackgroundView),
                     eq(null),
                     ArgumentMatchers.anyBoolean(),
@@ -201,7 +317,7 @@
         }
 
     @Test
-    fun chip_singleTask_clickListenerShowsCastDialog() =
+    fun chip_projectionStateSingleTask_clickListenerShowsScreenCastDialog() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
@@ -217,7 +333,36 @@
             clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
-                    eq(mockCastDialog),
+                    eq(mockScreenCastDialog),
+                    eq(chipBackgroundView),
+                    eq(null),
+                    ArgumentMatchers.anyBoolean(),
+                )
+        }
+
+    @Test
+    fun chip_routerStateCasting_clickListenerShowsGenericCastDialog() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            mediaRouterRepo.castDevices.value =
+                listOf(
+                    CastDevice(
+                        state = CastDevice.CastState.Connected,
+                        id = "id",
+                        name = "name",
+                        description = "desc",
+                        origin = CastDevice.CastOrigin.MediaRouter,
+                    )
+                )
+
+            val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
+
+            clickListener!!.onClick(chipView)
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    eq(mockGenericCastDialog),
                     eq(chipBackgroundView),
                     eq(null),
                     ArgumentMatchers.anyBoolean(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
index ee929ae..f9ad5ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
@@ -61,7 +61,7 @@
             underTest.getDialogMessage(
                 MediaProjectionState.Projecting.EntireScreen("host.package"),
                 R.string.accessibility_home,
-                R.string.cast_to_other_device_stop_dialog_message_specific_app
+                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
             )
 
         assertThat(result).isEqualTo(context.getString(R.string.accessibility_home))
@@ -84,7 +84,7 @@
             underTest.getDialogMessage(
                 projectionState,
                 R.string.accessibility_home,
-                R.string.cast_to_other_device_stop_dialog_message_specific_app
+                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
             )
 
         assertThat(result).isEqualTo(context.getString(R.string.accessibility_home))
@@ -109,7 +109,7 @@
             underTest.getDialogMessage(
                 projectionState,
                 R.string.accessibility_home,
-                R.string.cast_to_other_device_stop_dialog_message_specific_app
+                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
             )
 
         // It'd be nice to use the R.string resources directly, but they include the <b> tags which
@@ -123,7 +123,7 @@
             underTest.getDialogMessage(
                 specificTaskInfo = null,
                 R.string.accessibility_home,
-                R.string.cast_to_other_device_stop_dialog_message_specific_app,
+                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
             )
 
         assertThat(result).isEqualTo(context.getString(R.string.accessibility_home))
@@ -141,7 +141,7 @@
             underTest.getDialogMessage(
                 task,
                 R.string.accessibility_home,
-                R.string.cast_to_other_device_stop_dialog_message_specific_app
+                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
             )
 
         assertThat(result).isEqualTo(context.getString(R.string.accessibility_home))
@@ -161,7 +161,7 @@
             underTest.getDialogMessage(
                 task,
                 R.string.accessibility_home,
-                R.string.cast_to_other_device_stop_dialog_message_specific_app
+                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
             )
 
         assertThat(result.toString()).isEqualTo("You will stop casting Fake Package")
@@ -186,7 +186,7 @@
             underTest.getDialogMessage(
                 projectionState,
                 R.string.accessibility_home,
-                R.string.cast_to_other_device_stop_dialog_message_specific_app
+                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
             )
 
         assertThat(result.toString()).isEqualTo("You will stop casting Fake & Package <Here>")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
index 25533d8..d87b3e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
@@ -19,16 +19,23 @@
 
 import android.app.Notification
 import android.os.UserHandle
+import android.platform.test.flag.junit.FlagsParameterization
 import android.provider.Settings
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.data.repository.Idle
+import com.android.systemui.scene.data.repository.setTransition
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -49,6 +56,8 @@
 import com.android.systemui.util.mockito.withArgCaptor
 import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
+import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestCoroutineScheduler
@@ -62,22 +71,28 @@
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
-import java.util.function.Consumer
-import kotlin.time.Duration.Companion.seconds
 import org.mockito.Mockito.`when` as whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @SmallTest
-@RunWith(AndroidJUnit4::class)
-class KeyguardCoordinatorTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class KeyguardCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() {
+
+    private val kosmos = Kosmos()
 
     private val headsUpManager: HeadsUpManager = mock()
     private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock()
     private val keyguardRepository = FakeKeyguardRepository()
-    private val keyguardTransitionRepository = FakeKeyguardTransitionRepository()
+    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     private val notifPipeline: NotifPipeline = mock()
     private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock()
     private val statusBarStateController: StatusBarStateController = mock()
 
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
     @Test
     fun testSetSectionHeadersVisibleInShade() = runKeyguardCoordinatorTest {
         clearInvocations(sectionHeaderVisibilityProvider)
@@ -147,10 +162,9 @@
         keyguardRepository.setKeyguardShowing(false)
         whenever(statusBarStateController.isExpanded).thenReturn(false)
         runKeyguardCoordinatorTest {
-            keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.GONE,
-                    this.testScheduler,
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Gone),
+                stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
             )
 
             // WHEN: A notification is posted
@@ -163,24 +177,20 @@
 
             // WHEN: The keyguard is now showing
             keyguardRepository.setKeyguardShowing(true)
-            keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.AOD,
-                    this.testScheduler,
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Lockscreen),
+                stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD)
             )
-            testScheduler.runCurrent()
 
             // THEN: The notification is recognized as "seen" and is filtered out.
             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
 
             // WHEN: The keyguard goes away
             keyguardRepository.setKeyguardShowing(false)
-            keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.AOD,
-                    to = KeyguardState.GONE,
-                    this.testScheduler,
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Gone),
+                stateTransition = TransitionStep(KeyguardState.AOD, KeyguardState.GONE)
             )
-            testScheduler.runCurrent()
 
             // THEN: The notification is shown regardless
             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
@@ -344,9 +354,9 @@
             val fakeEntry = NotificationEntryBuilder().build()
             collectionListener.onEntryAdded(fakeEntry)
             keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.AOD,
-                    to = KeyguardState.LOCKSCREEN,
-                    this.testScheduler,
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
             )
             testScheduler.runCurrent()
 
@@ -356,21 +366,17 @@
 
             // WHEN: Keyguard is no longer showing
             keyguardRepository.setKeyguardShowing(false)
-            keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.GONE,
-                    this.testScheduler,
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Gone),
+                stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
             )
-            testScheduler.runCurrent()
 
             // WHEN: Keyguard is shown again
             keyguardRepository.setKeyguardShowing(true)
-            keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.AOD,
-                    this.testScheduler,
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Lockscreen),
+                stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD)
             )
-            testScheduler.runCurrent()
 
             // THEN: The notification is now recognized as "seen" and is filtered out.
             assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
@@ -383,9 +389,9 @@
         keyguardRepository.setKeyguardShowing(true)
         runKeyguardCoordinatorTest {
             keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.LOCKSCREEN,
-                    this.testScheduler,
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
             )
             val fakeEntry = NotificationEntryBuilder().build()
             collectionListener.onEntryAdded(fakeEntry)
@@ -393,9 +399,9 @@
             // WHEN: Keyguard is no longer showing
             keyguardRepository.setKeyguardShowing(false)
             keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.GONE,
-                    this.testScheduler,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                this.testScheduler,
             )
 
             // WHEN: Keyguard is shown again
@@ -413,10 +419,9 @@
         keyguardRepository.setKeyguardShowing(true)
         keyguardRepository.setIsDozing(false)
         runKeyguardCoordinatorTest {
-            keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.LOCKSCREEN,
-                    this.testScheduler,
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Lockscreen),
+                stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN)
             )
             val firstEntry = NotificationEntryBuilder().setId(1).build()
             collectionListener.onEntryAdded(firstEntry)
@@ -437,21 +442,17 @@
 
             // WHEN: the keyguard is no longer showing
             keyguardRepository.setKeyguardShowing(false)
-            keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.GONE,
-                    this.testScheduler,
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Gone),
+                stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
             )
-            testScheduler.runCurrent()
 
             // WHEN: Keyguard is shown again
             keyguardRepository.setKeyguardShowing(true)
-            keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.LOCKSCREEN,
-                    this.testScheduler,
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Lockscreen),
+                stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN)
             )
-            testScheduler.runCurrent()
 
             // THEN: The first notification is considered seen and is filtered out.
             assertThat(unseenFilter.shouldFilterOut(firstEntry, 0L)).isTrue()
@@ -468,9 +469,9 @@
         keyguardRepository.setIsDozing(false)
         runKeyguardCoordinatorTest {
             keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.LOCKSCREEN,
-                    this.testScheduler,
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
             )
             testScheduler.runCurrent()
 
@@ -498,18 +499,18 @@
             // WHEN: the keyguard is no longer showing
             keyguardRepository.setKeyguardShowing(false)
             keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.GONE,
-                    this.testScheduler,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                this.testScheduler,
             )
             testScheduler.runCurrent()
 
             // WHEN: Keyguard is shown again
             keyguardRepository.setKeyguardShowing(true)
             keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.LOCKSCREEN,
-                    this.testScheduler,
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
             )
             testScheduler.runCurrent()
 
@@ -525,9 +526,9 @@
         keyguardRepository.setIsDozing(false)
         runKeyguardCoordinatorTest {
             keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.LOCKSCREEN,
-                    this.testScheduler,
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
             )
             testScheduler.runCurrent()
 
@@ -555,18 +556,18 @@
             // WHEN: the keyguard is no longer showing
             keyguardRepository.setKeyguardShowing(false)
             keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.GONE,
-                    this.testScheduler,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                this.testScheduler,
             )
             testScheduler.runCurrent()
 
             // WHEN: Keyguard is shown again
             keyguardRepository.setKeyguardShowing(true)
             keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.LOCKSCREEN,
-                    this.testScheduler,
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
             )
             testScheduler.runCurrent()
 
@@ -582,9 +583,9 @@
         keyguardRepository.setIsDozing(false)
         runKeyguardCoordinatorTest {
             keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.LOCKSCREEN,
-                    this.testScheduler,
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
             )
             testScheduler.runCurrent()
 
@@ -608,18 +609,18 @@
             // WHEN: the keyguard is no longer showing
             keyguardRepository.setKeyguardShowing(false)
             keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.LOCKSCREEN,
-                    to = KeyguardState.GONE,
-                    this.testScheduler,
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                this.testScheduler,
             )
             testScheduler.runCurrent()
 
             // WHEN: Keyguard is shown again
             keyguardRepository.setKeyguardShowing(true)
             keyguardTransitionRepository.sendTransitionSteps(
-                    from = KeyguardState.GONE,
-                    to = KeyguardState.LOCKSCREEN,
-                    this.testScheduler,
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
             )
             testScheduler.runCurrent()
 
@@ -646,7 +647,7 @@
                 headsUpManager,
                 keyguardNotifVisibilityProvider,
                 keyguardRepository,
-                keyguardTransitionRepository,
+                kosmos.keyguardTransitionInteractor,
                 KeyguardCoordinatorLogger(logcatLogBuffer()),
                 testScope.backgroundScope,
                 sectionHeaderVisibilityProvider,
@@ -706,4 +707,12 @@
                 )
             }
     }
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
index ed8843b..db829a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
@@ -23,6 +23,8 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
 import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_QS;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -308,6 +310,52 @@
         mBatteryController.fireBatteryLevelChanged();
     }
 
+    @Test
+    public void plugAndUnplugWhenInBatterySaver_stateUpdatedWithoutBatterySaverBroadcast() {
+        PowerSaveState state = new PowerSaveState.Builder()
+                .setBatterySaverEnabled(false)
+                .build();
+        when(mPowerManager.getPowerSaveState(PowerManager.ServiceType.AOD)).thenReturn(state);
+
+        // Set up on power save and not charging
+        when(mPowerManager.isPowerSaveMode()).thenReturn(true);
+        mBatteryController.onReceive(
+                getContext(), new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED));
+        mBatteryController.onReceive(getContext(), createChargingIntent(false));
+
+        TestCallback callback = new TestCallback();
+        mBatteryController.addCallback(callback);
+
+        assertThat(callback.pluggedIn).isFalse();
+        assertThat(callback.powerSaverOn).isTrue();
+
+        // Plug in (battery saver turns off)
+        when(mPowerManager.isPowerSaveMode()).thenReturn(false);
+        mBatteryController.onReceive(getContext(), createChargingIntent(true));
+
+        assertThat(callback.pluggedIn).isTrue();
+        assertThat(callback.powerSaverOn).isFalse();
+
+        // Unplug (battery saver turns back on)
+        when(mPowerManager.isPowerSaveMode()).thenReturn(true);
+        mBatteryController.onReceive(getContext(), createChargingIntent(false));
+
+        assertThat(callback.pluggedIn).isFalse();
+        assertThat(callback.powerSaverOn).isTrue();
+    }
+
+    private Intent createChargingIntent(boolean charging) {
+        Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        if (charging) {
+            return intent
+                .putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING)
+                .putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_AC);
+        } else {
+            return intent
+                .putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_DISCHARGING);
+        }
+    }
+
     private void setupIncompatibleCharging() {
         final List<UsbPort> usbPorts = new ArrayList<>();
         usbPorts.add(mUsbPort);
@@ -318,4 +366,19 @@
         when(mUsbPortStatus.getComplianceWarnings())
                 .thenReturn(new int[]{UsbPortStatus.COMPLIANCE_WARNING_DEBUG_ACCESSORY});
     }
+
+    private static class TestCallback
+        implements BatteryController.BatteryStateChangeCallback {
+        boolean pluggedIn = false;
+        boolean powerSaverOn = false;
+        @Override
+        public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+            this.pluggedIn = pluggedIn;
+        }
+
+        @Override
+        public void onPowerSaveChanged(boolean isPowerSave) {
+            this.powerSaverOn = isPowerSave;
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/icons/AppCategoryIconProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/icons/AppCategoryIconProviderTest.kt
new file mode 100644
index 0000000..ef41b6ee
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/icons/AppCategoryIconProviderTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.icons
+
+import android.app.role.RoleManager.ROLE_ASSISTANT
+import android.content.ComponentName
+import android.content.Intent
+import android.content.Intent.CATEGORY_APP_BROWSER
+import android.content.Intent.CATEGORY_APP_CONTACTS
+import android.content.Intent.CATEGORY_APP_EMAIL
+import android.content.mockPackageManager
+import android.content.mockPackageManagerWrapper
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.ResolveInfo
+import android.graphics.drawable.Icon
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.assist.mockAssistManager
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AppCategoryIconProviderTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val packageManagerWrapper = kosmos.mockPackageManagerWrapper
+    private val packageManager = kosmos.mockPackageManager
+    private val assistManager = kosmos.mockAssistManager
+    private val provider = kosmos.appCategoryIconProvider
+
+    @Before
+    fun setUp() {
+        whenever(packageManagerWrapper.resolveActivity(any<Intent>(), any<Int>())).thenAnswer {
+            invocation ->
+            val category = (invocation.arguments[0] as Intent).categories.first()
+            val categoryAppIcon =
+                categoryAppIcons.firstOrNull { it.category == category } ?: return@thenAnswer null
+            val activityInfo = ActivityInfo().also { it.packageName = categoryAppIcon.packageName }
+            return@thenAnswer ResolveInfo().also { it.activityInfo = activityInfo }
+        }
+        whenever(packageManager.getPackageInfo(any<String>(), any<Int>())).thenAnswer { invocation
+            ->
+            val packageName = invocation.arguments[0] as String
+            val categoryAppIcon =
+                categoryAppIcons.firstOrNull { it.packageName == packageName }
+                    ?: return@thenAnswer null
+            val applicationInfo =
+                ApplicationInfo().also {
+                    it.packageName = packageName
+                    it.icon = categoryAppIcon.iconResId
+                }
+            return@thenAnswer PackageInfo().also {
+                it.packageName = packageName
+                it.applicationInfo = applicationInfo
+            }
+        }
+    }
+
+    @Test
+    fun assistantAppIcon_defaultAssistantSet_returnsIcon() =
+        testScope.runTest {
+            whenever(assistManager.assistInfo)
+                .thenReturn(ComponentName(ASSISTANT_PACKAGE, ASSISTANT_CLASS))
+
+            val icon = provider.assistantAppIcon() as Icon
+
+            assertThat(icon.resPackage).isEqualTo(ASSISTANT_PACKAGE)
+            assertThat(icon.resId).isEqualTo(ASSISTANT_ICON_RES_ID)
+        }
+
+    @Test
+    fun assistantAppIcon_defaultAssistantNotSet_returnsNull() =
+        testScope.runTest {
+            whenever(assistManager.assistInfo).thenReturn(null)
+
+            assertThat(provider.assistantAppIcon()).isNull()
+        }
+
+    @Test
+    fun categoryAppIcon_returnsIconOfKnownBrowserApp() {
+        testScope.runTest {
+            val icon = provider.categoryAppIcon(CATEGORY_APP_BROWSER) as Icon
+
+            assertThat(icon.resPackage).isEqualTo(BROWSER_PACKAGE)
+            assertThat(icon.resId).isEqualTo(BROWSER_ICON_RES_ID)
+        }
+    }
+
+    @Test
+    fun categoryAppIcon_returnsIconOfKnownContactsApp() {
+        testScope.runTest {
+            val icon = provider.categoryAppIcon(CATEGORY_APP_CONTACTS) as Icon
+
+            assertThat(icon.resPackage).isEqualTo(CONTACTS_PACKAGE)
+            assertThat(icon.resId).isEqualTo(CONTACTS_ICON_RES_ID)
+        }
+    }
+
+    @Test
+    fun categoryAppIcon_noDefaultAppForCategoryEmail_returnsNull() {
+        testScope.runTest {
+            val icon = provider.categoryAppIcon(CATEGORY_APP_EMAIL)
+
+            assertThat(icon).isNull()
+        }
+    }
+
+    private companion object {
+        private const val ASSISTANT_PACKAGE = "the.assistant.app"
+        private const val ASSISTANT_CLASS = "the.assistant.app.class"
+        private const val ASSISTANT_ICON_RES_ID = 123
+
+        private const val BROWSER_PACKAGE = "com.test.browser"
+        private const val BROWSER_ICON_RES_ID = 1
+
+        private const val CONTACTS_PACKAGE = "app.test.contacts"
+        private const val CONTACTS_ICON_RES_ID = 234
+
+        private val categoryAppIcons =
+            listOf(
+                App(ROLE_ASSISTANT, ASSISTANT_PACKAGE, ASSISTANT_ICON_RES_ID),
+                App(CATEGORY_APP_BROWSER, BROWSER_PACKAGE, BROWSER_ICON_RES_ID),
+                App(CATEGORY_APP_CONTACTS, CONTACTS_PACKAGE, CONTACTS_ICON_RES_ID),
+            )
+    }
+
+    private class App(val category: String, val packageName: String, val iconResId: Int)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerCollectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerCollectorTest.kt
new file mode 100644
index 0000000..dd78e4a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeControllerCollectorTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume
+
+import android.media.IVolumeController
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.media.data.repository.VolumeControllerEvent
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class VolumeControllerCollectorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val eventsFlow = MutableStateFlow<VolumeControllerEvent?>(null)
+    private val underTest = VolumeControllerCollector(kosmos.applicationCoroutineScope)
+
+    private val volumeController = mock<IVolumeController> {}
+
+    @Test
+    fun volumeControllerEvent_volumeChanged_callsMethod() =
+        testEvent(VolumeControllerEvent.VolumeChanged(3, 0)) {
+            verify(volumeController) { 1 * { volumeController.volumeChanged(eq(3), eq(0)) } }
+        }
+
+    @Test
+    fun volumeControllerEvent_dismiss_callsMethod() =
+        testEvent(VolumeControllerEvent.Dismiss) {
+            verify(volumeController) { 1 * { volumeController.dismiss() } }
+        }
+
+    @Test
+    fun volumeControllerEvent_displayCsdWarning_callsMethod() =
+        testEvent(VolumeControllerEvent.DisplayCsdWarning(0, 1)) {
+            verify(volumeController) { 1 * { volumeController.displayCsdWarning(eq(0), eq(1)) } }
+        }
+
+    @Test
+    fun volumeControllerEvent_displaySafeVolumeWarning_callsMethod() =
+        testEvent(VolumeControllerEvent.DisplaySafeVolumeWarning(1)) {
+            verify(volumeController) { 1 * { volumeController.displaySafeVolumeWarning(eq(1)) } }
+        }
+
+    @Test
+    fun volumeControllerEvent_masterMuteChanged_callsMethod() =
+        testEvent(VolumeControllerEvent.MasterMuteChanged(1)) {
+            verify(volumeController) { 1 * { volumeController.masterMuteChanged(1) } }
+        }
+
+    @Test
+    fun volumeControllerEvent_setA11yMode_callsMethod() =
+        testEvent(VolumeControllerEvent.SetA11yMode(1)) {
+            verify(volumeController) { 1 * { volumeController.setA11yMode(1) } }
+        }
+
+    @Test
+    fun volumeControllerEvent_SetLayoutDirection_callsMethod() =
+        testEvent(VolumeControllerEvent.SetLayoutDirection(1)) {
+            verify(volumeController) { 1 * { volumeController.setLayoutDirection(eq(1)) } }
+        }
+
+    private fun testEvent(event: VolumeControllerEvent, verify: () -> Unit) =
+        kosmos.testScope.runTest {
+            underTest.collectToController(eventsFlow.filterNotNull(), volumeController)
+
+            eventsFlow.value = event
+            runCurrent()
+
+            verify()
+        }
+}
diff --git a/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt
index 8901314..9d7d916 100644
--- a/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt
@@ -17,6 +17,13 @@
 
 import android.content.pm.PackageManager
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shared.system.PackageManagerWrapper
 import com.android.systemui.util.mockito.mock
 
-val Kosmos.packageManager by Kosmos.Fixture { mock<PackageManager>() }
+val Kosmos.mockPackageManager by Kosmos.Fixture { mock<PackageManager>() }
+
+var Kosmos.packageManager by Kosmos.Fixture { mockPackageManager }
+
+val Kosmos.mockPackageManagerWrapper by Kosmos.Fixture { mock<PackageManagerWrapper>() }
+
+var Kosmos.packageManagerWrapper by Kosmos.Fixture { mockPackageManagerWrapper }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt
index b7d6f3a..22eb646 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt
@@ -19,4 +19,6 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.assistManager by Kosmos.Fixture { mock<AssistManager>() }
+val Kosmos.mockAssistManager by Kosmos.Fixture { mock<AssistManager>() }
+
+var Kosmos.assistManager by Kosmos.Fixture { mockAssistManager }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index 55c803a..a1021f6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -100,6 +100,7 @@
 val Kosmos.shortcutHelperViewModel by
     Kosmos.Fixture {
         ShortcutHelperViewModel(
+            applicationCoroutineScope,
             testDispatcher,
             shortcutHelperStateInteractor,
             shortcutHelperCategoriesInteractor
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
index 24e47b0..550ecb3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
 
@@ -34,5 +35,6 @@
             shadeInteractor = shadeInteractor,
             applicationScope = applicationCoroutineScope,
             unfoldTransitionInteractor = unfoldTransitionInteractor,
+            occlusionInteractor = sceneContainerOcclusionInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/FakeMediaRouterRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/FakeMediaRouterRepository.kt
new file mode 100644
index 0000000..8aa7a03
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/FakeMediaRouterRepository.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.mediarouter.data.repository
+
+import com.android.systemui.statusbar.policy.CastDevice
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMediaRouterRepository : MediaRouterRepository {
+    override val castDevices: MutableStateFlow<List<CastDevice>> = MutableStateFlow(emptyList())
+
+    var lastStoppedDevice: CastDevice? = null
+        private set
+
+    override fun stopCasting(device: CastDevice) {
+        lastStoppedDevice = device
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryKosmos.kt
new file mode 100644
index 0000000..eec9920
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.mediarouter.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.policy.fakeCastController
+
+val Kosmos.realMediaRouterRepository by
+    Kosmos.Fixture {
+        MediaRouterRepositoryImpl(
+            scope = applicationCoroutineScope,
+            castController = fakeCastController,
+        )
+    }
+
+val Kosmos.fakeMediaRouterRepository by Kosmos.Fixture { FakeMediaRouterRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractorKosmos.kt
new file mode 100644
index 0000000..cb18b68
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractorKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.mediarouter.data.repository.fakeMediaRouterRepository
+
+val Kosmos.mediaRouterChipInteractor by
+    Kosmos.Fixture {
+        MediaRouterChipInteractor(
+            scope = applicationCoroutineScope,
+            mediaRouterRepository = fakeMediaRouterRepository,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
index 4baa8d0..a8de460 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor.mediaRouterChipInteractor
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
 import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper
 import com.android.systemui.util.time.fakeSystemClock
@@ -28,6 +29,7 @@
         CastToOtherDeviceChipViewModel(
             scope = applicationCoroutineScope,
             mediaProjectionChipInteractor = mediaProjectionChipInteractor,
+            mediaRouterChipInteractor = mediaRouterChipInteractor,
             systemClock = fakeSystemClock,
             endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
             dialogTransitionAnimator = mockDialogTransitionAnimator,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/CastControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/CastControllerKosmos.kt
new file mode 100644
index 0000000..8e77437
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/CastControllerKosmos.kt
@@ -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.systemui.statusbar.policy
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeCastController: FakeCastController by Kosmos.Fixture { FakeCastController() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeCastController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeCastController.kt
new file mode 100644
index 0000000..2df0c7a5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeCastController.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy
+
+import java.io.PrintWriter
+
+class FakeCastController : CastController {
+    private var listeners = mutableListOf<CastController.Callback>()
+
+    private var castDevices = emptyList<CastDevice>()
+
+    var lastStoppedDevice: CastDevice? = null
+        private set
+
+    override fun addCallback(listener: CastController.Callback) {
+        listeners += listener
+    }
+
+    override fun removeCallback(listener: CastController.Callback) {
+        listeners -= listener
+    }
+
+    override fun getCastDevices(): List<CastDevice> {
+        return castDevices
+    }
+
+    fun setCastDevices(devices: List<CastDevice>) {
+        castDevices = devices
+        listeners.forEach { it.onCastDevicesChanged() }
+    }
+
+    override fun startCasting(device: CastDevice?) {}
+
+    override fun stopCasting(device: CastDevice?) {
+        lastStoppedDevice = device
+    }
+
+    override fun hasConnectedCastDevice(): Boolean {
+        return castDevices.any { it.state == CastDevice.CastState.Connected }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {}
+
+    override fun setDiscovering(request: Boolean) {}
+
+    override fun setCurrentUserId(currentUserId: Int) {}
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/icons/AppCategoryIconProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/icons/AppCategoryIconProviderKosmos.kt
new file mode 100644
index 0000000..7987185
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/icons/AppCategoryIconProviderKosmos.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.systemui.util.icons
+
+import android.content.mockPackageManager
+import android.content.mockPackageManagerWrapper
+import com.android.systemui.assist.mockAssistManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+
+var Kosmos.fakeAppCategoryIconProvider by Kosmos.Fixture { FakeAppCategoryIconProvider() }
+
+var Kosmos.appCategoryIconProvider: AppCategoryIconProvider by
+    Kosmos.Fixture {
+        AppCategoryIconProviderImpl(
+            testDispatcher,
+            mockAssistManager,
+            mockPackageManager,
+            mockPackageManagerWrapper
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/icons/FakeAppCategoryIconProvider.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/icons/FakeAppCategoryIconProvider.kt
new file mode 100644
index 0000000..3e7bf21
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/icons/FakeAppCategoryIconProvider.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.icons
+
+import android.app.role.RoleManager.ROLE_ASSISTANT
+import android.graphics.drawable.Icon
+
+class FakeAppCategoryIconProvider : AppCategoryIconProvider {
+
+    private val installedApps = mutableMapOf<String, App>()
+
+    fun installCategoryApp(category: String, packageName: String, iconResId: Int) {
+        installedApps[category] = App(packageName, iconResId)
+    }
+
+    fun installAssistantApp(packageName: String, iconResId: Int) {
+        installedApps[ROLE_ASSISTANT] = App(packageName, iconResId)
+    }
+
+    override suspend fun assistantAppIcon() = categoryAppIcon(ROLE_ASSISTANT)
+
+    override suspend fun categoryAppIcon(category: String): Icon? {
+        val app = installedApps[category] ?: return null
+        return Icon.createWithResource(app.packageName, app.iconResId)
+    }
+
+    private class App(val packageName: String, val iconResId: Int)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckedTest.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckedTest.java
index 5d21ddd..372a7c7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckedTest.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckedTest.java
@@ -109,7 +109,7 @@
                 } else if (cls == ZenModeController.class) {
                     obj = new FakeZenModeController(this);
                 } else if (cls == CastController.class) {
-                    obj = new FakeCastController(this);
+                    obj = new LeakCheckerCastController(this);
                 } else if (cls == HotspotController.class) {
                     obj = new FakeHotspotController(this);
                 } else if (cls == FlashlightController.class) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeCastController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckerCastController.java
similarity index 67%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeCastController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckerCastController.java
index 5fae38f..2249bc0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeCastController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckerCastController.java
@@ -1,15 +1,17 @@
 /*
  * Copyright (C) 2016 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
+ * 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.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  */
 
 package com.android.systemui.utils.leaks;
@@ -23,8 +25,8 @@
 import java.util.ArrayList;
 import java.util.List;
 
-public class FakeCastController extends BaseLeakChecker<Callback> implements CastController {
-    public FakeCastController(LeakCheck test) {
+public class LeakCheckerCastController extends BaseLeakChecker<Callback> implements CastController {
+    public LeakCheckerCastController(LeakCheck test) {
         super(test, "cast");
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerCollectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerCollectorKosmos.kt
new file mode 100644
index 0000000..d60f14c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/VolumeControllerCollectorKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+val Kosmos.volumeControllerCollector by
+    Kosmos.Fixture { VolumeControllerCollector(applicationCoroutineScope) }
diff --git a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
index 4860a27..8abbe56 100644
--- a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
+++ b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
@@ -792,10 +792,11 @@
     }
 
     private String getVToUAllowlist(Context context, int userId) {
-        return Settings.Secure.getStringForUser(
+         String allowlist = Settings.Secure.getStringForUser(
                 context.getContentResolver(),
                 Settings.Secure.V_TO_U_RESTORE_ALLOWLIST,
                 userId);
+         return (allowlist == null) ? "" : allowlist;
     }
 
     private static long extractRadix(byte[] data, int offset, int maxChars, int radix)
diff --git a/services/core/java/com/android/server/CertBlacklister.java b/services/core/java/com/android/server/CertBlacklister.java
deleted file mode 100644
index e726c6a..0000000
--- a/services/core/java/com/android/server/CertBlacklister.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright (C) 2012 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;
-
-import android.content.Context;
-import android.content.ContentResolver;
-import android.database.ContentObserver;
-import android.os.Binder;
-import android.os.FileUtils;
-import android.provider.Settings;
-import android.util.Slog;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-
-import libcore.io.IoUtils;
-
-/**
- * <p>CertBlacklister provides a simple mechanism for updating the platform denylists for SSL
- * certificate public keys and serial numbers.
- */
-public class CertBlacklister extends Binder {
-
-    private static final String TAG = "CertBlacklister";
-
-    private static final String DENYLIST_ROOT = System.getenv("ANDROID_DATA") + "/misc/keychain/";
-
-    public static final String PUBKEY_PATH = DENYLIST_ROOT + "pubkey_blacklist.txt";
-    public static final String SERIAL_PATH = DENYLIST_ROOT + "serial_blacklist.txt";
-
-    public static final String PUBKEY_BLACKLIST_KEY = "pubkey_blacklist";
-    public static final String SERIAL_BLACKLIST_KEY = "serial_blacklist";
-
-    private static class BlacklistObserver extends ContentObserver {
-
-        private final String mKey;
-        private final String mName;
-        private final String mPath;
-        private final File mTmpDir;
-        private final ContentResolver mContentResolver;
-
-        public BlacklistObserver(String key, String name, String path, ContentResolver cr) {
-            super(null);
-            mKey = key;
-            mName = name;
-            mPath = path;
-            mTmpDir = new File(mPath).getParentFile();
-            mContentResolver = cr;
-        }
-
-        @Override
-        public void onChange(boolean selfChange) {
-            super.onChange(selfChange);
-            writeDenylist();
-        }
-
-        public String getValue() {
-            return Settings.Secure.getString(mContentResolver, mKey);
-        }
-
-        private void writeDenylist() {
-            new Thread("BlacklistUpdater") {
-                public void run() {
-                    synchronized(mTmpDir) {
-                        String blacklist = getValue();
-                        if (blacklist != null) {
-                            Slog.i(TAG, "Certificate blacklist changed, updating...");
-                            FileOutputStream out = null;
-                            try {
-                                // create a temporary file
-                                File tmp = File.createTempFile("journal", "", mTmpDir);
-                                // mark it -rw-r--r--
-                                tmp.setReadable(true, false);
-                                // write to it
-                                out = new FileOutputStream(tmp);
-                                out.write(blacklist.getBytes());
-                                // sync to disk
-                                FileUtils.sync(out);
-                                // atomic rename
-                                tmp.renameTo(new File(mPath));
-                                Slog.i(TAG, "Certificate blacklist updated");
-                            } catch (IOException e) {
-                                Slog.e(TAG, "Failed to write blacklist", e);
-                            } finally {
-                                IoUtils.closeQuietly(out);
-                            }
-                        }
-                    }
-                }
-            }.start();
-        }
-    }
-
-    public CertBlacklister(Context context) {
-        registerObservers(context.getContentResolver());
-    }
-
-    private BlacklistObserver buildPubkeyObserver(ContentResolver cr) {
-        return new BlacklistObserver(PUBKEY_BLACKLIST_KEY,
-                    "pubkey",
-                    PUBKEY_PATH,
-                    cr);
-    }
-
-    private BlacklistObserver buildSerialObserver(ContentResolver cr) {
-        return new BlacklistObserver(SERIAL_BLACKLIST_KEY,
-                    "serial",
-                    SERIAL_PATH,
-                    cr);
-    }
-
-    private void registerObservers(ContentResolver cr) {
-        // set up the public key denylist observer
-        cr.registerContentObserver(
-            Settings.Secure.getUriFor(PUBKEY_BLACKLIST_KEY),
-            true,
-            buildPubkeyObserver(cr)
-        );
-
-        // set up the serial number denylist observer
-        cr.registerContentObserver(
-            Settings.Secure.getUriFor(SERIAL_BLACKLIST_KEY),
-            true,
-            buildSerialObserver(cr)
-        );
-    }
-}
diff --git a/services/core/java/com/android/server/CertBlocklister.java b/services/core/java/com/android/server/CertBlocklister.java
new file mode 100644
index 0000000..9e23f88
--- /dev/null
+++ b/services/core/java/com/android/server/CertBlocklister.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2012 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;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Binder;
+import android.os.FileUtils;
+import android.provider.Settings;
+import android.util.Slog;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * <p>CertBlocklister provides a simple mechanism for updating the platform denylists for SSL
+ * certificate public keys and serial numbers.
+ */
+public class CertBlocklister extends Binder {
+
+    private static final String TAG = "CertBlocklister";
+
+    private static final String DENYLIST_ROOT = System.getenv("ANDROID_DATA") + "/misc/keychain/";
+
+    /* For compatibility reasons, the name of these paths cannot be changed */
+    public static final String PUBKEY_PATH = DENYLIST_ROOT + "pubkey_blacklist.txt";
+    public static final String SERIAL_PATH = DENYLIST_ROOT + "serial_blacklist.txt";
+
+    /* For compatibility reasons, the name of these keys cannot be changed */
+    public static final String PUBKEY_BLOCKLIST_KEY = "pubkey_blacklist";
+    public static final String SERIAL_BLOCKLIST_KEY = "serial_blacklist";
+
+    private static class BlocklistObserver extends ContentObserver {
+
+        private final String mKey;
+        private final String mName;
+        private final String mPath;
+        private final File mTmpDir;
+        private final ContentResolver mContentResolver;
+
+        BlocklistObserver(String key, String name, String path, ContentResolver cr) {
+            super(null);
+            mKey = key;
+            mName = name;
+            mPath = path;
+            mTmpDir = new File(mPath).getParentFile();
+            mContentResolver = cr;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            super.onChange(selfChange);
+            new Thread("BlocklistUpdater") {
+                public void run() {
+                    writeDenylist();
+                }
+            }.start();
+        }
+
+        public String getValue() {
+            return Settings.Secure.getStringForUser(
+                mContentResolver, mKey, mContentResolver.getUserId());
+        }
+
+        private void writeDenylist() {
+            synchronized (mTmpDir) {
+                String blocklist = getValue();
+                if (blocklist == null) {
+                    return;
+                }
+                if (mPath.equals(SERIAL_PATH)) {
+                    Slog.w(TAG, "The certificate blocklist based on serials is deprecated. "
+                            + "Please use the pubkey blocklist instead.");
+                }
+                Slog.i(TAG, "Certificate blocklist changed, updating...");
+                FileOutputStream out = null;
+                try {
+                    // Create a temporary file and rename it atomically.
+                    File tmp = File.createTempFile("journal", "", mTmpDir);
+                    tmp.setReadable(true /* readable */, false /* ownerOnly */);
+                    out = new FileOutputStream(tmp);
+                    out.write(blocklist.getBytes());
+                    FileUtils.sync(out);
+                    tmp.renameTo(new File(mPath));
+                    Slog.i(TAG, "Certificate blocklist updated");
+                } catch (IOException e) {
+                    Slog.e(TAG, "Failed to write blocklist", e);
+                } finally {
+                    IoUtils.closeQuietly(out);
+                }
+            }
+        }
+    }
+
+    public CertBlocklister(Context context) {
+        registerObservers(context.getContentResolver());
+    }
+
+    private BlocklistObserver buildPubkeyObserver(ContentResolver cr) {
+        return new BlocklistObserver(PUBKEY_BLOCKLIST_KEY,
+                    "pubkey",
+                    PUBKEY_PATH,
+                    cr);
+    }
+
+    private BlocklistObserver buildSerialObserver(ContentResolver cr) {
+        return new BlocklistObserver(SERIAL_BLOCKLIST_KEY,
+                    "serial",
+                    SERIAL_PATH,
+                    cr);
+    }
+
+    private void registerObservers(ContentResolver cr) {
+        // set up the public key denylist observer
+        cr.registerContentObserver(
+                Settings.Secure.getUriFor(PUBKEY_BLOCKLIST_KEY),
+                true,
+                buildPubkeyObserver(cr)
+        );
+
+        // set up the serial number denylist observer
+        cr.registerContentObserver(
+                Settings.Secure.getUriFor(SERIAL_BLOCKLIST_KEY),
+                true,
+                buildSerialObserver(cr)
+        );
+    }
+}
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index bc83a0e..bacfd8f 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -921,8 +921,7 @@
 
     //helper function to determine if limit on num listeners applies to callingUid
     private boolean doesLimitApplyForListeners(int callingUid, int exemptUid) {
-        return (callingUid != Process.SYSTEM_UID
-                && callingUid != Process.PHONE_UID
+        return (!TelephonyPermissions.isSystemOrPhone(callingUid)
                 && callingUid != exemptUid);
     }
 
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index fbb6ccf..b0d734d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -70,7 +70,6 @@
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentProvider;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -80,7 +79,6 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.res.Resources;
-import android.database.ContentObserver;
 import android.hardware.input.InputManager;
 import android.inputmethodservice.InputMethodService;
 import android.media.AudioManagerInternal;
@@ -195,7 +193,6 @@
 import java.security.InvalidParameterException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -349,8 +346,6 @@
     @GuardedBy("ImfLock.class")
     private UserDataRepository mUserDataRepository;
 
-    @MultiUserUnawareField
-    final SettingsObserver mSettingsObserver;
     final WindowManagerInternal mWindowManagerInternal;
     private final ActivityManagerInternal mActivityManagerInternal;
     final PackageManagerInternal mPackageManagerInternal;
@@ -570,82 +565,52 @@
     @NonNull
     private final ImeTrackerService mImeTrackerService;
 
-    class SettingsObserver extends ContentObserver {
-
-        /**
-         * <em>This constructor must be called within the lock.</em>
-         */
-        SettingsObserver(Handler handler) {
-            super(handler);
+    @GuardedBy("ImfLock.class")
+    private void onSecureSettingsChangedLocked(@NonNull String key, @UserIdInt int userId) {
+        if (!mConcurrentMultiUserModeEnabled && userId != mCurrentUserId) {
+            return;
         }
-
-        void registerContentObserverForAllUsers() {
-            ContentResolver resolver = mContext.getContentResolver();
-            resolver.registerContentObserverAsUser(Settings.Secure.getUriFor(
-                    Settings.Secure.DEFAULT_INPUT_METHOD), false, this, UserHandle.ALL);
-            resolver.registerContentObserverAsUser(Settings.Secure.getUriFor(
-                    Settings.Secure.ENABLED_INPUT_METHODS), false, this, UserHandle.ALL);
-            resolver.registerContentObserverAsUser(Settings.Secure.getUriFor(
-                    Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE), false, this, UserHandle.ALL);
-            resolver.registerContentObserverAsUser(Settings.Secure.getUriFor(
-                    Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD), false, this, UserHandle.ALL);
-            resolver.registerContentObserverAsUser(Settings.Secure.getUriFor(
-                    Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE), false, this, UserHandle.ALL);
-            resolver.registerContentObserverAsUser(Settings.Secure.getUriFor(
-                    STYLUS_HANDWRITING_ENABLED), false, this, UserHandle.ALL);
-        }
-
-        @Override
-        public void onChange(boolean selfChange, @NonNull Collection<Uri> uris, int flags,
-                @UserIdInt int userId) {
-            uris.forEach(uri -> onChangeInternal(uri, userId));
-        }
-
-        private void onChangeInternal(@NonNull Uri uri, @UserIdInt int userId) {
-            final Uri showImeUri = Settings.Secure.getUriFor(
-                    Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD);
-            final Uri accessibilityRequestingNoImeUri = Settings.Secure.getUriFor(
-                    Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE);
-            final Uri stylusHandwritingEnabledUri = Settings.Secure.getUriFor(
-                    STYLUS_HANDWRITING_ENABLED);
-            synchronized (ImfLock.class) {
-                if (!mConcurrentMultiUserModeEnabled && mCurrentUserId != userId) {
-                    return;
+        switch (key) {
+            case Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD: {
+                mMenuController.updateKeyboardFromSettingsLocked();
+                break;
+            }
+            case Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE: {
+                final int accessibilitySoftKeyboardSetting = Settings.Secure.getIntForUser(
+                        mContext.getContentResolver(),
+                        Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0, userId);
+                mVisibilityStateComputer.getImePolicy().setA11yRequestNoSoftKeyboard(
+                        accessibilitySoftKeyboardSetting);
+                final var userData = getUserData(userId);
+                if (mVisibilityStateComputer.getImePolicy().isA11yRequestNoSoftKeyboard()) {
+                    hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
+                            0 /* flags */, SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE, userId);
+                } else if (isShowRequestedForCurrentWindow(userId)) {
+                    showCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
+                            InputMethodManager.SHOW_IMPLICIT,
+                            SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE, userId);
                 }
-
-                if (showImeUri.equals(uri)) {
-                    mMenuController.updateKeyboardFromSettingsLocked();
-                } else if (accessibilityRequestingNoImeUri.equals(uri)) {
-                    final int accessibilitySoftKeyboardSetting = Settings.Secure.getIntForUser(
-                            mContext.getContentResolver(),
-                            Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0, userId);
-                    mVisibilityStateComputer.getImePolicy().setA11yRequestNoSoftKeyboard(
-                            accessibilitySoftKeyboardSetting);
-                    final var userData = getUserData(userId);
-                    if (mVisibilityStateComputer.getImePolicy().isA11yRequestNoSoftKeyboard()) {
-                        hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
-                                0 /* flags */, SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE,
-                                userId);
-                    } else if (isShowRequestedForCurrentWindow(userId)) {
-                        showCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
-                                InputMethodManager.SHOW_IMPLICIT,
-                                SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE, userId);
-                    }
-                } else if (stylusHandwritingEnabledUri.equals(uri)) {
-                    InputMethodManager.invalidateLocalStylusHandwritingAvailabilityCaches();
-                    InputMethodManager
-                            .invalidateLocalConnectionlessStylusHandwritingAvailabilityCaches();
-                } else {
-                    boolean enabledChanged = false;
-                    String newEnabled = InputMethodSettingsRepository.get(userId)
-                            .getEnabledInputMethodsStr();
-                    final var userData = getUserData(userId);
-                    if (!userData.mLastEnabledInputMethodsStr.equals(newEnabled)) {
-                        userData.mLastEnabledInputMethodsStr = newEnabled;
-                        enabledChanged = true;
-                    }
-                    updateInputMethodsFromSettingsLocked(enabledChanged, userId);
+                break;
+            }
+            case STYLUS_HANDWRITING_ENABLED: {
+                InputMethodManager.invalidateLocalStylusHandwritingAvailabilityCaches();
+                InputMethodManager
+                        .invalidateLocalConnectionlessStylusHandwritingAvailabilityCaches();
+                break;
+            }
+            case Settings.Secure.DEFAULT_INPUT_METHOD:
+            case Settings.Secure.ENABLED_INPUT_METHODS:
+            case Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE: {
+                boolean enabledChanged = false;
+                String newEnabled = InputMethodSettingsRepository.get(userId)
+                        .getEnabledInputMethodsStr();
+                final var userData = getUserData(userId);
+                if (!userData.mLastEnabledInputMethodsStr.equals(newEnabled)) {
+                    userData.mLastEnabledInputMethodsStr = newEnabled;
+                    enabledChanged = true;
                 }
+                updateInputMethodsFromSettingsLocked(enabledChanged, userId);
+                break;
             }
         }
     }
@@ -1118,8 +1083,6 @@
             }
             SystemLocaleWrapper.onStart(context, this::onActionLocaleChanged, mHandler);
             mImeTrackerService = new ImeTrackerService(mHandler);
-            // Note: SettingsObserver doesn't register observers in its constructor.
-            mSettingsObserver = new SettingsObserver(mHandler);
             mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
             mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
             mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
@@ -1389,7 +1352,19 @@
                 }, "Lazily initialize IMMS#mImeDrawsImeNavBarRes");
 
                 mMyPackageMonitor.register(mContext, UserHandle.ALL, mIoHandler);
-                mSettingsObserver.registerContentObserverForAllUsers();
+                SecureSettingsChangeCallback.register(mHandler, mContext.getContentResolver(),
+                        new String[] {
+                                Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE,
+                                Settings.Secure.DEFAULT_INPUT_METHOD,
+                                Settings.Secure.ENABLED_INPUT_METHODS,
+                                Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE,
+                                Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
+                                Settings.Secure.STYLUS_HANDWRITING_ENABLED,
+                        }, (key, flags, userId) -> {
+                            synchronized (ImfLock.class) {
+                                onSecureSettingsChangedLocked(key, userId);
+                            }
+                        });
 
                 final IntentFilter broadcastFilterForAllUsers = new IntentFilter();
                 broadcastFilterForAllUsers.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
diff --git a/services/core/java/com/android/server/inputmethod/SecureSettingsChangeCallback.java b/services/core/java/com/android/server/inputmethod/SecureSettingsChangeCallback.java
new file mode 100644
index 0000000..328d7c6
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/SecureSettingsChangeCallback.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.server.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.ArrayMap;
+
+import java.util.Collection;
+
+/**
+ * A wrapper interface to monitor the given set of {@link Settings.Secure}.
+ */
+@FunctionalInterface
+interface SecureSettingsChangeCallback {
+    /**
+     * Called back when the value associated with {@code key} is updated.
+     *
+     * @param key a key defined in {@link Settings.Secure}
+     * @param flags flags defined in {@link ContentResolver.NotifyFlags}
+     * @param userId the user ID with which the value is associated
+     */
+    void onChange(@NonNull String key, @ContentResolver.NotifyFlags int flags,
+            @UserIdInt int userId);
+
+    /**
+     * Registers {@link SecureSettingsChangeCallback} to the given set of {@link Settings.Secure}.
+     *
+     * @param handler  {@link Handler} to be used to call back {@link #onChange(String, int, int)}
+     * @param resolver {@link ContentResolver} with which {@link Settings.Secure} will be retrieved
+     * @param keys     A set of {@link Settings.Secure} to be monitored
+     * @param callback {@link SecureSettingsChangeCallback} to be called back
+     */
+    @NonNull
+    static void register(@NonNull Handler handler, @NonNull ContentResolver resolver,
+            @NonNull String[] keys, @NonNull SecureSettingsChangeCallback callback) {
+        final ArrayMap<Uri, String> uriMapper = new ArrayMap<>();
+        for (String key : keys) {
+            uriMapper.put(Settings.Secure.getUriFor(key), key);
+        }
+        final ContentObserver observer = new ContentObserver(handler) {
+            @Override
+            public void onChange(boolean selfChange, @NonNull Collection<Uri> uris, int flags,
+                    @UserIdInt int userId) {
+                uris.forEach(uri -> {
+                    final String key = uriMapper.get(uri);
+                    if (key != null) {
+                        callback.onChange(key, flags, userId);
+                    }
+                });
+            }
+        };
+        for (Uri uri : uriMapper.keySet()) {
+            resolver.registerContentObserverAsUser(uri, false /* notifyForDescendants */, observer,
+                    UserHandle.ALL);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 3a0c1d0..c09504f 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -196,9 +196,12 @@
         int USER_LOCKED_BUBBLE = 0x00000002;
     }
 
+    private final Object mLock = new Object();
     // pkg|uid => PackagePreferences
+    @GuardedBy("mLock")
     private final ArrayMap<String, PackagePreferences> mPackagePreferences = new ArrayMap<>();
     // pkg|userId => PackagePreferences
+    @GuardedBy("mLock")
     private final ArrayMap<String, PackagePreferences> mRestoredWithoutUids = new ArrayMap<>();
 
     private final Context mContext;
@@ -270,7 +273,7 @@
                     Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
                     NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW);
         }
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
                 tag = parser.getName();
                 if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
@@ -492,6 +495,7 @@
                 DEFAULT_BUBBLE_PREFERENCE, mClock.millis());
     }
 
+    @GuardedBy("mLock")
     private PackagePreferences getOrCreatePackagePreferencesLocked(String pkg,
             @UserIdInt int userId, int uid, int importance, int priority, int visibility,
             boolean showBadge, int bubblePreference, long creationTime) {
@@ -661,7 +665,7 @@
             notifPermissions = mPermissionHelper.getNotificationPermissionValues(userId);
         }
 
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             final int N = mPackagePreferences.size();
             for (int i = 0; i < N; i++) {
                 final PackagePreferences r = mPackagePreferences.valueAt(i);
@@ -670,11 +674,10 @@
                 }
                 writePackageXml(r, out, notifPermissions, forBackup);
             }
-        }
-        if (Flags.persistIncompleteRestoreData() && !forBackup) {
-            synchronized (mRestoredWithoutUids) {
-                final int N = mRestoredWithoutUids.size();
-                for (int i = 0; i < N; i++) {
+
+            if (Flags.persistIncompleteRestoreData() && !forBackup) {
+                final int M = mRestoredWithoutUids.size();
+                for (int i = 0; i < M; i++) {
                     final PackagePreferences r = mRestoredWithoutUids.valueAt(i);
                     writePackageXml(r, out, notifPermissions, false);
                 }
@@ -777,7 +780,7 @@
      */
     public void setBubblesAllowed(String pkg, int uid, int bubblePreference) {
         boolean changed;
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences p = getOrCreatePackagePreferencesLocked(pkg, uid);
             changed = p.bubblePreference != bubblePreference;
             p.bubblePreference = bubblePreference;
@@ -797,20 +800,20 @@
      */
     @Override
     public int getBubblePreference(String pkg, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             return getOrCreatePackagePreferencesLocked(pkg, uid).bubblePreference;
         }
     }
 
     public int getAppLockedFields(String pkg, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             return getOrCreatePackagePreferencesLocked(pkg, uid).lockedAppFields;
         }
     }
 
     @Override
     public boolean canShowBadge(String packageName, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             return getOrCreatePackagePreferencesLocked(packageName, uid).showBadge;
         }
     }
@@ -818,7 +821,7 @@
     @Override
     public void setShowBadge(String packageName, int uid, boolean showBadge) {
         boolean changed = false;
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences pkgPrefs = getOrCreatePackagePreferencesLocked(packageName, uid);
             if (pkgPrefs.showBadge != showBadge) {
                 pkgPrefs.showBadge = showBadge;
@@ -831,28 +834,28 @@
     }
 
     public boolean isInInvalidMsgState(String packageName, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
             return r.hasSentInvalidMessage && !r.hasSentValidMessage;
         }
     }
 
     public boolean hasUserDemotedInvalidMsgApp(String packageName, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
             return isInInvalidMsgState(packageName, uid) ? r.userDemotedMsgApp : false;
         }
     }
 
     public void setInvalidMsgAppDemoted(String packageName, int uid, boolean isDemoted) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
             r.userDemotedMsgApp = isDemoted;
         }
     }
 
     public boolean setInvalidMessageSent(String packageName, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
             boolean valueChanged = r.hasSentInvalidMessage == false;
             r.hasSentInvalidMessage = true;
@@ -862,7 +865,7 @@
     }
 
     public boolean setValidMessageSent(String packageName, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
             boolean valueChanged = r.hasSentValidMessage == false;
             r.hasSentValidMessage = true;
@@ -873,7 +876,7 @@
 
     @VisibleForTesting
     boolean hasSentInvalidMsg(String packageName, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
             return r.hasSentInvalidMessage;
         }
@@ -881,7 +884,7 @@
 
     @VisibleForTesting
     boolean hasSentValidMsg(String packageName, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
             return r.hasSentValidMessage;
         }
@@ -889,7 +892,7 @@
 
     @VisibleForTesting
     boolean didUserEverDemoteInvalidMsgApp(String packageName, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
             return r.userDemotedMsgApp;
         }
@@ -897,7 +900,7 @@
 
     /** Sets whether this package has sent a notification with valid bubble metadata. */
     public boolean setValidBubbleSent(String packageName, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
             boolean valueChanged = !r.hasSentValidBubble;
             r.hasSentValidBubble = true;
@@ -906,14 +909,14 @@
     }
 
     boolean hasSentValidBubble(String packageName, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
             return r.hasSentValidBubble;
         }
     }
 
     boolean isImportanceLocked(String pkg, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
             return r.fixedImportance || r.defaultAppLockedImportance;
         }
@@ -924,7 +927,7 @@
         if (groupId == null) {
             return false;
         }
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(packageName, uid);
             NotificationChannelGroup group = r.groups.get(groupId);
             if (group == null) {
@@ -935,13 +938,13 @@
     }
 
     int getPackagePriority(String pkg, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             return getOrCreatePackagePreferencesLocked(pkg, uid).priority;
         }
     }
 
     int getPackageVisibility(String pkg, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             return getOrCreatePackagePreferencesLocked(pkg, uid).visibility;
         }
     }
@@ -956,7 +959,7 @@
             throw new IllegalArgumentException("group.getName() can't be empty");
         }
         boolean needsDndChange = false;
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 throw new IllegalArgumentException("Invalid package");
@@ -1010,7 +1013,7 @@
                 && channel.getImportance() <= IMPORTANCE_MAX, "Invalid importance level");
 
         boolean needsPolicyFileChange = false, wasUndeleted = false, needsDndChange = false;
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 throw new IllegalArgumentException("Invalid package");
@@ -1154,7 +1157,7 @@
 
     void unlockNotificationChannelImportance(String pkg, int uid, String updatedChannelId) {
         Objects.requireNonNull(updatedChannelId);
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 throw new IllegalArgumentException("Invalid package");
@@ -1176,7 +1179,7 @@
         Objects.requireNonNull(updatedChannel.getId());
         boolean changed = false;
         boolean needsDndChange = false;
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 throw new IllegalArgumentException("Invalid package");
@@ -1351,7 +1354,7 @@
             String channelId, String conversationId, boolean returnParentIfNoConversationChannel,
             boolean includeDeleted) {
         Preconditions.checkNotNull(pkg);
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return null;
@@ -1392,7 +1395,7 @@
         Preconditions.checkNotNull(pkg);
         Preconditions.checkNotNull(conversationId);
         List<NotificationChannel> channels = new ArrayList<>();
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return channels;
@@ -1412,7 +1415,7 @@
             int callingUid, boolean fromSystemOrSystemUi) {
         boolean deletedChannel = false;
         boolean channelBypassedDnd = false;
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return false;
@@ -1448,7 +1451,7 @@
     public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) {
         Objects.requireNonNull(pkg);
         Objects.requireNonNull(channelId);
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return;
@@ -1460,7 +1463,7 @@
     @Override
     public void permanentlyDeleteNotificationChannels(String pkg, int uid) {
         Objects.requireNonNull(pkg);
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return;
@@ -1491,7 +1494,7 @@
                 boolean fixed = mPermissionHelper.isPermissionFixed(
                         pi.packageName, user.getUserHandle().getIdentifier());
                 if (fixed) {
-                    synchronized (mPackagePreferences) {
+                    synchronized (mLock) {
                         PackagePreferences p = getOrCreatePackagePreferencesLocked(
                                 pi.packageName, pi.applicationInfo.uid);
                         p.fixedImportance = true;
@@ -1506,7 +1509,7 @@
 
     public void updateDefaultApps(int userId, ArraySet<String> toRemove,
             ArraySet<Pair<String, Integer>> toAdd) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             for (PackagePreferences p : mPackagePreferences.values()) {
                 if (userId == UserHandle.getUserId(p.uid)) {
                     if (toRemove != null && toRemove.contains(p.pkg)) {
@@ -1536,7 +1539,7 @@
     public NotificationChannelGroup getNotificationChannelGroupWithChannels(String pkg,
             int uid, String groupId, boolean includeDeleted) {
         Objects.requireNonNull(pkg);
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null || groupId == null || !r.groups.containsKey(groupId)) {
                 return null;
@@ -1559,7 +1562,7 @@
     public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg,
             int uid) {
         Objects.requireNonNull(pkg);
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return null;
@@ -1573,7 +1576,7 @@
             boolean includeBlocked, Set<String> activeChannelFilter) {
         Objects.requireNonNull(pkg);
         Map<String, NotificationChannelGroup> groups = new ArrayMap<>();
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return ParceledListSlice.emptyList();
@@ -1624,7 +1627,7 @@
             String groupId, int callingUid, boolean fromSystemOrSystemUi) {
         List<NotificationChannel> deletedChannels = new ArrayList<>();
         boolean groupBypassedDnd = false;
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null || TextUtils.isEmpty(groupId)) {
                 return deletedChannels;
@@ -1656,7 +1659,7 @@
     public Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
             int uid) {
         List<NotificationChannelGroup> groups = new ArrayList<>();
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return groups;
@@ -1667,7 +1670,7 @@
     }
 
     public NotificationChannelGroup getGroupForChannel(String pkg, int uid, String channelId) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences p = getPackagePreferencesLocked(pkg, uid);
             if (p != null) {
                 NotificationChannel nc = p.channels.get(channelId);
@@ -1686,7 +1689,7 @@
 
     public ArrayList<ConversationChannelWrapper> getConversations(IntArray userIds,
             boolean onlyImportant) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             ArrayList<ConversationChannelWrapper> conversations = new ArrayList<>();
             for (PackagePreferences p : mPackagePreferences.values()) {
                 if (userIds.binarySearch(UserHandle.getUserId(p.uid)) >= 0) {
@@ -1730,7 +1733,7 @@
 
     public ArrayList<ConversationChannelWrapper> getConversations(String pkg, int uid) {
         Objects.requireNonNull(pkg);
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return new ArrayList<>();
@@ -1772,7 +1775,7 @@
     public @NonNull List<String> deleteConversations(String pkg, int uid,
             Set<String> conversationIds, int callingUid, boolean fromSystemOrSystemUi) {
         List<String> deletedChannelIds = new ArrayList<>();
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return deletedChannelIds;
@@ -1805,7 +1808,7 @@
             boolean includeDeleted) {
         Objects.requireNonNull(pkg);
         List<NotificationChannel> channels = new ArrayList<>();
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return ParceledListSlice.emptyList();
@@ -1827,7 +1830,7 @@
     public ParceledListSlice<NotificationChannel> getNotificationChannelsBypassingDnd(String pkg,
             int uid) {
         List<NotificationChannel> channels = new ArrayList<>();
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             final PackagePreferences r = mPackagePreferences.get(
                     packagePreferencesKey(pkg, uid));
             if (r != null) {
@@ -1848,7 +1851,7 @@
      * upgrades.
      */
     public boolean onlyHasDefaultChannel(String pkg, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
             if (r.channels.size() == (notificationClassification() ? 5 : 1)
                     && r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) {
@@ -1861,7 +1864,7 @@
     public int getDeletedChannelCount(String pkg, int uid) {
         Objects.requireNonNull(pkg);
         int deletedCount = 0;
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return deletedCount;
@@ -1880,7 +1883,7 @@
     public int getBlockedChannelCount(String pkg, int uid) {
         Objects.requireNonNull(pkg);
         int blockedCount = 0;
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return blockedCount;
@@ -1923,7 +1926,7 @@
         ArraySet<Pair<String, Integer>> candidatePkgs = new ArraySet<>();
 
         final IntArray currentUserIds = mUserProfiles.getCurrentProfileIds();
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             final int numPackagePreferences = mPackagePreferences.size();
             for (int i = 0; i < numPackagePreferences; i++) {
                 final PackagePreferences r = mPackagePreferences.valueAt(i);
@@ -1992,7 +1995,7 @@
      * considered for sentiment adjustments (and thus never show a blocking helper).
      */
     public void setAppImportanceLocked(String packageName, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences prefs = getOrCreatePackagePreferencesLocked(packageName, uid);
             if ((prefs.lockedAppFields & LockableAppFields.USER_LOCKED_IMPORTANCE) != 0) {
                 return;
@@ -2008,7 +2011,7 @@
      * Returns the delegate for a given package, if it's allowed by the package and the user.
      */
     public @Nullable String getNotificationDelegate(String sourcePkg, int sourceUid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences prefs = getPackagePreferencesLocked(sourcePkg, sourceUid);
 
             if (prefs == null || prefs.delegate == null) {
@@ -2026,7 +2029,7 @@
      */
     public void setNotificationDelegate(String sourcePkg, int sourceUid,
             String delegatePkg, int delegateUid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences prefs = getOrCreatePackagePreferencesLocked(sourcePkg, sourceUid);
             prefs.delegate = new Delegate(delegatePkg, delegateUid, true);
         }
@@ -2036,7 +2039,7 @@
      * Used by an app to turn off its notification delegate.
      */
     public void revokeNotificationDelegate(String sourcePkg, int sourceUid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences prefs = getPackagePreferencesLocked(sourcePkg, sourceUid);
             if (prefs != null && prefs.delegate != null) {
                 prefs.delegate.mEnabled = false;
@@ -2050,7 +2053,7 @@
      */
     public boolean isDelegateAllowed(String sourcePkg, int sourceUid,
             String potentialDelegatePkg, int potentialDelegateUid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences prefs = getPackagePreferencesLocked(sourcePkg, sourceUid);
 
             return prefs != null && prefs.isValidDelegate(potentialDelegatePkg,
@@ -2096,24 +2099,25 @@
         pw.println("per-package config version: " + XML_VERSION);
 
         pw.println("PackagePreferences:");
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             dumpPackagePreferencesLocked(pw, prefix, filter, mPackagePreferences, pkgPermissions);
+            pw.println("Restored without uid:");
+            dumpPackagePreferencesLocked(pw, prefix, filter, mRestoredWithoutUids, null);
         }
-        pw.println("Restored without uid:");
-        dumpPackagePreferencesLocked(pw, prefix, filter, mRestoredWithoutUids, null);
     }
 
     public void dump(ProtoOutputStream proto,
             @NonNull NotificationManagerService.DumpFilter filter,
             ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> pkgPermissions) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             dumpPackagePreferencesLocked(proto, RankingHelperProto.RECORDS, filter,
                     mPackagePreferences, pkgPermissions);
+            dumpPackagePreferencesLocked(proto, RankingHelperProto.RECORDS_RESTORED_WITHOUT_UID,
+                    filter, mRestoredWithoutUids, null);
         }
-        dumpPackagePreferencesLocked(proto, RankingHelperProto.RECORDS_RESTORED_WITHOUT_UID, filter,
-                mRestoredWithoutUids, null);
     }
 
+    @GuardedBy("mLock")
     private void dumpPackagePreferencesLocked(PrintWriter pw, String prefix,
             @NonNull NotificationManagerService.DumpFilter filter,
             ArrayMap<String, PackagePreferences> packagePreferences,
@@ -2298,7 +2302,7 @@
             pkgsWithPermissionsToHandle = pkgPermissions.keySet();
         }
         int pulledEvents = 0;
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             for (int i = 0; i < mPackagePreferences.size(); i++) {
                 if (pulledEvents > NOTIFICATION_PREFERENCES_PULL_LIMIT) {
                     break;
@@ -2378,7 +2382,7 @@
      * {@link StatsEvent}.
      */
     public void pullPackageChannelPreferencesStats(List<StatsEvent> events) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             int totalChannelsPulled = 0;
             for (int i = 0; i < mPackagePreferences.size(); i++) {
                 if (totalChannelsPulled > NOTIFICATION_CHANNEL_PULL_LIMIT) {
@@ -2414,7 +2418,7 @@
      * {@link StatsEvent}.
      */
     public void pullPackageChannelGroupPreferencesStats(List<StatsEvent> events) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             int totalGroupsPulled = 0;
             for (int i = 0; i < mPackagePreferences.size(); i++) {
                 if (totalGroupsPulled > NOTIFICATION_CHANNEL_GROUP_PULL_LIMIT) {
@@ -2443,10 +2447,12 @@
             ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> pkgPermissions) {
         JSONObject ranking = new JSONObject();
         JSONArray PackagePreferencess = new JSONArray();
-        try {
-            ranking.put("noUid", mRestoredWithoutUids.size());
-        } catch (JSONException e) {
-            // pass
+        synchronized (mLock) {
+            try {
+                ranking.put("noUid", mRestoredWithoutUids.size());
+            } catch (JSONException e) {
+                // pass
+            }
         }
 
         // Track data that we've handled from the permissions-based list
@@ -2455,7 +2461,7 @@
             pkgsWithPermissionsToHandle = pkgPermissions.keySet();
         }
 
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             final int N = mPackagePreferences.size();
             for (int i = 0; i < N; i++) {
                 final PackagePreferences r = mPackagePreferences.valueAt(i);
@@ -2561,7 +2567,7 @@
     }
 
     public Map<Integer, String> getPackageBans() {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             final int N = mPackagePreferences.size();
             ArrayMap<Integer, String> packageBans = new ArrayMap<>(N);
             for (int i = 0; i < N; i++) {
@@ -2620,7 +2626,7 @@
 
     private Map<String, Integer> getPackageChannels() {
         ArrayMap<String, Integer> packageChannels = new ArrayMap<>();
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             for (int i = 0; i < mPackagePreferences.size(); i++) {
                 final PackagePreferences r = mPackagePreferences.valueAt(i);
                 int channelCount = 0;
@@ -2636,7 +2642,7 @@
     }
 
     public void onUserRemoved(int userId) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             int N = mPackagePreferences.size();
             for (int i = N - 1; i >= 0; i--) {
                 PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i);
@@ -2648,7 +2654,7 @@
     }
 
     protected void onLocaleChanged(Context context, int userId) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             int N = mPackagePreferences.size();
             for (int i = 0; i < N; i++) {
                 PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i);
@@ -2678,22 +2684,24 @@
             for (int i = 0; i < size; i++) {
                 final String pkg = pkgList[i];
                 final int uid = uidList[i];
-                synchronized (mPackagePreferences) {
+                synchronized (mLock) {
                     mPackagePreferences.remove(packagePreferencesKey(pkg, uid));
+                    mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, changeUserId));
                 }
-                mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, changeUserId));
                 updated = true;
             }
         } else {
             for (String pkg : pkgList) {
-                // Package install
-                final PackagePreferences r =
-                        mRestoredWithoutUids.get(unrestoredPackageKey(pkg, changeUserId));
-                if (r != null) {
-                    try {
-                        r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId);
-                        mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, changeUserId));
-                        synchronized (mPackagePreferences) {
+                try {
+                    // Package install
+                    int uid = mPm.getPackageUidAsUser(pkg, changeUserId);
+                    PackagePermission p = null;
+                    synchronized (mLock) {
+                        final PackagePreferences r =
+                                mRestoredWithoutUids.get(unrestoredPackageKey(pkg, changeUserId));
+                        if (r != null) {
+                            r.uid = uid;
+                            mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, changeUserId));
                             mPackagePreferences.put(packagePreferencesKey(r.pkg, r.uid), r);
 
                             // Try to restore any unrestored sound resources
@@ -2715,32 +2723,29 @@
                                     channel.setSound(restoredUri, channel.getAudioAttributes());
                                 }
                             }
-                        }
-                        if (r.migrateToPm) {
-                            try {
-                                PackagePermission p = new PackagePermission(
+
+                            if (r.migrateToPm) {
+                                p = new PackagePermission(
                                         r.pkg, UserHandle.getUserId(r.uid),
                                         r.importance != IMPORTANCE_NONE,
                                         hasUserConfiguredSettings(r));
-                                mPermissionHelper.setNotificationPermission(p);
-                            } catch (Exception e) {
-                                Slog.e(TAG, "could not migrate setting for " + r.pkg, e);
                             }
+                            updated = true;
                         }
-                        updated = true;
-                    } catch (Exception e) {
-                        Slog.e(TAG, "could not restore " + r.pkg, e);
                     }
+                    if (p != null) {
+                        mPermissionHelper.setNotificationPermission(p);
+                    }
+                } catch (Exception e) {
+                    Slog.e(TAG, "could not restore " + pkg, e);
                 }
                 // Package upgrade
                 try {
-                    synchronized (mPackagePreferences) {
-                        PackagePreferences fullPackagePreferences = getPackagePreferencesLocked(pkg,
-                                mPm.getPackageUidAsUser(pkg, changeUserId));
-                        if (fullPackagePreferences != null) {
-                            updated |= createDefaultChannelIfNeededLocked(fullPackagePreferences);
-                            updated |= deleteDefaultChannelIfNeededLocked(fullPackagePreferences);
-                        }
+                    PackagePreferences fullPackagePreferences = getPackagePreferencesLocked(pkg,
+                            mPm.getPackageUidAsUser(pkg, changeUserId));
+                    if (fullPackagePreferences != null) {
+                        updated |= createDefaultChannelIfNeededLocked(fullPackagePreferences);
+                        updated |= deleteDefaultChannelIfNeededLocked(fullPackagePreferences);
                     }
                 } catch (PackageManager.NameNotFoundException e) {
                 }
@@ -2754,7 +2759,7 @@
     }
 
     public void clearData(String pkg, int uid) {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             PackagePreferences p = getPackagePreferencesLocked(pkg, uid);
             if (p != null) {
                 p.channels = new ArrayMap<>();
@@ -2941,7 +2946,7 @@
     }
 
     public void unlockAllNotificationChannels() {
-        synchronized (mPackagePreferences) {
+        synchronized (mLock) {
             final int numPackagePreferences = mPackagePreferences.size();
             for (int i = 0; i < numPackagePreferences; i++) {
                 final PackagePreferences r = mPackagePreferences.valueAt(i);
@@ -2958,7 +2963,7 @@
                     PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL),
                     user.getUserHandle().getIdentifier());
             for (PackageInfo pi : packages) {
-                synchronized (mPackagePreferences) {
+                synchronized (mLock) {
                     PackagePreferences p = getOrCreatePackagePreferencesLocked(
                             pi.packageName, pi.applicationInfo.uid);
                     if (p.migrateToPm && p.uid != UNKNOWN_UID) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index c0b8034..2e63cdb 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -186,6 +186,7 @@
 import com.android.internal.pm.pkg.component.ParsedMainComponent;
 import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
 import com.android.internal.telephony.CarrierAppUtils;
+import com.android.internal.telephony.TelephonyPermissions;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.ConcurrentUtils;
@@ -4492,8 +4493,7 @@
     void setSystemAppHiddenUntilInstalled(@NonNull Computer snapshot, String packageName,
             boolean hidden) {
         final int callingUid = Binder.getCallingUid();
-        final boolean calledFromSystemOrPhone = callingUid == Process.PHONE_UID
-                || callingUid == Process.SYSTEM_UID;
+        final boolean calledFromSystemOrPhone = TelephonyPermissions.isSystemOrPhone(callingUid);
         if (!calledFromSystemOrPhone) {
             mContext.enforceCallingOrSelfPermission(Manifest.permission.SUSPEND_APPS,
                     "setSystemAppHiddenUntilInstalled");
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index ff8abf8..924b36c 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -92,6 +92,7 @@
 
 import com.android.internal.content.InstallLocationUtils;
 import com.android.internal.content.NativeLibraryHelper;
+import com.android.internal.telephony.TelephonyPermissions;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FastPrintWriter;
 import com.android.internal.util.HexDump;
@@ -356,7 +357,7 @@
      * If not, throws a {@link SecurityException}.
      */
     public static void enforceSystemOrPhoneCaller(String methodName, int callingUid) {
-        if (callingUid != Process.PHONE_UID && callingUid != Process.SYSTEM_UID) {
+        if (!TelephonyPermissions.isSystemOrPhone(callingUid)) {
             throw new SecurityException(
                     "Cannot call " + methodName + " from UID " + callingUid);
         }
diff --git a/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java b/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java
index bf32045..5565b6f 100644
--- a/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java
+++ b/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java
@@ -47,7 +47,7 @@
     private static final String LOGDIR_PREFIX = "logs-";
 
     public CertificateTransparencyLogInstallReceiver() {
-        super("/data/misc/keychain/trusted_ct_logs/", "ct_logs", "metadata/", "version");
+        super("/data/misc/keychain/ct/", "ct_logs", "metadata/", "version");
     }
 
     @Override
@@ -85,7 +85,7 @@
             }
         }
         try {
-            // 3. Create /data/misc/keychain/trusted_ct_logs/<new_version>/ .
+            // 3. Create /data/misc/keychain/ct/<new_version>/ .
             newVersion.mkdir();
             if (!newVersion.isDirectory()) {
                 throw new IOException("Unable to make directory " + newVersion.getCanonicalPath());
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 543eb70..129cee7 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -53,6 +53,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.app.WindowConfiguration.activityTypeToString;
+import static android.app.WindowConfiguration.isFloating;
 import static android.app.admin.DevicePolicyResources.Drawables.Source.PROFILE_SWITCH_ANIMATION;
 import static android.app.admin.DevicePolicyResources.Drawables.Style.OUTLINE;
 import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
@@ -342,6 +343,7 @@
 import android.service.dreams.DreamActivity;
 import android.service.voice.IVoiceInteractionSession;
 import android.util.ArraySet;
+import android.util.DisplayMetrics;
 import android.util.EventLog;
 import android.util.Log;
 import android.util.MergedConfiguration;
@@ -2923,14 +2925,10 @@
 
     /** Makes starting window always fill the associated task. */
     private void attachStartingSurfaceToAssociatedTask() {
-        if (mSyncState == SYNC_STATE_NONE && isEmbedded()) {
-            // Collect this activity since it's starting window will reparent to task. To ensure
-            // any starting window's transaction will occur in order.
-            mTransitionController.collect(this);
-        }
+        mTransitionController.collect(mStartingWindow);
         // Associate the configuration of starting window with the task.
         overrideConfigurationPropagation(mStartingWindow, mStartingData.mAssociatedTask);
-        getSyncTransaction().reparent(mStartingWindow.mSurfaceControl,
+        mStartingWindow.getSyncTransaction().reparent(mStartingWindow.mSurfaceControl,
                 mStartingData.mAssociatedTask.mSurfaceControl);
     }
 
@@ -6537,9 +6535,7 @@
             // and the token could be null.
             return;
         }
-        if (r.mDisplayContent.mActivityRefresher != null) {
-            r.mDisplayContent.mActivityRefresher.onActivityRefreshed(r);
-        }
+        r.mDisplayContent.mAppCompatCameraPolicy.onActivityRefreshed(r);
     }
 
     static void splashScreenAttachedLocked(IBinder token) {
@@ -8196,7 +8192,7 @@
     }
 
     void setRequestedOrientation(@ActivityInfo.ScreenOrientation int requestedOrientation) {
-        if (mAppCompatController.getAppCompatOrientationOverrides()
+        if (mAppCompatController.getOrientationPolicy()
                 .shouldIgnoreRequestedOrientation(requestedOrientation)) {
             return;
         }
@@ -8685,7 +8681,7 @@
             resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds);
         }
 
-        applySizeOverrideIfNeeded(newParentConfiguration, resolvedConfig);
+        applySizeOverrideIfNeeded(newParentConfiguration, parentWindowingMode, resolvedConfig);
         mResolveConfigHint.resetTmpOverrides();
 
         logAppCompatState();
@@ -8708,15 +8704,85 @@
      * TODO: Consider integrate this with computeConfigByResolveHint()
      */
     private void applySizeOverrideIfNeeded(Configuration newParentConfiguration,
-            Configuration inOutConfig) {
-        applySizeOverride(
-                mDisplayContent,
-                info.applicationInfo,
-                newParentConfiguration,
-                inOutConfig,
-                mOptOutEdgeToEdge,
-                hasFixedRotationTransform(),
-                getCompatDisplayInsets() != null);
+            int parentWindowingMode, Configuration inOutConfig) {
+        if (mDisplayContent == null) {
+            return;
+        }
+        final Rect parentBounds = newParentConfiguration.windowConfiguration.getBounds();
+        int rotation = newParentConfiguration.windowConfiguration.getRotation();
+        if (rotation == ROTATION_UNDEFINED && !isFixedRotationTransforming()) {
+            rotation = mDisplayContent.getRotation();
+        }
+        if (!mOptOutEdgeToEdge && (!mResolveConfigHint.mUseOverrideInsetsForConfig
+                || getCompatDisplayInsets() != null
+                || (isFloating(parentWindowingMode)
+                        // Check the requested windowing mode of activity as well in case it is
+                        // switching between PiP and fullscreen.
+                        && (inOutConfig.windowConfiguration.getWindowingMode()
+                                == WINDOWING_MODE_UNDEFINED
+                                || isFloating(inOutConfig.windowConfiguration.getWindowingMode())))
+                || rotation == ROTATION_UNDEFINED)) {
+            // If the insets configuration decoupled logic is not enabled for the app, or the app
+            // already has a compat override, or the context doesn't contain enough info to
+            // calculate the override, skip the override.
+            return;
+        }
+        // Make sure the orientation related fields will be updated by the override insets, because
+        // fixed rotation has assigned the fields from display's configuration.
+        if (hasFixedRotationTransform()) {
+            inOutConfig.windowConfiguration.setAppBounds(null);
+            inOutConfig.screenWidthDp = Configuration.SCREEN_WIDTH_DP_UNDEFINED;
+            inOutConfig.screenHeightDp = Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
+            inOutConfig.smallestScreenWidthDp = Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
+            inOutConfig.orientation = ORIENTATION_UNDEFINED;
+        }
+
+        // Override starts here.
+        final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
+        final int dw = rotated ? mDisplayContent.mBaseDisplayHeight
+                : mDisplayContent.mBaseDisplayWidth;
+        final int dh = rotated ? mDisplayContent.mBaseDisplayWidth
+                : mDisplayContent.mBaseDisplayHeight;
+        final Rect nonDecorInsets = mDisplayContent.getDisplayPolicy()
+                .getDecorInsetsInfo(rotation, dw, dh).mOverrideNonDecorInsets;
+        // This should be the only place override the configuration for ActivityRecord. Override
+        // the value if not calculated yet.
+        Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
+        if (outAppBounds == null || outAppBounds.isEmpty()) {
+            inOutConfig.windowConfiguration.setAppBounds(parentBounds);
+            outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
+            outAppBounds.inset(nonDecorInsets);
+        }
+        float density = inOutConfig.densityDpi;
+        if (density == Configuration.DENSITY_DPI_UNDEFINED) {
+            density = newParentConfiguration.densityDpi;
+        }
+        density *= DisplayMetrics.DENSITY_DEFAULT_SCALE;
+        if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
+            inOutConfig.screenWidthDp = (int) (outAppBounds.width() / density + 0.5f);
+        }
+        if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
+            inOutConfig.screenHeightDp = (int) (outAppBounds.height() / density + 0.5f);
+        }
+        if (inOutConfig.smallestScreenWidthDp
+                == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED
+                && parentWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+            // For the case of PIP transition and multi-window environment, the
+            // smallestScreenWidthDp is handled already. Override only if the app is in
+            // fullscreen.
+            final DisplayInfo info = new DisplayInfo(mDisplayContent.getDisplayInfo());
+            mDisplayContent.computeSizeRanges(info, rotated, dw, dh,
+                    mDisplayContent.getDisplayMetrics().density,
+                    inOutConfig, true /* overrideConfig */);
+        }
+
+        // It's possible that screen size will be considered in different orientation with or
+        // without considering the system bar insets. Override orientation as well.
+        if (inOutConfig.orientation == ORIENTATION_UNDEFINED) {
+            inOutConfig.orientation =
+                    (inOutConfig.screenWidthDp <= inOutConfig.screenHeightDp)
+                            ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
+        }
     }
 
     private void computeConfigByResolveHint(@NonNull Configuration resolvedConfig,
@@ -9952,16 +10018,6 @@
         return updateReportedConfigurationAndSend();
     }
 
-    /**
-     * @return {@code true} if the Camera is active for the current activity
-     */
-    boolean isCameraActive() {
-        return mDisplayContent != null
-                && mDisplayContent.getDisplayRotationCompatPolicy() != null
-                && mDisplayContent.getDisplayRotationCompatPolicy()
-                    .isCameraActive(this, /* mustBeFullscreen */ true);
-    }
-
     boolean updateReportedConfigurationAndSend() {
         if (isConfigurationDispatchPaused()) {
             Slog.wtf(TAG, "trying to update reported(client) config while dispatch is paused");
@@ -10109,11 +10165,10 @@
 
     private void notifyActivityRefresherAboutConfigurationChange(
             Configuration newConfig, Configuration lastReportedConfig) {
-        if (mDisplayContent.mActivityRefresher == null
-                || !shouldBeResumed(/* activeActivity */ null)) {
+        if (!shouldBeResumed(/* activeActivity */ null)) {
             return;
         }
-        mDisplayContent.mActivityRefresher.onActivityConfigurationChanging(
+        mDisplayContent.mAppCompatCameraPolicy.onActivityConfigurationChanging(
                 this, newConfig, lastReportedConfig);
     }
 
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
index c0e5005..0d108e1 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
@@ -97,8 +97,7 @@
      * </ul>
      */
     boolean shouldOverrideMinAspectRatioForCamera() {
-        return mActivityRecord.isCameraActive()
-                && mAllowMinAspectRatioOverrideOptProp
+        return isCameraActive() && mAllowMinAspectRatioOverrideOptProp
                 .shouldEnableWithOptInOverrideAndOptOutProperty(
                         isCompatChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA));
     }
@@ -174,6 +173,15 @@
     }
 
     /**
+     * @return {@code true} if the Camera is active for the current activity
+     */
+    boolean isCameraActive() {
+        return mActivityRecord.mDisplayContent != null
+                && mActivityRecord.mDisplayContent.mAppCompatCameraPolicy
+                    .isCameraActive(mActivityRecord, /* mustBeFullscreen */ true);
+    }
+
+    /**
      * @return {@code true} if the configuration needs to be recomputed after a camera state update.
      */
     boolean shouldRecomputeConfigurationForCameraCompat() {
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
index ee523a2..53729a2 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -16,34 +16,158 @@
 
 package com.android.server.wm;
 
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.ActivityInfo.ScreenOrientation;
+import android.content.res.Configuration;
+import android.widget.Toast;
+
+import com.android.window.flags.Flags;
 
 /**
- * Encapsulate the app compat logic related to camera.
+ * Encapsulate policy logic related to app compat display rotation.
  */
 class AppCompatCameraPolicy {
 
-    private static final String TAG = TAG_WITH_CLASS_NAME
-            ? "AppCompatCameraPolicy" : TAG_ATM;
+    @Nullable
+    private final CameraStateMonitor mCameraStateMonitor;
+    @Nullable
+    private final ActivityRefresher mActivityRefresher;
+    @Nullable
+    final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
+    @Nullable
+    final CameraCompatFreeformPolicy mCameraCompatFreeformPolicy;
 
-    @NonNull
-    private final ActivityRecord mActivityRecord;
-
-    @NonNull
-    private final AppCompatCameraOverrides mAppCompatCameraOverrides;
-
-    AppCompatCameraPolicy(@NonNull ActivityRecord activityRecord,
-            @NonNull AppCompatCameraOverrides appCompatCameraOverrides) {
-        mActivityRecord = activityRecord;
-        mAppCompatCameraOverrides = appCompatCameraOverrides;
-    }
-
-    void recomputeConfigurationForCameraCompatIfNeeded() {
-        if (mAppCompatCameraOverrides.shouldRecomputeConfigurationForCameraCompat()) {
-            mActivityRecord.recomputeConfiguration();
+    AppCompatCameraPolicy(@NonNull WindowManagerService wmService,
+            @NonNull DisplayContent displayContent) {
+        // Not checking DeviceConfig value here to allow enabling via DeviceConfig
+        // without the need to restart the device.
+        final boolean needsDisplayRotationCompatPolicy =
+                wmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabledAtBuildTime();
+        final boolean needsCameraCompatFreeformPolicy = Flags.cameraCompatForFreeform()
+                && DesktopModeLaunchParamsModifier.canEnterDesktopMode(wmService.mContext);
+        if (needsDisplayRotationCompatPolicy || needsCameraCompatFreeformPolicy) {
+            mCameraStateMonitor = new CameraStateMonitor(displayContent, wmService.mH);
+            mActivityRefresher = new ActivityRefresher(wmService, wmService.mH);
+            mDisplayRotationCompatPolicy =
+                    needsDisplayRotationCompatPolicy ? new DisplayRotationCompatPolicy(
+                            displayContent, mCameraStateMonitor, mActivityRefresher) : null;
+            mCameraCompatFreeformPolicy =
+                    needsCameraCompatFreeformPolicy ? new CameraCompatFreeformPolicy(displayContent,
+                            mCameraStateMonitor, mActivityRefresher) : null;
+        } else {
+            mDisplayRotationCompatPolicy = null;
+            mCameraCompatFreeformPolicy = null;
+            mCameraStateMonitor = null;
+            mActivityRefresher = null;
         }
     }
+
+    void onActivityRefreshed(@NonNull ActivityRecord activity) {
+        if (mActivityRefresher != null) {
+            mActivityRefresher.onActivityRefreshed(activity);
+        }
+    }
+
+    /**
+     * "Refreshes" activity by going through "stopped -> resumed" or "paused -> resumed" cycle.
+     * This allows to clear cached values in apps (e.g. display or camera rotation) that influence
+     * camera preview and can lead to sideways or stretching issues persisting even after force
+     * rotation.
+     */
+    void onActivityConfigurationChanging(@NonNull ActivityRecord activity,
+            @NonNull Configuration newConfig, @NonNull Configuration lastReportedConfig) {
+        if (mActivityRefresher != null) {
+            mActivityRefresher.onActivityConfigurationChanging(activity, newConfig,
+                    lastReportedConfig);
+        }
+    }
+
+    /**
+     * Notifies that animation in {@link ScreenRotationAnimation} has finished.
+     *
+     * <p>This class uses this signal as a trigger for notifying the user about forced rotation
+     * reason with the {@link Toast}.
+     */
+    void onScreenRotationAnimationFinished() {
+        if (mDisplayRotationCompatPolicy != null) {
+            mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+        }
+    }
+
+    boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) {
+        if (mDisplayRotationCompatPolicy != null) {
+            return mDisplayRotationCompatPolicy.isActivityEligibleForOrientationOverride(activity);
+        }
+        return false;
+    }
+
+    /**
+     * Whether camera compat treatment is applicable for the given activity.
+     *
+     * <p>Conditions that need to be met:
+     * <ul>
+     *     <li>Camera is active for the package.
+     *     <li>The activity is in fullscreen
+     *     <li>The activity has fixed orientation but not "locked" or "nosensor" one.
+     * </ul>
+     */
+    boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
+        if (mDisplayRotationCompatPolicy != null) {
+            return mDisplayRotationCompatPolicy.isTreatmentEnabledForActivity(activity);
+        }
+        return false;
+    }
+
+    void start() {
+        if (mCameraCompatFreeformPolicy != null) {
+            mCameraCompatFreeformPolicy.start();
+        }
+        if (mCameraStateMonitor != null) {
+            mCameraStateMonitor.startListeningToCameraState();
+        }
+    }
+
+    void dispose() {
+        if (mDisplayRotationCompatPolicy != null) {
+            mDisplayRotationCompatPolicy.dispose();
+        }
+        if (mCameraCompatFreeformPolicy != null) {
+            mCameraCompatFreeformPolicy.dispose();
+        }
+        if (mCameraStateMonitor != null) {
+            mCameraStateMonitor.dispose();
+        }
+    }
+
+    boolean hasDisplayRotationCompatPolicy() {
+        return mDisplayRotationCompatPolicy != null;
+    }
+
+    boolean hasCameraCompatFreeformPolicy() {
+        return mCameraCompatFreeformPolicy != null;
+    }
+
+    @ScreenOrientation
+    int getOrientation() {
+        return mDisplayRotationCompatPolicy != null
+                ? mDisplayRotationCompatPolicy.getOrientation()
+                : SCREEN_ORIENTATION_UNSPECIFIED;
+    }
+
+    boolean isCameraActive(@NonNull ActivityRecord activity, boolean mustBeFullscreen) {
+        return mDisplayRotationCompatPolicy != null
+                && mDisplayRotationCompatPolicy.isCameraActive(activity, mustBeFullscreen);
+    }
+
+    @Nullable
+    String getSummaryForDisplayRotationHistoryRecord() {
+        if (mDisplayRotationCompatPolicy != null) {
+            return mDisplayRotationCompatPolicy.getSummaryForDisplayRotationHistoryRecord();
+        }
+        return null;
+    }
+
 }
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index d8c0c17..16d3787 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -16,6 +16,7 @@
 package com.android.server.wm;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.pm.PackageManager;
 
 import com.android.server.wm.utils.OptPropFactory;
@@ -26,16 +27,17 @@
 class AppCompatController {
 
     @NonNull
+    private final ActivityRecord mActivityRecord;
+    @NonNull
     private final TransparentPolicy mTransparentPolicy;
     @NonNull
     private final AppCompatOrientationPolicy mOrientationPolicy;
     @NonNull
     private final AppCompatOverrides mAppCompatOverrides;
-    @NonNull
-    private final AppCompatCameraPolicy mAppCompatCameraPolicy;
 
     AppCompatController(@NonNull WindowManagerService wmService,
                         @NonNull ActivityRecord activityRecord) {
+        mActivityRecord = activityRecord;
         final PackageManager packageManager = wmService.mContext.getPackageManager();
         final OptPropFactory optPropBuilder = new OptPropFactory(packageManager,
                 activityRecord.packageName);
@@ -49,8 +51,6 @@
                 mAppCompatOverrides, tmpController::shouldApplyUserFullscreenOverride,
                 tmpController::shouldApplyUserMinAspectRatioOverride,
                 tmpController::isSystemOverrideToFullscreenEnabled);
-        mAppCompatCameraPolicy = new AppCompatCameraPolicy(activityRecord,
-                mAppCompatOverrides.getAppCompatCameraOverrides());
     }
 
     @NonNull
@@ -64,11 +64,6 @@
     }
 
     @NonNull
-    AppCompatCameraPolicy getAppCompatCameraPolicy() {
-        return mAppCompatCameraPolicy;
-    }
-
-    @NonNull
     AppCompatOverrides getAppCompatOverrides() {
         return mAppCompatOverrides;
     }
@@ -82,4 +77,12 @@
     AppCompatCameraOverrides getAppCompatCameraOverrides() {
         return mAppCompatOverrides.getAppCompatCameraOverrides();
     }
+
+    @Nullable
+    AppCompatCameraPolicy getAppCompatCameraPolicy() {
+        if (mActivityRecord.mDisplayContent != null) {
+            return mActivityRecord.mDisplayContent.mAppCompatCameraPolicy;
+        }
+        return null;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java
index b0fdbb5..155e246 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java
@@ -22,7 +22,6 @@
 import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
 import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
 import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
-import static android.content.pm.ActivityInfo.screenOrientationToString;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
 import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 
@@ -31,8 +30,6 @@
 import static com.android.server.wm.AppCompatUtils.asLazy;
 
 import android.annotation.NonNull;
-import android.content.pm.ActivityInfo;
-import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.wm.utils.OptPropFactory;
@@ -51,6 +48,8 @@
 
     @NonNull
     private final ActivityRecord mActivityRecord;
+    @NonNull
+    private final AppCompatCameraOverrides mAppCompatCameraOverrides;
 
     @NonNull
     private final OptPropFactory.OptProp mIgnoreRequestedOrientationOptProp;
@@ -62,8 +61,10 @@
 
     AppCompatOrientationOverrides(@NonNull ActivityRecord activityRecord,
             @NonNull LetterboxConfiguration letterboxConfiguration,
-            @NonNull OptPropFactory optPropBuilder) {
+            @NonNull OptPropFactory optPropBuilder,
+            @NonNull AppCompatCameraOverrides appCompatCameraOverrides) {
         mActivityRecord = activityRecord;
+        mAppCompatCameraOverrides = appCompatCameraOverrides;
         mOrientationOverridesState = new OrientationOverridesState(mActivityRecord,
                 System::currentTimeMillis);
         final BooleanSupplier isPolicyForIgnoringRequestedOrientationEnabled = asLazy(
@@ -76,59 +77,9 @@
                 isPolicyForIgnoringRequestedOrientationEnabled);
     }
 
-    /**
-     * Whether should ignore app requested orientation in response to an app
-     * calling {@link android.app.Activity#setRequestedOrientation}.
-     *
-     * <p>This is needed to avoid getting into {@link android.app.Activity#setRequestedOrientation}
-     * loop when {@link DisplayContent#getIgnoreOrientationRequest} is enabled or device has
-     * landscape natural orientation which app developers don't expect. For example, the loop can
-     * look like this:
-     * <ol>
-     *     <li>App sets default orientation to "unspecified" at runtime
-     *     <li>App requests to "portrait" after checking some condition (e.g. display rotation).
-     *     <li>(2) leads to fullscreen -> letterboxed bounds change and activity relaunch because
-     *     app can't handle the corresponding config changes.
-     *     <li>Loop goes back to (1)
-     * </ol>
-     *
-     * <p>This treatment is enabled when the following conditions are met:
-     * <ul>
-     *     <li>Flag gating the treatment is enabled
-     *     <li>Opt-out component property isn't enabled
-     *     <li>Opt-in component property or per-app override are enabled
-     *     <li>Activity is relaunched after {@link android.app.Activity#setRequestedOrientation}
-     *     call from an app or camera compat force rotation treatment is active for the activity.
-     *     <li>Orientation request loop detected and is not letterboxed for fixed orientation
-     * </ul>
-     */
-    boolean shouldIgnoreRequestedOrientation(
-            @ActivityInfo.ScreenOrientation int requestedOrientation) {
-        if (mIgnoreRequestedOrientationOptProp.shouldEnableWithOverrideAndProperty(
-                isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION))) {
-            if (mOrientationOverridesState.mIsRelaunchingAfterRequestedOrientationChanged) {
-                Slog.w(TAG, "Ignoring orientation update to "
-                        + screenOrientationToString(requestedOrientation)
-                        + " due to relaunching after setRequestedOrientation for "
-                        + mActivityRecord);
-                return true;
-            }
-            if (isCameraCompatTreatmentActive()) {
-                Slog.w(TAG, "Ignoring orientation update to "
-                        + screenOrientationToString(requestedOrientation)
-                        + " due to camera compat treatment for " + mActivityRecord);
-                return true;
-            }
-        }
-
-        if (shouldIgnoreOrientationRequestLoop()) {
-            Slog.w(TAG, "Ignoring orientation update to "
-                    + screenOrientationToString(requestedOrientation)
-                    + " as orientation request loop was detected for "
-                    + mActivityRecord);
-            return true;
-        }
-        return false;
+    boolean shouldEnableIgnoreOrientationRequest() {
+        return mIgnoreRequestedOrientationOptProp.shouldEnableWithOverrideAndProperty(
+                isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION));
     }
 
     /**
@@ -183,20 +134,6 @@
         return mActivityRecord.info.isChangeEnabled(overrideChangeId);
     }
 
-    /**
-     * @return {@code true} if the App Compat Camera Policy is active for the current activity.
-     */
-    // TODO(b/346253439): Remove after defining dependency with Camera capabilities.
-    private boolean isCameraCompatTreatmentActive() {
-        DisplayContent displayContent = mActivityRecord.mDisplayContent;
-        if (displayContent == null) {
-            return false;
-        }
-        return displayContent.mDisplayRotationCompatPolicy != null
-                && displayContent.mDisplayRotationCompatPolicy
-                .isTreatmentEnabledForActivity(mActivityRecord);
-    }
-
     static class OrientationOverridesState {
         // Corresponds to OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR
         final boolean mIsOverrideToNosensorOrientationEnabled;
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
index 960ef5a..69ba59b 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
@@ -46,7 +46,6 @@
 
     @NonNull
     private final AppCompatOverrides mAppCompatOverrides;
-
     @NonNull
     private final BooleanSupplier mShouldApplyUserFullscreenOverride;
     @NonNull
@@ -78,7 +77,7 @@
                 // often results in sideways or stretched previews. As the camera compat treatment
                 // targets fixed-orientation activities, overriding the orientation disables the
                 // treatment.
-                && !mActivityRecord.isCameraActive()) {
+                && !mAppCompatOverrides.getAppCompatCameraOverrides().isCameraActive()) {
             Slog.v(TAG, "Requested orientation " + screenOrientationToString(candidate)
                     + " for " + mActivityRecord + " is overridden to "
                     + screenOrientationToString(SCREEN_ORIENTATION_USER)
@@ -103,11 +102,11 @@
             return candidate;
         }
 
-        if (displayContent != null && mAppCompatOverrides.getAppCompatCameraOverrides()
-                .isOverrideOrientationOnlyForCameraEnabled()
-                    && (displayContent.mDisplayRotationCompatPolicy == null
-                    || !displayContent.mDisplayRotationCompatPolicy
-                        .isActivityEligibleForOrientationOverride(mActivityRecord))) {
+        if (displayContent != null
+                && mAppCompatOverrides.getAppCompatCameraOverrides()
+                    .isOverrideOrientationOnlyForCameraEnabled()
+                && !displayContent.mAppCompatCameraPolicy
+                    .isActivityEligibleForOrientationOverride(mActivityRecord)) {
             return candidate;
         }
 
@@ -120,7 +119,7 @@
                 // often results in sideways or stretched previews. As the camera compat treatment
                 // targets fixed-orientation activities, overriding the orientation disables the
                 // treatment.
-                && !mActivityRecord.isCameraActive()) {
+                && !mAppCompatOverrides.getAppCompatCameraOverrides().isCameraActive()) {
             Slog.v(TAG, "Requested orientation  " + screenOrientationToString(candidate)
                     + " for " + mActivityRecord + " is overridden to "
                     + screenOrientationToString(SCREEN_ORIENTATION_USER));
@@ -161,4 +160,62 @@
         return candidate;
     }
 
+    /**
+     * Whether should ignore app requested orientation in response to an app
+     * calling {@link android.app.Activity#setRequestedOrientation}.
+     *
+     * <p>This is needed to avoid getting into {@link android.app.Activity#setRequestedOrientation}
+     * loop when {@link DisplayContent#getIgnoreOrientationRequest} is enabled or device has
+     * landscape natural orientation which app developers don't expect. For example, the loop can
+     * look like this:
+     * <ol>
+     *     <li>App sets default orientation to "unspecified" at runtime
+     *     <li>App requests to "portrait" after checking some condition (e.g. display rotation).
+     *     <li>(2) leads to fullscreen -> letterboxed bounds change and activity relaunch because
+     *     app can't handle the corresponding config changes.
+     *     <li>Loop goes back to (1)
+     * </ol>
+     *
+     * <p>This treatment is enabled when the following conditions are met:
+     * <ul>
+     *     <li>Flag gating the treatment is enabled
+     *     <li>Opt-out component property isn't enabled
+     *     <li>Opt-in component property or per-app override are enabled
+     *     <li>Activity is relaunched after {@link android.app.Activity#setRequestedOrientation}
+     *     call from an app or camera compat force rotation treatment is active for the activity.
+     *     <li>Orientation request loop detected and is not letterboxed for fixed orientation
+     * </ul>
+     */
+    boolean shouldIgnoreRequestedOrientation(
+            @ActivityInfo.ScreenOrientation int requestedOrientation) {
+        final AppCompatOrientationOverrides orientationOverrides =
+                mAppCompatOverrides.getAppCompatOrientationOverrides();
+        if (orientationOverrides.shouldEnableIgnoreOrientationRequest()) {
+            if (orientationOverrides.getIsRelaunchingAfterRequestedOrientationChanged()) {
+                Slog.w(TAG, "Ignoring orientation update to "
+                        + screenOrientationToString(requestedOrientation)
+                        + " due to relaunching after setRequestedOrientation for "
+                        + mActivityRecord);
+                return true;
+            }
+            final AppCompatCameraPolicy cameraPolicy = mActivityRecord.mAppCompatController
+                    .getAppCompatCameraPolicy();
+            if (cameraPolicy != null
+                    && cameraPolicy.isTreatmentEnabledForActivity(mActivityRecord)) {
+                Slog.w(TAG, "Ignoring orientation update to "
+                        + screenOrientationToString(requestedOrientation)
+                        + " due to camera compat treatment for " + mActivityRecord);
+                return true;
+            }
+        }
+        if (orientationOverrides.shouldIgnoreOrientationRequestLoop()) {
+            Slog.w(TAG, "Ignoring orientation update to "
+                    + screenOrientationToString(requestedOrientation)
+                    + " as orientation request loop was detected for "
+                    + mActivityRecord);
+            return true;
+        }
+        return false;
+    }
+
 }
diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java
index c20da7c..94c6ba9 100644
--- a/services/core/java/com/android/server/wm/AppCompatOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java
@@ -79,10 +79,10 @@
         mLetterboxConfiguration = letterboxConfiguration;
         mActivityRecord = activityRecord;
 
-        mAppCompatOrientationOverrides = new AppCompatOrientationOverrides(mActivityRecord,
-                mLetterboxConfiguration, optPropBuilder);
         mAppCompatCameraOverrides = new AppCompatCameraOverrides(mActivityRecord,
                 mLetterboxConfiguration, optPropBuilder);
+        mAppCompatOrientationOverrides = new AppCompatOrientationOverrides(mActivityRecord,
+                mLetterboxConfiguration, optPropBuilder, mAppCompatCameraOverrides);
 
         mFakeFocusOptProp = optPropBuilder.create(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS,
                 mLetterboxConfiguration::isCompatFakeFocusEnabled);
@@ -113,19 +113,6 @@
                 mLetterboxConfiguration::isUserAppAspectRatioFullscreenEnabled);
     }
 
-    /**
-     * @return {@code true} if the App Compat Camera Policy is active for the current activity.
-     */
-    boolean isCameraCompatTreatmentActive() {
-        final DisplayContent displayContent = mActivityRecord.mDisplayContent;
-        if (displayContent == null) {
-            return false;
-        }
-        return displayContent.mDisplayRotationCompatPolicy != null
-                && displayContent.mDisplayRotationCompatPolicy
-                    .isTreatmentEnabledForActivity(mActivityRecord);
-    }
-
     @NonNull
     AppCompatOrientationOverrides getAppCompatOrientationOverrides() {
         return mAppCompatOrientationOverrides;
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 14e256f..8421765 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -210,6 +210,9 @@
                             + "topRunningActivity=%s, callbackInfo=%s, currentFocus=%s",
                     currentTask, currentActivity, callbackInfo, window);
 
+            // Clear the pointer down outside focus if any.
+            mWindowManagerService.clearPointerDownOutsideFocusRunnable();
+
             // If we don't need to set up the animation, we return early. This is the case when
             // - We have an application callback.
             // - We don't have any ActivityRecord or Task to animate.
@@ -1361,6 +1364,8 @@
                                     synchronized (openTask.mWmService.mGlobalLock) {
                                         if (mRequestedStartingSurfaceId != INVALID_TASK_ID) {
                                             mStartingSurface = sc;
+                                        } else {
+                                            sc.release();
                                         }
                                     }
                                 }
@@ -1599,12 +1604,20 @@
                     @NonNull ActivityRecord[] visibleOpenActivities) {
                 boolean needsLaunchBehind = true;
                 if (isSupportWindowlessSurface() && mShowWindowlessSurface && !mIsLaunchBehind) {
+                    boolean activitiesAreDrawn = false;
+                    for (int i = visibleOpenActivities.length - 1; i >= 0; --i) {
+                        // If the activity hasn't stopped, it's window should remain drawn.
+                        activitiesAreDrawn |= visibleOpenActivities[i].firstWindowDrawn;
+                    }
                     final WindowContainer mainOpen = openAnimationAdaptor.mAdaptors[0].mTarget;
                     final TaskSnapshot snapshot = getSnapshot(mainOpen, visibleOpenActivities);
-                    openAnimationAdaptor.createStartingSurface(snapshot);
-                    // set LaunchBehind if we are creating splash screen surface.
-                    needsLaunchBehind = snapshot == null
-                            && openAnimationAdaptor.mRequestedStartingSurfaceId != INVALID_TASK_ID;
+                    // Don't create starting surface if previous activities haven't stopped or
+                    // the snapshot does not exist.
+                    if (snapshot != null || !activitiesAreDrawn) {
+                        openAnimationAdaptor.createStartingSurface(snapshot);
+                    }
+                    // Only use LaunchBehind if snapshot does not exist.
+                    needsLaunchBehind = snapshot == null;
                 }
                 if (needsLaunchBehind) {
                     for (int i = visibleOpenActivities.length - 1; i >= 0; --i) {
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index 70e6d5d..efd5202 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -22,23 +22,14 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.app.WindowConfiguration.activityTypeToString;
-import static android.app.WindowConfiguration.isFloating;
 import static android.app.WindowConfiguration.windowingModeToString;
 import static android.app.WindowConfigurationProto.WINDOWING_MODE;
 import static android.content.ConfigurationProto.WINDOW_CONFIGURATION;
-import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED;
-import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION;
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
-import static android.view.Surface.ROTATION_270;
-import static android.view.Surface.ROTATION_90;
 
 import static com.android.server.wm.ConfigurationContainerProto.FULL_CONFIGURATION;
 import static com.android.server.wm.ConfigurationContainerProto.MERGED_OVERRIDE_CONFIGURATION;
@@ -47,14 +38,11 @@
 import android.annotation.CallSuper;
 import android.annotation.NonNull;
 import android.app.WindowConfiguration;
-import android.content.pm.ApplicationInfo;
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.LocaleList;
-import android.util.DisplayMetrics;
 import android.util.proto.ProtoOutputStream;
-import android.view.DisplayInfo;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -536,133 +524,22 @@
     }
 
     /**
-     * @see ActivityRecord#applySizeOverrideIfNeeded
-     */
-    public static boolean applySizeOverride(DisplayContent displayContent, ApplicationInfo appInfo,
-            Configuration newParentConfiguration, Configuration inOutConfig,
-            boolean optOutEdgeToEdge, boolean hasFixedRotationTransform,
-            boolean hasCompatDisplayInsets) {
-        if (displayContent == null) {
-            return false;
-        }
-        final boolean useOverrideInsetsForConfig =
-                displayContent.mWmService.mFlags.mInsetsDecoupledConfiguration
-                        ? !appInfo.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED)
-                                && !appInfo.isChangeEnabled(
-                                        OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION)
-                        : appInfo.isChangeEnabled(OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION);
-        if (newParentConfiguration == null) {
-            newParentConfiguration = displayContent.getConfiguration();
-        }
-        final int parentWindowingMode =
-                newParentConfiguration.windowConfiguration.getWindowingMode();
-        final boolean isFloating = isFloating(parentWindowingMode)
-                // Check the requested windowing mode of activity as well in case it is
-                // switching between PiP and fullscreen.
-                && (inOutConfig.windowConfiguration.getWindowingMode() == WINDOWING_MODE_UNDEFINED
-                        || isFloating(inOutConfig.windowConfiguration.getWindowingMode()));
-        final Rect parentBounds = newParentConfiguration.windowConfiguration.getBounds();
-        int rotation = newParentConfiguration.windowConfiguration.getRotation();
-        if (rotation == ROTATION_UNDEFINED && !hasFixedRotationTransform) {
-            rotation = displayContent.getRotation();
-        }
-        if (!optOutEdgeToEdge && (!useOverrideInsetsForConfig
-                || hasCompatDisplayInsets
-                || isFloating
-                || rotation == ROTATION_UNDEFINED)) {
-            // If the insets configuration decoupled logic is not enabled for the app, or the app
-            // already has a compat override, or the context doesn't contain enough info to
-            // calculate the override, skip the override.
-            return false;
-        }
-        // Make sure the orientation related fields will be updated by the override insets, because
-        // fixed rotation has assigned the fields from display's configuration.
-        if (hasFixedRotationTransform) {
-            inOutConfig.windowConfiguration.setAppBounds(null);
-            inOutConfig.screenWidthDp = Configuration.SCREEN_WIDTH_DP_UNDEFINED;
-            inOutConfig.screenHeightDp = Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
-            inOutConfig.smallestScreenWidthDp = Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
-            inOutConfig.orientation = ORIENTATION_UNDEFINED;
-        }
-
-        // Override starts here.
-        final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
-        final int dw = rotated
-                ? displayContent.mBaseDisplayHeight
-                : displayContent.mBaseDisplayWidth;
-        final int dh = rotated
-                ? displayContent.mBaseDisplayWidth
-                : displayContent.mBaseDisplayHeight;
-        final Rect nonDecorFrame = displayContent.getDisplayPolicy()
-                .getDecorInsetsInfo(rotation, dw, dh).mOverrideNonDecorFrame;
-        // This should be the only place override the configuration for ActivityRecord. Override
-        // the value if not calculated yet.
-        Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
-        if (outAppBounds == null || outAppBounds.isEmpty()) {
-            inOutConfig.windowConfiguration.setAppBounds(parentBounds);
-            outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
-            outAppBounds.intersect(nonDecorFrame);
-        }
-        float density = inOutConfig.densityDpi;
-        if (density == Configuration.DENSITY_DPI_UNDEFINED) {
-            density = newParentConfiguration.densityDpi;
-        }
-        density *= DisplayMetrics.DENSITY_DEFAULT_SCALE;
-        if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
-            inOutConfig.screenWidthDp = (int) (outAppBounds.width() / density + 0.5f);
-        }
-        if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
-            inOutConfig.screenHeightDp = (int) (outAppBounds.height() / density + 0.5f);
-        }
-        if (inOutConfig.smallestScreenWidthDp == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED
-                && parentWindowingMode == WINDOWING_MODE_FULLSCREEN) {
-            // For the case of PIP transition and multi-window environment, the
-            // smallestScreenWidthDp is handled already. Override only if the app is in
-            // fullscreen.
-            final DisplayInfo info = new DisplayInfo(displayContent.getDisplayInfo());
-            displayContent.computeSizeRanges(info, rotated, dw, dh,
-                    displayContent.getDisplayMetrics().density,
-                    inOutConfig, true /* overrideConfig */);
-        }
-
-        // It's possible that screen size will be considered in different orientation with or
-        // without considering the system bar insets. Override orientation as well.
-        if (inOutConfig.orientation == ORIENTATION_UNDEFINED) {
-            inOutConfig.orientation =
-                    (inOutConfig.screenWidthDp <= inOutConfig.screenHeightDp)
-                            ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
-        }
-        return true;
-    }
-
-    /**
-     * Gives the derived class a chance to apply the app-specific configuration.
-     *
-     * @param inOutConfig the configuration as the requested configuration.
-     * @return true if any of the given configuration has been updated.
-     */
-    public boolean onApplyAppSpecificConfig(Configuration inOutConfig) {
-        return false;
-    }
-
-    /**
      * Applies app-specific nightMode and {@link LocaleList} on requested configuration.
      * @return true if any of the requested configuration has been updated.
      */
     public boolean applyAppSpecificConfig(Integer nightMode, LocaleList locales,
             @Configuration.GrammaticalGender Integer gender) {
         mRequestsTmpConfig.setTo(getRequestedOverrideConfiguration());
-        boolean changed = onApplyAppSpecificConfig(mRequestsTmpConfig);
         boolean newNightModeSet = (nightMode != null) && setOverrideNightMode(mRequestsTmpConfig,
                 nightMode);
         boolean newLocalesSet = (locales != null) && setOverrideLocales(mRequestsTmpConfig,
                 locales);
         boolean newGenderSet = setOverrideGender(mRequestsTmpConfig,
                 gender == null ? Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED : gender);
-        if (changed || newNightModeSet || newLocalesSet || newGenderSet) {
+        if (newNightModeSet || newLocalesSet || newGenderSet) {
             onRequestedOverrideConfigurationChanged(mRequestsTmpConfig);
         }
-        return changed || newNightModeSet || newLocalesSet || newGenderSet;
+        return newNightModeSet || newLocalesSet || newGenderSet;
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index b5b9377..a8aa0ba 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -263,7 +263,6 @@
 import com.android.server.wm.utils.RegionUtils;
 import com.android.server.wm.utils.RotationCache;
 import com.android.server.wm.utils.WmDisplayCutout;
-import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -475,14 +474,8 @@
     private final DisplayPolicy mDisplayPolicy;
     private final DisplayRotation mDisplayRotation;
 
-    @Nullable
-    final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
-    @Nullable
-    final CameraCompatFreeformPolicy mCameraCompatFreeformPolicy;
-    @Nullable
-    final CameraStateMonitor mCameraStateMonitor;
-    @Nullable
-    final ActivityRefresher mActivityRefresher;
+    @NonNull
+    AppCompatCameraPolicy mAppCompatCameraPolicy;
 
     DisplayFrames mDisplayFrames;
     final DisplayUpdater mDisplayUpdater;
@@ -1191,6 +1184,7 @@
 
         mDeviceStateController = deviceStateController;
 
+        mAppCompatCameraPolicy = new AppCompatCameraPolicy(mWmService, this);
         mDisplayPolicy = new DisplayPolicy(mWmService, this);
         mDisplayRotation = new DisplayRotation(mWmService, this, mDisplayInfo.address,
                 mDeviceStateController, root.getDisplayRotationCoordinator());
@@ -1231,40 +1225,6 @@
         onDisplayChanged(this);
         updateDisplayAreaOrganizers();
 
-        // Not checking DeviceConfig value here to allow enabling via DeviceConfig
-        // without the need to restart the device.
-        final boolean shouldCreateDisplayRotationCompatPolicy =
-                mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabledAtBuildTime();
-        final boolean shouldCreateCameraCompatFreeformPolicy = Flags.cameraCompatForFreeform()
-                && DesktopModeLaunchParamsModifier.canEnterDesktopMode(mWmService.mContext);
-        if (shouldCreateDisplayRotationCompatPolicy || shouldCreateCameraCompatFreeformPolicy) {
-            mCameraStateMonitor = new CameraStateMonitor(this, mWmService.mH);
-            mActivityRefresher = new ActivityRefresher(mWmService, mWmService.mH);
-            if (shouldCreateDisplayRotationCompatPolicy) {
-                mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(this,
-                        mCameraStateMonitor, mActivityRefresher);
-                mDisplayRotationCompatPolicy.start();
-            } else {
-                mDisplayRotationCompatPolicy = null;
-            }
-
-            if (shouldCreateCameraCompatFreeformPolicy) {
-                mCameraCompatFreeformPolicy = new CameraCompatFreeformPolicy(this,
-                        mCameraStateMonitor, mActivityRefresher);
-                mCameraCompatFreeformPolicy.start();
-            } else {
-                mCameraCompatFreeformPolicy = null;
-            }
-
-            mCameraStateMonitor.startListeningToCameraState();
-        } else {
-            // These are to satisfy the `final` check.
-            mCameraStateMonitor = null;
-            mActivityRefresher = null;
-            mDisplayRotationCompatPolicy = null;
-            mCameraCompatFreeformPolicy = null;
-        }
-
         mRotationReversionController = new DisplayRotationReversionController(this);
 
         mInputMonitor = new InputMonitor(mWmService, this);
@@ -1280,6 +1240,7 @@
                 R.bool.config_defaultInTouchMode);
         mWmService.mInputManager.setInTouchMode(mInTouchMode, mWmService.MY_PID, mWmService.MY_UID,
                 /* hasPermission= */ true, mDisplayId);
+        mAppCompatCameraPolicy.start();
     }
 
     private void beginHoldScreenUpdate() {
@@ -1314,15 +1275,6 @@
         }
     }
 
-    /**
-     * @return The {@link DisplayRotationCompatPolicy} for this DisplayContent
-     */
-    // TODO(b/335387481) Allow access to DisplayRotationCompatPolicy only with getters
-    @Nullable
-    DisplayRotationCompatPolicy getDisplayRotationCompatPolicy() {
-        return mDisplayRotationCompatPolicy;
-    }
-
     @Override
     void migrateToNewSurfaceControl(Transaction t) {
         t.remove(mSurfaceControl);
@@ -2889,12 +2841,10 @@
             }
         }
 
-        if (mDisplayRotationCompatPolicy != null) {
-            int compatOrientation = mDisplayRotationCompatPolicy.getOrientation();
-            if (compatOrientation != SCREEN_ORIENTATION_UNSPECIFIED) {
-                mLastOrientationSource = null;
-                return compatOrientation;
-            }
+        final int compatOrientation = mAppCompatCameraPolicy.getOrientation();
+        if (compatOrientation != SCREEN_ORIENTATION_UNSPECIFIED) {
+            mLastOrientationSource = null;
+            return compatOrientation;
         }
 
         final int orientation = super.getOrientation();
@@ -3364,17 +3314,7 @@
         getPendingTransaction().apply();
         mWmService.mWindowPlacerLocked.requestTraversal();
 
-        if (mDisplayRotationCompatPolicy != null) {
-            mDisplayRotationCompatPolicy.dispose();
-        }
-
-        if (mCameraCompatFreeformPolicy != null) {
-            mCameraCompatFreeformPolicy.dispose();
-        }
-
-        if (mCameraStateMonitor != null) {
-            mCameraStateMonitor.dispose();
-        }
+        mAppCompatCameraPolicy.dispose();
     }
 
     /** Returns true if a removal action is still being deferred. */
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index f3ccc3b..c67928a 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -2294,10 +2294,8 @@
                     mInHalfFoldTransition = false;
                     mDeviceState = DeviceStateController.DeviceState.UNKNOWN;
                 }
-                mDisplayRotationCompatPolicySummary = dc.mDisplayRotationCompatPolicy == null
-                        ? null
-                        : dc.mDisplayRotationCompatPolicy
-                                .getSummaryForDisplayRotationHistoryRecord();
+                mDisplayRotationCompatPolicySummary = dc.mAppCompatCameraPolicy
+                        .getSummaryForDisplayRotationHistoryRecord();
                 mRotationReversionSlots =
                         dr.mDisplayContent.getRotationReversionController().getSlotsCopy();
             }
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index 3d71e95..9998e1a 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -299,8 +299,7 @@
         // Checking whether an activity in fullscreen rather than the task as this camera
         // compat treatment doesn't cover activity embedding.
         if (cameraActivity.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
-            cameraActivity.mAppCompatController
-                    .getAppCompatCameraPolicy().recomputeConfigurationForCameraCompatIfNeeded();
+            recomputeConfigurationForCameraCompatIfNeeded(cameraActivity);
             mDisplayContent.updateOrientation();
             return true;
         }
@@ -367,8 +366,7 @@
                 || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
             return true;
         }
-        topActivity.mAppCompatController
-                .getAppCompatCameraPolicy().recomputeConfigurationForCameraCompatIfNeeded();
+        recomputeConfigurationForCameraCompatIfNeeded(topActivity);
         mDisplayContent.updateOrientation();
         return true;
     }
@@ -383,4 +381,12 @@
         }
         return mActivityRefresher.isActivityRefreshing(topActivity);
     }
+
+    private void recomputeConfigurationForCameraCompatIfNeeded(
+            @NonNull ActivityRecord activityRecord) {
+        if (activityRecord.mAppCompatController.getAppCompatCameraOverrides()
+                .shouldRecomputeConfigurationForCameraCompat()) {
+            activityRecord.recomputeConfiguration();
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/DisplayRotationReversionController.java b/services/core/java/com/android/server/wm/DisplayRotationReversionController.java
index f94b8c4..b955738 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationReversionController.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationReversionController.java
@@ -61,7 +61,7 @@
     }
 
     boolean isRotationReversionEnabled() {
-        return mDisplayContent.mDisplayRotationCompatPolicy != null
+        return mDisplayContent.mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy()
                 || mDisplayContent.getDisplayRotation().mFoldController != null
                 || mDisplayContent.getIgnoreOrientationRequest();
     }
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 30f2d0d..6abef8b 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.content.ClipDescription.EXTRA_HIDE_DRAG_SOURCE_TASK_ID;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.view.View.DRAG_FLAG_GLOBAL;
 import static android.view.View.DRAG_FLAG_GLOBAL_SAME_APPLICATION;
@@ -217,6 +218,11 @@
                     mDragState.mToken = dragToken;
                     mDragState.mDisplayContent = displayContent;
                     mDragState.mData = data;
+                    mDragState.mCallingTaskIdToHide = shouldMoveCallingTaskToBack(callingWin,
+                            flags);
+                    if (DEBUG_DRAG) {
+                        Slog.d(TAG_WM, "Calling task to hide=" + mDragState.mCallingTaskIdToHide);
+                    }
 
                     if ((flags & View.DRAG_FLAG_ACCESSIBILITY_ACTION) == 0) {
                         final Display display = displayContent.getDisplay();
@@ -364,6 +370,23 @@
     }
 
     /**
+     * If the calling window's task should be hidden for the duration of the drag, this returns the
+     * task id of the task (or -1 otherwise).
+     */
+    private int shouldMoveCallingTaskToBack(WindowState callingWin, int flags) {
+        if ((flags & View.DRAG_FLAG_HIDE_CALLING_TASK_ON_DRAG_START) == 0) {
+            // Not requested by the app
+            return -1;
+        }
+        final ActivityRecord callingActivity = callingWin.getActivityRecord();
+        if (callingActivity == null || callingActivity.getTask() == null) {
+            // Not an activity
+            return -1;
+        }
+        return callingActivity.getTask().mTaskId;
+    }
+
+    /**
      * Notifies the unhandled drag listener if needed.
      * @return whether the listener was notified and subsequent drag completion should be deferred
      *         until the listener calls back
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 4be5bad..ba74f50 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.content.ClipDescription.EXTRA_HIDE_DRAG_SOURCE_TASK_ID;
 import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY;
 import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT;
 import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK;
@@ -48,6 +49,7 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.IBinder;
+import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -117,6 +119,8 @@
     InputInterceptor mInputInterceptor;
     ArrayList<WindowState> mNotifiedWindows;
     boolean mDragInProgress;
+    // Set to non -1 value if a valid app requests DRAG_FLAG_HIDE_CALLING_TASK_ON_DRAG_START
+    int mCallingTaskIdToHide;
     /**
      * Whether if animation is completed. Needs to be volatile to update from the animation thread
      * without having a WM lock.
@@ -320,12 +324,12 @@
                 }
             }
             final boolean targetInterceptsGlobalDrag = targetInterceptsGlobalDrag(touchedWin);
-            return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mData,
+            return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mDataDescription, mData,
                     /* includeDragSurface= */ targetInterceptsGlobalDrag,
                     /* includeDragFlags= */ targetInterceptsGlobalDrag,
                     dragAndDropPermissions);
         } else {
-            return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mData,
+            return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mDataDescription, mData,
                     /* includeDragSurface= */ includePrivateInfo,
                     /* includeDragFlags= */ includePrivateInfo,
                     null /* dragAndDropPermissions */);
@@ -527,11 +531,24 @@
                 Slog.d(TAG_WM, "Sending DRAG_STARTED to new window " + newWin);
             }
             // Only allow the extras to be dispatched to a global-intercepting drag target
-            ClipData data = interceptsGlobalDrag ? mData.copyForTransferWithActivityInfo() : null;
+            ClipData data = null;
+            if (interceptsGlobalDrag) {
+                data = mData.copyForTransferWithActivityInfo();
+                PersistableBundle extras = data.getDescription().getExtras() != null
+                        ? data.getDescription().getExtras()
+                        : new PersistableBundle();
+                extras.putInt(EXTRA_HIDE_DRAG_SOURCE_TASK_ID, mCallingTaskIdToHide);
+                // Note that setting extras always copies the bundle
+                data.getDescription().setExtras(extras);
+                if (DEBUG_DRAG) {
+                    Slog.d(TAG_WM, "Adding EXTRA_HIDE_DRAG_SOURCE_TASK_ID=" + mCallingTaskIdToHide);
+                }
+            }
+            ClipDescription description = data != null ? data.getDescription() : mDataDescription;
             DragEvent event = obtainDragEvent(DragEvent.ACTION_DRAG_STARTED,
                     newWin.translateToWindowX(touchX), newWin.translateToWindowY(touchY),
-                    data, false /* includeDragSurface */, true /* includeDragFlags */,
-                    null /* dragAndDropPermission */);
+                    description, data, false /* includeDragSurface */,
+                    true /* includeDragFlags */, null /* dragAndDropPermission */);
             try {
                 newWin.mClient.dispatchDragEvent(event);
                 // track each window that we've notified that the drag is starting
@@ -700,37 +717,51 @@
         return mDragInProgress;
     }
 
-    private DragEvent obtainDragEvent(int action, float x, float y, ClipData data,
-            boolean includeDragSurface, boolean includeDragFlags,
+    private DragEvent obtainDragEvent(int action, float x, float y, ClipDescription description,
+            ClipData data, boolean includeDragSurface, boolean includeDragFlags,
             IDragAndDropPermissions dragAndDropPermissions) {
         return DragEvent.obtain(action, x, y, mThumbOffsetX, mThumbOffsetY,
                 includeDragFlags ? mFlags : 0,
-                null  /* localState */, mDataDescription, data,
+                null  /* localState */, description, data,
                 includeDragSurface ? mSurfaceControl : null,
                 dragAndDropPermissions, false /* result */);
     }
 
     private ValueAnimator createReturnAnimationLocked() {
-        final ValueAnimator animator = ValueAnimator.ofPropertyValuesHolder(
-                PropertyValuesHolder.ofFloat(
-                        ANIMATED_PROPERTY_X, mCurrentX - mThumbOffsetX,
-                        mOriginalX - mThumbOffsetX),
-                PropertyValuesHolder.ofFloat(
-                        ANIMATED_PROPERTY_Y, mCurrentY - mThumbOffsetY,
-                        mOriginalY - mThumbOffsetY),
-                PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale,
-                        mAnimatedScale),
-                PropertyValuesHolder.ofFloat(
-                        ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, mOriginalAlpha / 2));
+        final ValueAnimator animator;
+        final long duration;
+        if (mCallingTaskIdToHide != -1) {
+            animator = ValueAnimator.ofPropertyValuesHolder(
+                    PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, mCurrentX, mCurrentX),
+                    PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, mCurrentY, mCurrentY),
+                    PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale,
+                            mAnimatedScale),
+                    PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, 0f));
+            duration = MIN_ANIMATION_DURATION_MS;
+        } else {
+            animator = ValueAnimator.ofPropertyValuesHolder(
+                    PropertyValuesHolder.ofFloat(
+                            ANIMATED_PROPERTY_X, mCurrentX - mThumbOffsetX,
+                            mOriginalX - mThumbOffsetX),
+                    PropertyValuesHolder.ofFloat(
+                            ANIMATED_PROPERTY_Y, mCurrentY - mThumbOffsetY,
+                            mOriginalY - mThumbOffsetY),
+                    PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale,
+                            mAnimatedScale),
+                    PropertyValuesHolder.ofFloat(
+                            ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, mOriginalAlpha / 2));
 
-        final float translateX = mOriginalX - mCurrentX;
-        final float translateY = mOriginalY - mCurrentY;
-        // Adjust the duration to the travel distance.
-        final double travelDistance = Math.sqrt(translateX * translateX + translateY * translateY);
-        final double displayDiagonal =
-                Math.sqrt(mDisplaySize.x * mDisplaySize.x + mDisplaySize.y * mDisplaySize.y);
-        final long duration = MIN_ANIMATION_DURATION_MS + (long) (travelDistance / displayDiagonal
-                * (MAX_ANIMATION_DURATION_MS - MIN_ANIMATION_DURATION_MS));
+            final float translateX = mOriginalX - mCurrentX;
+            final float translateY = mOriginalY - mCurrentY;
+            // Adjust the duration to the travel distance.
+            final double travelDistance = Math.sqrt(
+                    translateX * translateX + translateY * translateY);
+            final double displayDiagonal =
+                    Math.sqrt(mDisplaySize.x * mDisplaySize.x + mDisplaySize.y * mDisplaySize.y);
+            duration = MIN_ANIMATION_DURATION_MS + (long) (travelDistance / displayDiagonal
+                    * (MAX_ANIMATION_DURATION_MS - MIN_ANIMATION_DURATION_MS));
+        }
+
         final AnimationListener listener = new AnimationListener();
         animator.setDuration(duration);
         animator.setInterpolator(mCubicEaseOutInterpolator);
@@ -742,13 +773,24 @@
     }
 
     private ValueAnimator createCancelAnimationLocked() {
-        final ValueAnimator animator = ValueAnimator.ofPropertyValuesHolder(
-                PropertyValuesHolder.ofFloat(
-                        ANIMATED_PROPERTY_X, mCurrentX - mThumbOffsetX, mCurrentX),
-                PropertyValuesHolder.ofFloat(
-                        ANIMATED_PROPERTY_Y, mCurrentY - mThumbOffsetY, mCurrentY),
-                PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale, 0),
-                PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, 0));
+        final ValueAnimator animator;
+        if (mCallingTaskIdToHide != -1) {
+             animator = ValueAnimator.ofPropertyValuesHolder(
+                    PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, mCurrentX, mCurrentX),
+                    PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, mCurrentY, mCurrentY),
+                    PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale,
+                            mAnimatedScale),
+                    PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, 0f));
+        } else {
+            animator = ValueAnimator.ofPropertyValuesHolder(
+                    PropertyValuesHolder.ofFloat(
+                            ANIMATED_PROPERTY_X, mCurrentX - mThumbOffsetX, mCurrentX),
+                    PropertyValuesHolder.ofFloat(
+                            ANIMATED_PROPERTY_Y, mCurrentY - mThumbOffsetY, mCurrentY),
+                    PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale, 0),
+                    PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, 0));
+        }
+
         final AnimationListener listener = new AnimationListener();
         animator.setDuration(MIN_ANIMATION_DURATION_MS);
         animator.setInterpolator(mCubicEaseOutInterpolator);
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index e924fb6..a3550bc 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -462,12 +462,16 @@
         final boolean isTabletopMode = isDisplayFullScreenAndInPosture(/* isTabletop */ true);
         final boolean isLandscape = isFixedOrientationLandscape(
                 mActivityRecord.getOverrideOrientation());
-
+        final AppCompatCameraOverrides appCompatCameraOverrides =
+                mActivityRecord.mAppCompatController.getAppCompatCameraOverrides();
+        final AppCompatCameraPolicy cameraPolicy =
+                mActivityRecord.mAppCompatController.getAppCompatCameraPolicy();
+        final boolean isCameraCompatTreatmentActive = cameraPolicy != null
+                && cameraPolicy.isTreatmentEnabledForActivity(mActivityRecord);
         // Don't resize to split screen size when in book mode if letterbox position is centered
         return (isBookMode && isNotCenteredHorizontally || isTabletopMode && isLandscape)
-                    || mActivityRecord.mAppCompatController.getAppCompatCameraOverrides()
-                            .isCameraCompatSplitScreenAspectRatioAllowed()
-                                && getAppCompatOverrides().isCameraCompatTreatmentActive();
+                    || (appCompatCameraOverrides.isCameraCompatSplitScreenAspectRatioAllowed()
+                    && isCameraCompatTreatmentActive);
     }
 
     private float getDefaultMinAspectRatioForUnresizableApps() {
diff --git a/services/core/java/com/android/server/wm/SafeActivityOptions.java b/services/core/java/com/android/server/wm/SafeActivityOptions.java
index b452131..8c7b637 100644
--- a/services/core/java/com/android/server/wm/SafeActivityOptions.java
+++ b/services/core/java/com/android/server/wm/SafeActivityOptions.java
@@ -442,7 +442,10 @@
         return taskDisplayArea;
     }
 
-    private boolean isAssistant(ActivityTaskManagerService atmService, int callingUid) {
+    /**
+     * Returns whether the given UID caller is the assistant.
+     */
+    public static boolean isAssistant(ActivityTaskManagerService atmService, int callingUid) {
         if (atmService.mActiveVoiceInteractionServiceComponent == null) {
             return false;
         }
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index 3eb3218..31fda77 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -815,10 +815,8 @@
                 if (mDisplayContent.getRotationAnimation() == ScreenRotationAnimation.this) {
                     // It also invokes kill().
                     mDisplayContent.setRotationAnimation(null);
-                    if (mDisplayContent.mDisplayRotationCompatPolicy != null) {
-                        mDisplayContent.mDisplayRotationCompatPolicy
-                                .onScreenRotationAnimationFinished();
-                    }
+                    mDisplayContent.mAppCompatCameraPolicy
+                            .onScreenRotationAnimationFinished();
                 } else {
                     kill();
                 }
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 310516b..75e3e65 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -349,7 +349,7 @@
         final int callingPid = Binder.getCallingPid();
         // Validate and resolve ClipDescription data before clearing the calling identity
         validateAndResolveDragMimeTypeExtras(data, callingUid, callingPid, mPackageName);
-        validateDragFlags(flags);
+        validateDragFlags(flags, callingUid);
         final long ident = Binder.clearCallingIdentity();
         try {
             return mDragDropController.performDrag(mPid, mUid, window, flags, surface, touchSource,
@@ -375,12 +375,17 @@
      * Validates the given drag flags.
      */
     @VisibleForTesting
-    void validateDragFlags(int flags) {
+    void validateDragFlags(int flags, int callingUid) {
         if ((flags & View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION) != 0) {
             if (!mCanStartTasksFromRecents) {
                 throw new SecurityException("Requires START_TASKS_FROM_RECENTS permission");
             }
         }
+        if ((flags & View.DRAG_FLAG_HIDE_CALLING_TASK_ON_DRAG_START) != 0) {
+            if (!SafeActivityOptions.isAssistant(mService.mAtmService, callingUid)) {
+                throw new SecurityException("Caller is not the assistant");
+            }
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index f839ed6..47af6fc 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -105,8 +105,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.ColorUtils;
 import com.android.internal.policy.TransitionAnimation;
-import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.statusbar.StatusBarManagerInternal;
@@ -1443,11 +1443,11 @@
                 asyncRotationController.onTransitionFinished();
             }
             dc.onTransitionFinished();
-            if (hasParticipatedDisplay && dc.mDisplayRotationCompatPolicy != null) {
+            if (hasParticipatedDisplay) {
                 final ChangeInfo changeInfo = mChanges.get(dc);
                 if (changeInfo != null
                         && changeInfo.mRotation != dc.getWindowConfiguration().getRotation()) {
-                    dc.mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+                    dc.mAppCompatCameraPolicy.onScreenRotationAnimationFinished();
                 }
             }
             if (mTransientLaunches != null) {
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index edd118d..1d02f1c 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -179,7 +179,7 @@
 
     // List of children for this window container. List is in z-order as the children appear on
     // screen with the top-most window container at the tail of the list.
-    protected final WindowList<E> mChildren = new WindowList<E>();
+    protected final ArrayList<E> mChildren = new ArrayList<E>();
 
     // The specified orientation for this window container.
     // Shouldn't be accessed directly since subclasses can override getOverrideOrientation.
@@ -855,7 +855,7 @@
             mSurfaceFreezer.unfreeze(getSyncTransaction());
         }
         while (!mChildren.isEmpty()) {
-            final E child = mChildren.peekLast();
+            final E child = mChildren.getLast();
             child.removeImmediately();
             // Need to do this after calling remove on the child because the child might try to
             // remove/detach itself from its parent which will cause an exception if we remove
@@ -979,7 +979,7 @@
 
         switch (position) {
             case POSITION_TOP:
-                if (mChildren.peekLast() != child) {
+                if (getTopChild() != child) {
                     mChildren.remove(child);
                     mChildren.add(child);
                     onChildPositionChanged(child);
@@ -990,7 +990,7 @@
                 }
                 break;
             case POSITION_BOTTOM:
-                if (mChildren.peekFirst() != child) {
+                if (getBottomChild() != child) {
                     mChildren.remove(child);
                     mChildren.addFirst(child);
                     onChildPositionChanged(child);
@@ -1445,7 +1445,13 @@
 
     /** Returns the top child container. */
     E getTopChild() {
-        return mChildren.peekLast();
+        final int n = mChildren.size();
+        return n == 0 ? null : mChildren.get(n - 1);
+    }
+
+    E getBottomChild() {
+        final int n = mChildren.size();
+        return n == 0 ? null : mChildren.get(0);
     }
 
     /**
@@ -2550,7 +2556,7 @@
         }
 
         if (mParent != null && mParent == other.mParent) {
-            final WindowList<WindowContainer> list = mParent.mChildren;
+            final ArrayList<WindowContainer> list = mParent.mChildren;
             return list.indexOf(this) > list.indexOf(other) ? 1 : -1;
         }
 
@@ -2587,7 +2593,7 @@
 
             // The position of the first non-common ancestor in the common ancestor list determines
             // which is greater the which.
-            final WindowList<WindowContainer> list = commonAncestor.mChildren;
+            final ArrayList<WindowContainer> list = commonAncestor.mChildren;
             return list.indexOf(thisParentChain.peekLast()) > list.indexOf(otherParentChain.peekLast())
                     ? 1 : -1;
         } finally {
diff --git a/services/core/java/com/android/server/wm/WindowList.java b/services/core/java/com/android/server/wm/WindowList.java
deleted file mode 100644
index 1e888f5..0000000
--- a/services/core/java/com/android/server/wm/WindowList.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2017 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.wm;
-
-import java.util.ArrayList;
-
-/**
- * An {@link ArrayList} with extended functionality to be used as the children data structure in
- * {@link WindowContainer}.
- */
-class WindowList<E> extends ArrayList<E> {
-
-    public void addFirst(E e) {
-        add(0, e);
-    }
-
-    E peekLast() {
-        return size() > 0 ? get(size() - 1) : null;
-    }
-
-    E peekFirst() {
-        return size() > 0 ? get(0) : null;
-    }
-}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 0c1ec504..9a5f84c 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -732,6 +732,13 @@
     final DisplayWindowListenerController mDisplayNotificationController;
     final TaskSystemBarsListenerController mTaskSystemBarsListenerController;
 
+    /** Amount of time (in milliseconds) to delay the pointer down outside focus handling */
+    private static final int POINTER_DOWN_OUTSIDE_FOCUS_TIMEOUT_MS = 50;
+
+    /** A runnable to handle pointer down outside focus event. */
+    @Nullable
+    private Runnable mPointerDownOutsideFocusRunnable;
+
     boolean mDisplayFrozen = false;
     long mDisplayFreezeTime = 0;
     int mLastDisplayFreezeDuration = 0;
@@ -5896,7 +5903,8 @@
                 case ON_POINTER_DOWN_OUTSIDE_FOCUS: {
                     synchronized (mGlobalLock) {
                         final IBinder touchedToken = (IBinder) msg.obj;
-                        onPointerDownOutsideFocusLocked(getInputTargetFromToken(touchedToken));
+                        onPointerDownOutsideFocusLocked(getInputTargetFromToken(touchedToken),
+                                true /* fromHandler */);
                     }
                     break;
                 }
@@ -7922,7 +7930,8 @@
             synchronized (mGlobalLock) {
                 final InputTarget inputTarget =
                         WindowManagerService.this.getInputTargetFromWindowTokenLocked(windowToken);
-                WindowManagerService.this.onPointerDownOutsideFocusLocked(inputTarget);
+                WindowManagerService.this.onPointerDownOutsideFocusLocked(inputTarget,
+                        false /* fromHandler */);
             }
         }
 
@@ -8997,39 +9006,78 @@
         }
     }
 
-    private void onPointerDownOutsideFocusLocked(InputTarget t) {
+    void clearPointerDownOutsideFocusRunnable() {
+        if (mPointerDownOutsideFocusRunnable == null) return;
+
+        mH.removeCallbacks(mPointerDownOutsideFocusRunnable);
+        mPointerDownOutsideFocusRunnable = null;
+    }
+
+    private void onPointerDownOutsideFocusLocked(InputTarget t, boolean fromHandler) {
         if (t == null || !t.receiveFocusFromTapOutside()) {
             // If the window that received the input event cannot receive keys, don't move the
             // display it's on to the top since that window won't be able to get focus anyway.
             return;
         }
-        if (mRecentsAnimationController != null
-            && mRecentsAnimationController.getTargetAppMainWindow() == t) {
-            // If there is an active recents animation and touched window is the target, then ignore
-            // the touch. The target already handles touches using its own input monitor and we
-            // don't want to trigger any lifecycle changes from focusing another window.
-            // TODO(b/186770026): We should remove this once we support multiple resumed activities
-            //                    while in overview
-            return;
-        }
+        clearPointerDownOutsideFocusRunnable();
+
+        // For embedded activity that is showing side-by-side with another activity, delay
+        // handling the touch-outside event to prevent focus rapid changes back-n-forth.
+        // Otherwise, handle the touch-outside event directly.
         final WindowState w = t.getWindowState();
-        if (w != null) {
-            final Task task = w.getTask();
-            if (task != null && w.mTransitionController.isTransientHide(task)) {
-                // Don't disturb transient animation by accident touch.
+        final ActivityRecord activity = w != null ? w.getActivityRecord() : null;
+        if (activity != null && activity.isEmbedded()
+                && activity.getTaskFragment().getAdjacentTaskFragment() != null) {
+            mPointerDownOutsideFocusRunnable = () -> handlePointerDownOutsideFocus(t);
+            mH.postDelayed(mPointerDownOutsideFocusRunnable, POINTER_DOWN_OUTSIDE_FOCUS_TIMEOUT_MS);
+        } else if (!fromHandler) {
+            // Still post the runnable to handler thread in case there is already a runnable
+            // in execution, but still waiting to hold the wm lock.
+            mPointerDownOutsideFocusRunnable = () -> handlePointerDownOutsideFocus(t);
+            mH.post(mPointerDownOutsideFocusRunnable);
+        } else {
+            handlePointerDownOutsideFocus(t);
+        }
+    }
+
+    private void handlePointerDownOutsideFocus(InputTarget t) {
+        synchronized (mGlobalLock) {
+            if (mPointerDownOutsideFocusRunnable != null
+                    && mH.hasCallbacks(mPointerDownOutsideFocusRunnable)) {
+                // Skip if there's another pending pointer-down-outside-focus event.
                 return;
             }
-        }
+            clearPointerDownOutsideFocusRunnable();
 
-        ProtoLog.i(WM_DEBUG_FOCUS_LIGHT, "onPointerDownOutsideFocusLocked called on %s",
-                t);
-        if (mFocusedInputTarget != t && mFocusedInputTarget != null) {
-            mFocusedInputTarget.handleTapOutsideFocusOutsideSelf();
+            if (mRecentsAnimationController != null
+                    && mRecentsAnimationController.getTargetAppMainWindow() == t) {
+                // If there is an active recents animation and touched window is the target,
+                // then ignore the touch. The target already handles touches using its own
+                // input monitor and we don't want to trigger any lifecycle changes from
+                // focusing another window.
+                // TODO(b/186770026): We should remove this once we support multiple resumed
+                //  activities while in overview
+                return;
+            }
+
+            final WindowState w = t.getWindowState();
+            if (w != null) {
+                final Task task = w.getTask();
+                if (task != null && w.mTransitionController.isTransientHide(task)) {
+                    // Don't disturb transient animation by accident touch.
+                    return;
+                }
+            }
+
+            ProtoLog.i(WM_DEBUG_FOCUS_LIGHT, "onPointerDownOutsideFocusLocked called on %s", t);
+            if (mFocusedInputTarget != t && mFocusedInputTarget != null) {
+                mFocusedInputTarget.handleTapOutsideFocusOutsideSelf();
+            }
+            // Trigger Activity#onUserLeaveHint() if the order change of task pauses any activities.
+            mAtmService.mTaskSupervisor.mUserLeaving = true;
+            t.handleTapOutsideFocusInsideSelf();
+            mAtmService.mTaskSupervisor.mUserLeaving = false;
         }
-        // Trigger Activity#onUserLeaveHint() if the order change of task pauses any activities.
-        mAtmService.mTaskSupervisor.mUserLeaving = true;
-        t.handleTapOutsideFocusInsideSelf();
-        mAtmService.mTaskSupervisor.mUserLeaving = false;
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index deb7098..e900488 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1596,7 +1596,7 @@
             case OP_TYPE_REORDER_TO_BOTTOM_OF_TASK: {
                 final Task task = taskFragment.getTask();
                 if (task != null) {
-                    if (task.mChildren.peekFirst() != taskFragment) {
+                    if (task.getBottomChild() != taskFragment) {
                         task.mChildren.remove(taskFragment);
                         task.mChildren.add(0, taskFragment);
                         if (!taskFragment.hasChild()) {
@@ -1612,7 +1612,7 @@
             case OP_TYPE_REORDER_TO_TOP_OF_TASK: {
                 final Task task = taskFragment.getTask();
                 if (task != null) {
-                    if (task.mChildren.peekLast() != taskFragment) {
+                    if (task.getTopChild() != taskFragment) {
                         task.mChildren.remove(taskFragment);
                         task.mChildren.add(taskFragment);
                         if (!taskFragment.hasChild()) {
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index f6942ee..60d3e78 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -1581,25 +1581,6 @@
     }
 
     @Override
-    public boolean onApplyAppSpecificConfig(Configuration inOutConfig) {
-        if (mConfigActivityRecord != null) {
-            // Let the activity decide whether to apply the size override.
-            return false;
-        }
-        final DisplayContent displayContent = mAtm.mWindowManager != null
-                ? mAtm.mWindowManager.getDefaultDisplayContentLocked()
-                : null;
-        return applySizeOverride(
-                displayContent,
-                mInfo,
-                null /* newParentConfiguration */,
-                inOutConfig,
-                false /* optOutEdgeToEdge */,
-                false /* hasFixedRotationTransform */,
-                false /* hasCompatDisplayInsets */);
-    }
-
-    @Override
     public void onConfigurationChanged(Configuration newGlobalConfig) {
         super.onConfigurationChanged(newGlobalConfig);
 
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index 7649a4e..3cd5f76 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -210,6 +210,7 @@
         "android.system.suspend-V1-ndk",
         "server_configurable_flags",
         "service.incremental",
+        "android.companion.virtualdevice.flags-aconfig-cc",
     ],
 
     static_libs: [
diff --git a/services/core/jni/com_android_server_companion_virtual_InputController.cpp b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
index 5c4db24..a32b0f1 100644
--- a/services/core/jni/com_android_server_companion_virtual_InputController.cpp
+++ b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
@@ -19,6 +19,7 @@
 #include <android-base/unique_fd.h>
 #include <android/input.h>
 #include <android/keycodes.h>
+#include <android_companion_virtualdevice_flags.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <input/Input.h>
@@ -37,6 +38,8 @@
 
 namespace android {
 
+namespace vd_flags = android::companion::virtualdevice::flags;
+
 static constexpr jlong INVALID_PTR = 0;
 
 enum class DeviceType {
@@ -88,6 +91,10 @@
             ioctl(fd, UI_SET_RELBIT, REL_Y);
             ioctl(fd, UI_SET_RELBIT, REL_WHEEL);
             ioctl(fd, UI_SET_RELBIT, REL_HWHEEL);
+            if (vd_flags::high_resolution_scroll()) {
+                ioctl(fd, UI_SET_RELBIT, REL_WHEEL_HI_RES);
+                ioctl(fd, UI_SET_RELBIT, REL_HWHEEL_HI_RES);
+            }
             break;
         case DeviceType::TOUCHSCREEN:
             ioctl(fd, UI_SET_EVBIT, EV_ABS);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 791d030..e5a1ebf 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -2445,11 +2445,11 @@
                 t.traceEnd();
             }
 
-            t.traceBegin("CertBlacklister");
+            t.traceBegin("CertBlocklister");
             try {
-                CertBlacklister blacklister = new CertBlacklister(context);
+                CertBlocklister blocklister = new CertBlocklister(context);
             } catch (Throwable e) {
-                reportWtf("starting CertBlacklister", e);
+                reportWtf("starting CertBlocklister", e);
             }
             t.traceEnd();
 
diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index c76bcf8..3ed6ad7 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -404,7 +404,7 @@
                 String traceTag = traceInitialization ? "camera_init" : "camera";
                 BackgroundThread.get().getThreadHandler().postDelayed(() -> {
                     try {
-                        mIProfcollect.trace_system(traceTag);
+                        mIProfcollect.trace_process(traceTag, "android.hardware.camera.provider");
                     } catch (RemoteException e) {
                         Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
                     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
index 7aec42b..00daf41 100644
--- a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
@@ -43,6 +43,7 @@
 import android.Manifest;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
 import android.app.AlarmManager;
 import android.app.IActivityManager;
 import android.app.admin.DevicePolicyManager;
@@ -142,6 +143,7 @@
     private final Map<ComponentName, ITrustAgentService.Stub> mMockTrustAgents = new HashMap<>();
 
     private @Mock ActivityManager mActivityManager;
+    private @Mock ActivityManagerInternal mActivityManagerInternal;
     private @Mock AlarmManager mAlarmManager;
     private @Mock BiometricManager mBiometricManager;
     private @Mock DevicePolicyManager mDevicePolicyManager;
@@ -158,6 +160,7 @@
     private HandlerThread mHandlerThread;
     private TrustManagerService mService;
     private ITrustManager mTrustManager;
+    private ActivityManagerInternal mPreviousActivityManagerInternal;
 
     @Before
     public void setUp() throws Exception {
@@ -210,6 +213,11 @@
         mMockContext.setMockPackageManager(mPackageManager);
         mMockContext.addMockSystemService(UserManager.class, mUserManager);
         doReturn(mWindowManager).when(() -> WindowManagerGlobal.getWindowManagerService());
+        mPreviousActivityManagerInternal = LocalServices.getService(
+                ActivityManagerInternal.class);
+        LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+        LocalServices.addService(ActivityManagerInternal.class,
+                mActivityManagerInternal);
         LocalServices.addService(SystemServiceManager.class, mock(SystemServiceManager.class));
 
         grantPermission(Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE);
@@ -257,7 +265,14 @@
     @After
     public void tearDown() {
         LocalServices.removeServiceForTest(SystemServiceManager.class);
-        mHandlerThread.quit();
+        LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+        if (mPreviousActivityManagerInternal != null) {
+            LocalServices.addService(ActivityManagerInternal.class,
+                    mPreviousActivityManagerInternal);
+        }
+        if (mHandlerThread != null) {
+            mHandlerThread.quit();
+        }
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index d151345..559c324 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -188,6 +188,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ThreadLocalRandom;
 
 @SmallTest
@@ -489,6 +490,34 @@
         when(mPm.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
     }
 
+    private static void testThreadSafety(Runnable operationToTest, int nThreads,
+            int nRunsPerThread) throws Exception {
+        final CountDownLatch startLatch = new CountDownLatch(1);
+        final CountDownLatch doneLatch = new CountDownLatch(nThreads);
+
+        for (int i = 0; i < nThreads; i++) {
+            Runnable threadRunnable = () -> {
+                try {
+                    startLatch.await();
+                    for (int j = 0; j < nRunsPerThread; j++) {
+                        operationToTest.run();
+                    }
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                } finally {
+                    doneLatch.countDown();
+                }
+            };
+            new Thread(threadRunnable, "Test Thread #" + i).start();
+        }
+
+        // Ready set go
+        startLatch.countDown();
+
+        // Wait for all test threads to be done.
+        doneLatch.await();
+    }
+
     @Test
     public void testWriteXml_onlyBackupsTargetUser() throws Exception {
         // Setup package notifications.
@@ -6193,6 +6222,36 @@
                 .isEqualTo(IMPORTANCE_LOW);
     }
 
+
+    @Test
+    public void testRestoredWithoutUid_threadSafety() throws Exception {
+        when(mPm.getPackageUidAsUser(anyString(), anyInt())).thenReturn(UNKNOWN_UID);
+        when(mPm.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())).thenThrow(
+                new PackageManager.NameNotFoundException());
+        when(mClock.millis()).thenReturn(System.currentTimeMillis());
+        testThreadSafety(() -> {
+            String id = "id";
+            String xml = "<ranking version=\"1\">\n"
+                    + "<package name=\"" + Thread.currentThread()+ "\" show_badge=\"true\">\n"
+                    + "<channel id=\"" + id + "\" name=\"name\" importance=\"2\" "
+                    + "show_badge=\"true\" />\n"
+                    + "</package>\n"
+                    + "<package name=\"" + PKG_P + "\" show_badge=\"true\">\n"
+                    + "</package>\n"
+                    + "</ranking>\n";
+
+            try {
+                loadByteArrayXml(xml.getBytes(), true, USER_SYSTEM);
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+
+            // trigger a removal from the list
+            mXmlHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_P},
+                    new int[]{UNKNOWN_UID});
+        }, 20, 50);
+    }
+
     private static NotificationChannel cloneChannel(NotificationChannel original) {
         Parcel parcel = Parcel.obtain();
         try {
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index ef3df6c..eb8825c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -116,7 +116,6 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.when;
 
 import android.app.ActivityOptions;
 import android.app.AppOpsManager;
@@ -2482,10 +2481,10 @@
         assertTrue(activity.mChildren.contains(win4));
 
         // The starting window should be on-top of all other windows.
-        assertEquals(startingWin, activity.mChildren.peekLast());
+        assertEquals(startingWin, activity.getTopChild());
 
         // The base application window should be below all other windows.
-        assertEquals(baseWin, activity.mChildren.peekFirst());
+        assertEquals(baseWin, activity.getBottomChild());
         activity.removeImmediately();
     }
 
@@ -3508,23 +3507,6 @@
     }
 
     @Test
-    public void testIsCameraActive() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
-        final DisplayRotationCompatPolicy displayRotationCompatPolicy = mock(
-                DisplayRotationCompatPolicy.class);
-        when(mDisplayContent.getDisplayRotationCompatPolicy()).thenReturn(
-                displayRotationCompatPolicy);
-
-        when(displayRotationCompatPolicy.isCameraActive(any(ActivityRecord.class),
-                anyBoolean())).thenReturn(false);
-        assertFalse(app.mActivityRecord.isCameraActive());
-
-        when(displayRotationCompatPolicy.isCameraActive(any(ActivityRecord.class),
-                anyBoolean())).thenReturn(true);
-        assertTrue(app.mActivityRecord.isCameraActive());
-    }
-
-    @Test
     public void testUpdateCameraCompatStateFromUser_clickedOnDismiss() throws RemoteException {
         final ActivityRecord activity = createActivityWithTask();
         // Mock a flag being enabled.
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 467050e..f79cdc1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -69,6 +69,7 @@
 
     private final int mDisplayWidth;
     private final int mDisplayHeight;
+    private DisplayContent mDisplayContent;
 
     AppCompatActivityRobot(@NonNull WindowManagerService wm,
             @NonNull ActivityTaskManagerService atm, @NonNull ActivityTaskSupervisor supervisor,
@@ -79,6 +80,7 @@
         mDisplayHeight = displayHeight;
         mActivityStack = new TestComponentStack<>();
         mTaskStack = new TestComponentStack<>();
+        createNewDisplay();
     }
 
     AppCompatActivityRobot(@NonNull WindowManagerService wm,
@@ -87,13 +89,19 @@
     }
 
     void createActivityWithComponent() {
-        createActivityWithComponentInNewTask(/* inNewTask */ mTaskStack.isEmpty());
+        createActivityWithComponentInNewTask(/* inNewTask */ mTaskStack.isEmpty(),
+                /* inNewDisplay */ false);
     }
 
     void createActivityWithComponentInNewTask() {
-        createActivityWithComponentInNewTask(/* inNewTask */ true);
+        createActivityWithComponentInNewTask(/* inNewTask */ true, /* inNewDisplay */ false);
     }
 
+    void createActivityWithComponentInNewTaskAndDisplay() {
+        createActivityWithComponentInNewTask(/* inNewTask */ true, /* inNewDisplay */ true);
+    }
+
+
     void configureTopActivity(float minAspect, float maxAspect, int screenOrientation,
             boolean isUnresizable) {
         prepareLimitedBounds(mActivityStack.top(), minAspect, maxAspect, screenOrientation,
@@ -110,12 +118,22 @@
                 /* isUnresizable */ true);
     }
 
+    void activateCameraInPolicy(boolean isCameraActive) {
+        doReturn(isCameraActive).when(mDisplayContent.mAppCompatCameraPolicy)
+                .isCameraActive(any(ActivityRecord.class), anyBoolean());
+    }
+
     @NonNull
     ActivityRecord top() {
         return mActivityStack.top();
     }
 
     @NonNull
+    DisplayContent displayContent() {
+        return mDisplayContent;
+    }
+
+    @NonNull
     ActivityRecord getFromTop(int fromTop) {
         return mActivityStack.getFromTop(fromTop);
     }
@@ -130,7 +148,7 @@
     }
 
     void enableTreatmentForTopActivity(boolean enabled) {
-        doReturn(enabled).when(getTopDisplayRotationCompatPolicy())
+        doReturn(enabled).when(mDisplayContent.mAppCompatCameraPolicy)
                 .isTreatmentEnabledForActivity(eq(mActivityStack.top()));
     }
 
@@ -164,7 +182,7 @@
     }
 
     void setIgnoreOrientationRequest(boolean enabled) {
-        mActivityStack.top().mDisplayContent.setIgnoreOrientationRequest(enabled);
+        mDisplayContent.setIgnoreOrientationRequest(enabled);
     }
 
     void setTopActivityAsEmbedded(boolean embedded) {
@@ -179,20 +197,22 @@
         mActivityStack.applyTo(/* fromTop */ fromTop, ActivityRecord::removeImmediately);
     }
 
+    void createNewDisplay() {
+        mDisplayContent = new TestDisplayContent.Builder(mAtm, mDisplayWidth, mDisplayHeight)
+                .build();
+        spyOnAppCompatCameraPolicy();
+    }
+
     void createNewTask() {
-        final DisplayContent displayContent = new TestDisplayContent
-                .Builder(mAtm, mDisplayWidth, mDisplayHeight).build();
         final Task newTask = new WindowTestsBase.TaskBuilder(mSupervisor)
-                .setDisplay(displayContent).build();
+                .setDisplay(mDisplayContent).build();
         mTaskStack.push(newTask);
     }
 
     void createNewTaskWithBaseActivity() {
-        final DisplayContent displayContent = new TestDisplayContent
-                .Builder(mAtm, mDisplayWidth, mDisplayHeight).build();
         final Task newTask = new WindowTestsBase.TaskBuilder(mSupervisor)
                 .setCreateActivity(true)
-                .setDisplay(displayContent).build();
+                .setDisplay(mDisplayContent).build();
         mTaskStack.push(newTask);
         pushActivity(newTask.getTopNonFinishingActivity());
     }
@@ -319,7 +339,10 @@
         pushActivity(newActivity);
     }
 
-    private void createActivityWithComponentInNewTask(boolean inNewTask) {
+    private void createActivityWithComponentInNewTask(boolean inNewTask, boolean inNewDisplay) {
+        if (inNewDisplay) {
+            createNewDisplay();
+        }
         if (inNewTask) {
             createNewTask();
         }
@@ -369,7 +392,8 @@
     }
 
     private DisplayRotationCompatPolicy getTopDisplayRotationCompatPolicy() {
-        return mActivityStack.top().mDisplayContent.mDisplayRotationCompatPolicy;
+        return mActivityStack.top().mDisplayContent
+                .mAppCompatCameraPolicy.mDisplayRotationCompatPolicy;
     }
 
     // We add the activity to the stack and spyOn() on its properties.
@@ -377,10 +401,16 @@
         mActivityStack.push(activity);
         spyOn(activity);
         spyOn(activity.mAppCompatController.getTransparentPolicy());
-        if (activity.mDisplayContent != null
-                && activity.mDisplayContent.mDisplayRotationCompatPolicy != null) {
-            spyOn(activity.mDisplayContent.mDisplayRotationCompatPolicy);
-        }
         spyOn(activity.mLetterboxUiController);
     }
+
+    private void spyOnAppCompatCameraPolicy() {
+        spyOn(mDisplayContent.mAppCompatCameraPolicy);
+        if (mDisplayContent.mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy()) {
+            spyOn(mDisplayContent.mAppCompatCameraPolicy.mDisplayRotationCompatPolicy);
+        }
+        if (mDisplayContent.mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()) {
+            spyOn(mDisplayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy);
+        }
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
index 9263b4f..2d94b34 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
@@ -26,7 +26,6 @@
 import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
 import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
 
@@ -264,14 +263,29 @@
     public void testShouldRecomputeConfigurationForCameraCompat() {
         runTestScenario((robot) -> {
             robot.conf().enableCameraCompatSplitScreenAspectRatio(true);
-            robot.activity().createActivityWithComponentInNewTask();
-            robot.activateCamera(true);
-            robot.activity().setShouldCreateCompatDisplayInsets(false);
+            robot.applyOnActivity((a) -> {
+                a.createActivityWithComponentInNewTask();
+                a.activateCameraInPolicy(true);
+                a.setShouldCreateCompatDisplayInsets(false);
+            });
 
             robot.checkShouldApplyFreeformTreatmentForCameraCompat(true);
         });
     }
 
+    @Test
+    public void testIsCameraActive() {
+        runTestScenario((robot) -> {
+            robot.applyOnActivity((a) -> {
+                a.createActivityWithComponent();
+                a.activateCameraInPolicy(/* isCameraActive */ false);
+                robot.checkIsCameraActive(/* active */ false);
+                a.activateCameraInPolicy(/* isCameraActive */ true);
+                robot.checkIsCameraActive(/* active */ true);
+            });
+        });
+    }
+
     /**
      * Runs a test scenario providing a Robot.
      */
@@ -289,10 +303,6 @@
             super(wm, atm, supervisor);
         }
 
-        void activateCamera(boolean isCameraActive) {
-            doReturn(isCameraActive).when(activity().top()).isCameraActive();
-        }
-
         void checkShouldRefreshActivityForCameraCompat(boolean expected) {
             Assert.assertEquals(getAppCompatCameraOverrides()
                     .shouldRefreshActivityForCameraCompat(), expected);
@@ -313,6 +323,10 @@
                     .shouldApplyFreeformTreatmentForCameraCompat(), expected);
         }
 
+        void checkIsCameraActive(boolean active) {
+            Assert.assertEquals(getAppCompatCameraOverrides().isCameraActive(), active);
+        }
+
         private AppCompatCameraOverrides getAppCompatCameraOverrides() {
             return activity().top().mAppCompatController.getAppCompatCameraOverrides();
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
index 4116313..006b370 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
@@ -16,23 +16,20 @@
 
 package com.android.server.wm;
 
-import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA;
-import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
-
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
 
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
+import static org.mockito.ArgumentMatchers.any;
 
 import android.compat.testing.PlatformCompatChangeRule;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.annotation.NonNull;
 
-import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
-import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
-
+import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestRule;
@@ -54,96 +51,128 @@
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
 
     @Test
-    @DisableCompatChanges({OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
-    public void testRecomputeConfigurationForCameraCompatIfNeeded_allDisabledNoRecompute() {
+    public void testDisplayRotationCompatPolicy_presentWhenEnabled() {
         runTestScenario((robot) -> {
-            robot.activity().createActivityWithComponent();
-            robot.conf().enableCameraCompatSplitScreenAspectRatio(false);
-            robot.activateCamera(/* isCameraActive */ false);
-
-            robot.recomputeConfigurationForCameraCompatIfNeeded();
-            robot.checkRecomputeConfigurationInvoked(/* invoked */ false);
-
+            robot.conf().enableCameraCompatTreatmentAtBuildTime(true);
+            robot.activity().createActivityWithComponentInNewTaskAndDisplay();
+            robot.checkTopActivityHasDisplayRotationCompatPolicy(true);
         });
     }
 
     @Test
-    @EnableCompatChanges({OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
-    public void testRecomputeConfigurationForCameraCompatIfNeeded_cameraEnabledRecompute() {
+    public void testDisplayRotationCompatPolicy_notPresentWhenDisabled() {
         runTestScenario((robot) -> {
-            robot.activity().createActivityWithComponent();
-            robot.conf().enableCameraCompatSplitScreenAspectRatio(false);
-            robot.activateCamera(/* isCameraActive */ false);
-
-            robot.recomputeConfigurationForCameraCompatIfNeeded();
-            robot.checkRecomputeConfigurationInvoked(/* invoked */ true);
+            robot.conf().enableCameraCompatTreatmentAtBuildTime(false);
+            robot.activity().createActivityWithComponentInNewTaskAndDisplay();
+            robot.checkTopActivityHasDisplayRotationCompatPolicy(false);
         });
     }
 
     @Test
-    @DisableCompatChanges({OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
-    public void testRecomputeConfigurationForCameraSplitScreenCompatIfNeeded_recompute() {
+    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    public void testCameraCompatFreeformPolicy_presentWhenEnabledAndDW() {
         runTestScenario((robot) -> {
-            robot.activity().createActivityWithComponent();
-            robot.conf().enableCameraCompatSplitScreenAspectRatio(true);
-            robot.activateCamera(/* isCameraActive */ false);
-
-            robot.recomputeConfigurationForCameraCompatIfNeeded();
-            robot.checkRecomputeConfigurationInvoked(/* invoked */ true);
+            robot.allowEnterDesktopMode(true);
+            robot.activity().createActivityWithComponentInNewTaskAndDisplay();
+            robot.checkTopActivityHasCameraCompatFreeformPolicy(true);
         });
     }
 
     @Test
-    @DisableCompatChanges({OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
-    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
-    public void testRecomputeConfigurationForCameraSplitScreenCompatIfNeededWithCamera_recompute() {
+    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    public void testCameraCompatFreeformPolicy_notPresentWhenNoDW() {
         runTestScenario((robot) -> {
-            robot.activity().createActivityWithComponent();
-            robot.conf().enableCameraCompatSplitScreenAspectRatio(false);
-            robot.activateCamera(/* isCameraActive */ true);
-
-            robot.recomputeConfigurationForCameraCompatIfNeeded();
-            robot.checkRecomputeConfigurationInvoked(/* invoked */ true);
+            robot.allowEnterDesktopMode(false);
+            robot.activity().createActivityWithComponentInNewTaskAndDisplay();
+            robot.checkTopActivityHasCameraCompatFreeformPolicy(false);
         });
     }
 
-    void runTestScenario(@NonNull Consumer<CameraPolicyRobotTest> consumer) {
+    @Test
+    @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    public void testCameraCompatFreeformPolicy_notPresentWhenNoFlag() {
+        runTestScenario((robot) -> {
+            robot.allowEnterDesktopMode(true);
+            robot.activity().createActivityWithComponentInNewTaskAndDisplay();
+            robot.checkTopActivityHasCameraCompatFreeformPolicy(false);
+        });
+    }
+
+    @Test
+    @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+    public void testCameraCompatFreeformPolicy_notPresentWhenNoFlagAndNoDW() {
+        runTestScenario((robot) -> {
+            robot.allowEnterDesktopMode(false);
+            robot.activity().createActivityWithComponentInNewTaskAndDisplay();
+            robot.checkTopActivityHasCameraCompatFreeformPolicy(false);
+        });
+    }
+
+    /**
+     * Runs a test scenario providing a Robot.
+     */
+    void runTestScenario(@NonNull Consumer<DisplayRotationPolicyRobotTest> consumer) {
         spyOn(mWm.mLetterboxConfiguration);
-        final CameraPolicyRobotTest robot = new CameraPolicyRobotTest(mWm, mAtm, mSupervisor);
+        final DisplayRotationPolicyRobotTest robot =
+                new DisplayRotationPolicyRobotTest(mWm, mAtm, mSupervisor);
         consumer.accept(robot);
     }
 
-    private static class CameraPolicyRobotTest extends AppCompatRobotBase {
+    @Test
+    public void testIsCameraCompatTreatmentActive_whenTreatmentForTopActivityIsEnabled() {
+        runTestScenario((robot) -> {
+            robot.applyOnActivity((a)-> {
+                a.createActivityWithComponent();
+                a.enableTreatmentForTopActivity(/* enabled */ true);
+            });
 
-        private final WindowManagerService mWm;
+            robot.checkIsCameraCompatTreatmentActiveForTopActivity(/* active */ true);
+        });
+    }
 
-        CameraPolicyRobotTest(@NonNull WindowManagerService wm,
+    @Test
+    public void testIsCameraCompatTreatmentNotActive_whenTreatmentForTopActivityIsDisabled() {
+        runTestScenario((robot) -> {
+            robot.applyOnActivity((a)-> {
+                a.createActivityWithComponent();
+                a.enableTreatmentForTopActivity(/* enabled */ false);
+            });
+
+            robot.checkIsCameraCompatTreatmentActiveForTopActivity(/* active */ false);
+        });
+    }
+
+    private static class DisplayRotationPolicyRobotTest extends AppCompatRobotBase {
+
+        DisplayRotationPolicyRobotTest(@NonNull WindowManagerService wm,
                 @NonNull ActivityTaskManagerService atm,
                 @NonNull ActivityTaskSupervisor supervisor) {
             super(wm, atm, supervisor);
-            mWm = wm;
-            spyOn(mWm);
         }
 
-        void activateCamera(boolean isCameraActive) {
-            doReturn(isCameraActive).when(activity().top()).isCameraActive();
+        void checkTopActivityHasDisplayRotationCompatPolicy(boolean exists) {
+            Assert.assertEquals(exists, activity().top().mDisplayContent
+                    .mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy());
         }
 
-        void recomputeConfigurationForCameraCompatIfNeeded() {
-            getAppCompatCameraPolicy().recomputeConfigurationForCameraCompatIfNeeded();
+        void checkTopActivityHasCameraCompatFreeformPolicy(boolean exists) {
+            Assert.assertEquals(exists, activity().top().mDisplayContent
+                    .mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
         }
 
-        void checkRecomputeConfigurationInvoked(boolean invoked) {
-            if (invoked) {
-                verify(activity().top()).recomputeConfiguration();
-            } else {
-                verify(activity().top(), never()).recomputeConfiguration();
-            }
+        void checkIsCameraCompatTreatmentActiveForTopActivity(boolean active) {
+            Assert.assertEquals(getTopAppCompatCameraPolicy()
+                    .isTreatmentEnabledForActivity(activity().top()), active);
         }
 
-        private AppCompatCameraPolicy getAppCompatCameraPolicy() {
-            return activity().top().mAppCompatController.getAppCompatCameraPolicy();
+        // TODO(b/350460645): Create Desktop Windowing Robot to reuse common functionalities.
+        void allowEnterDesktopMode(boolean isAllowed) {
+            doReturn(isAllowed).when(() ->
+                    DesktopModeLaunchParamsModifier.canEnterDesktopMode(any()));
+        }
+
+        private AppCompatCameraPolicy getTopAppCompatCameraPolicy() {
+            return activity().top().mDisplayContent.mAppCompatCameraPolicy;
         }
     }
-
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
index 1720b64..35c2ee0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
@@ -15,12 +15,8 @@
  */
 package com.android.server.wm;
 
-import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
 import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
-import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
-import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.server.wm.AppCompatOrientationOverrides.OrientationOverridesState.MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP;
@@ -31,7 +27,6 @@
 import static org.junit.Assert.assertTrue;
 
 import android.compat.testing.PlatformCompatChangeRule;
-import android.content.res.Configuration;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.annotation.NonNull;
@@ -62,82 +57,6 @@
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
 
     @Test
-    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
-    public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() {
-        runTestScenario((robot) -> {
-            robot.conf().enablePolicyForIgnoringRequestedOrientation(true);
-            robot.activity().createActivityWithComponent();
-            robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
-
-            robot.checkShouldIgnoreRequestedOrientation(/* expected */ true,
-                    /* requestedOrientation */ SCREEN_ORIENTATION_UNSPECIFIED);
-        });
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
-    public void testShouldIgnoreRequestedOrientation_cameraCompatTreatment_returnsTrue() {
-        runTestScenario((robot) -> {
-            robot.applyOnConf((c) -> {
-                c.enableCameraCompatTreatment(true);
-                c.enableCameraCompatTreatmentAtBuildTime(true);
-                c.enablePolicyForIgnoringRequestedOrientation(true);
-            });
-            robot.applyOnActivity((a) -> {
-                a.createActivityWithComponentInNewTask();
-                a.enableTreatmentForTopActivity(true);
-            });
-            robot.prepareRelaunchingAfterRequestedOrientationChanged(false);
-
-            robot.checkShouldIgnoreRequestedOrientation(/* expected */ true,
-                    /* requestedOrientation */ SCREEN_ORIENTATION_UNSPECIFIED);
-        });
-    }
-
-    @Test
-    public void testShouldIgnoreRequestedOrientation_overrideDisabled_returnsFalse() {
-        runTestScenario((robot) -> {
-            robot.conf().enablePolicyForIgnoringRequestedOrientation(true);
-
-            robot.activity().createActivityWithComponent();
-            robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
-
-            robot.checkShouldIgnoreRequestedOrientation(/* expected */ false,
-                    /* requestedOrientation */ SCREEN_ORIENTATION_UNSPECIFIED);
-        });
-    }
-
-    @Test
-    public void testShouldIgnoreRequestedOrientation_propertyIsTrue_returnsTrue() {
-        runTestScenario((robot) -> {
-            robot.conf().enablePolicyForIgnoringRequestedOrientation(true);
-            robot.prop().enable(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
-
-            robot.activity().createActivityWithComponent();
-            robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
-
-            robot.checkShouldIgnoreRequestedOrientation(/* expected */ true,
-                    /* requestedOrientation */ SCREEN_ORIENTATION_UNSPECIFIED);
-        });
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
-    public void testShouldIgnoreRequestedOrientation_propertyIsFalseAndOverride_returnsFalse()
-            throws Exception {
-        runTestScenario((robot) -> {
-            robot.conf().enablePolicyForIgnoringRequestedOrientation(true);
-            robot.prop().disable(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
-
-            robot.activity().createActivityWithComponent();
-            robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
-
-            robot.checkShouldIgnoreRequestedOrientation(/* expected */ false,
-                    /* requestedOrientation */ SCREEN_ORIENTATION_UNSPECIFIED);
-        });
-    }
-
-    @Test
     public void testShouldIgnoreOrientationRequestLoop_overrideDisabled_returnsFalse() {
         runTestScenario((robot) -> {
             robot.conf().enablePolicyForIgnoringRequestedOrientation(true);
@@ -239,21 +158,6 @@
         });
     }
 
-    @Test
-    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
-    public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() {
-        runTestScenario((robot) -> {
-            robot.conf().enablePolicyForIgnoringRequestedOrientation(true);
-            robot.applyOnActivity((a) -> {
-                a.createActivityWithComponent();
-                a.setLetterboxedForFixedOrientationAndAspectRatio(false);
-            });
-
-            robot.checkShouldIgnoreRequestedOrientation(/* expected */ false,
-                    /* requestedOrientation */ SCREEN_ORIENTATION_UNSPECIFIED);
-        });
-    }
-
     /**
      * Runs a test scenario providing a Robot.
      */
@@ -276,10 +180,6 @@
             mTestCurrentTimeMillisSupplier = new CurrentTimeMillisSupplierFake();
         }
 
-        void prepareRelaunchingAfterRequestedOrientationChanged(boolean enabled) {
-            getTopOrientationOverrides().setRelaunchingAfterRequestedOrientationChanged(enabled);
-        }
-
         // Useful to reduce timeout during tests
         void prepareMockedTime() {
             getTopOrientationOverrides().mOrientationOverridesState.mCurrentTimeMillisSupplier =
@@ -290,12 +190,6 @@
             mTestCurrentTimeMillisSupplier.delay(SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS);
         }
 
-        void checkShouldIgnoreRequestedOrientation(boolean expected,
-                @Configuration.Orientation int requestedOrientation) {
-            assertEquals(expected, getTopOrientationOverrides()
-                    .shouldIgnoreRequestedOrientation(requestedOrientation));
-        }
-
         void checkExpectedLoopCount(int expectedCount) {
             assertEquals(expectedCount, getTopOrientationOverrides()
                     .getSetOrientationRequestCounter());
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
index 9885a2d..aa520e9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
@@ -18,6 +18,8 @@
 
 import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
 import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
 import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
 import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
@@ -33,13 +35,16 @@
 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
+import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.verify;
 
 import android.compat.testing.PlatformCompatChangeRule;
 import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.annotation.NonNull;
@@ -266,7 +271,7 @@
                 c.enableCameraCompatTreatmentAtBuildTime(true);
             });
             robot.applyOnActivity((a) -> {
-                a.createActivityWithComponentInNewTask();
+                a.createActivityWithComponentInNewTaskAndDisplay();
                 a.setTopActivityEligibleForOrientationOverride(false);
             });
 
@@ -285,7 +290,7 @@
                 c.enableCameraCompatTreatmentAtBuildTime(true);
             });
             robot.applyOnActivity((a) -> {
-                a.createActivityWithComponentInNewTask();
+                a.createActivityWithComponentInNewTaskAndDisplay();
                 a.setTopActivityEligibleForOrientationOverride(true);
             });
 
@@ -315,7 +320,7 @@
                 c.enableCameraCompatTreatmentAtBuildTime(true);
             });
             robot.applyOnActivity((a) -> {
-                a.createActivityWithComponentInNewTask();
+                a.createActivityWithComponentInNewTaskAndDisplay();
                 a.setTopActivityCameraActive(false);
             });
 
@@ -398,6 +403,97 @@
         });
     }
 
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+    public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() {
+        runTestScenario((robot) -> {
+            robot.conf().enablePolicyForIgnoringRequestedOrientation(true);
+            robot.activity().createActivityWithComponent();
+            robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
+
+            robot.checkShouldIgnoreRequestedOrientation(/* expected */ true,
+                    /* requestedOrientation */ SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+    public void testShouldIgnoreRequestedOrientation_cameraCompatTreatment_returnsTrue() {
+        runTestScenario((robot) -> {
+            robot.applyOnConf((c) -> {
+                c.enableCameraCompatTreatment(true);
+                c.enableCameraCompatTreatmentAtBuildTime(true);
+                c.enablePolicyForIgnoringRequestedOrientation(true);
+            });
+            robot.applyOnActivity((a) -> {
+                a.createActivityWithComponentInNewTask();
+                a.enableTreatmentForTopActivity(true);
+            });
+            robot.prepareRelaunchingAfterRequestedOrientationChanged(false);
+
+            robot.checkShouldIgnoreRequestedOrientation(/* expected */ true,
+                    /* requestedOrientation */ SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    @Test
+    public void testShouldIgnoreRequestedOrientation_overrideDisabled_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.conf().enablePolicyForIgnoringRequestedOrientation(true);
+
+            robot.activity().createActivityWithComponent();
+            robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
+
+            robot.checkShouldIgnoreRequestedOrientation(/* expected */ false,
+                    /* requestedOrientation */ SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    @Test
+    public void testShouldIgnoreRequestedOrientation_propertyIsTrue_returnsTrue() {
+        runTestScenario((robot) -> {
+            robot.conf().enablePolicyForIgnoringRequestedOrientation(true);
+            robot.prop().enable(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+
+            robot.activity().createActivityWithComponent();
+            robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
+
+            robot.checkShouldIgnoreRequestedOrientation(/* expected */ true,
+                    /* requestedOrientation */ SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+    public void testShouldIgnoreRequestedOrientation_propertyIsFalseAndOverride_returnsFalse()
+            throws Exception {
+        runTestScenario((robot) -> {
+            robot.conf().enablePolicyForIgnoringRequestedOrientation(true);
+            robot.prop().disable(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+
+            robot.activity().createActivityWithComponent();
+            robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
+
+            robot.checkShouldIgnoreRequestedOrientation(/* expected */ false,
+                    /* requestedOrientation */ SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
+    public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.conf().enablePolicyForIgnoringRequestedOrientation(true);
+            robot.applyOnActivity((a) -> {
+                a.createActivityWithComponent();
+                a.setLetterboxedForFixedOrientationAndAspectRatio(false);
+            });
+
+            robot.checkShouldIgnoreRequestedOrientation(/* expected */ false,
+                    /* requestedOrientation */ SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
 
     /**
      * Runs a test scenario with an existing activity providing a Robot.
@@ -440,6 +536,10 @@
             }
         }
 
+        void prepareRelaunchingAfterRequestedOrientationChanged(boolean enabled) {
+            getTopOrientationOverrides().setRelaunchingAfterRequestedOrientationChanged(enabled);
+        }
+
         int overrideOrientationIfNeeded(@ActivityInfo.ScreenOrientation int candidate) {
             return activity().top().mAppCompatController.getOrientationPolicy()
                     .overrideOrientationIfNeeded(candidate);
@@ -451,12 +551,27 @@
 
         void checkOverrideOrientation(@ActivityInfo.ScreenOrientation int candidate,
                                       @ActivityInfo.ScreenOrientation int expected) {
-            Assert.assertEquals(expected, overrideOrientationIfNeeded(candidate));
+            assertEquals(expected, overrideOrientationIfNeeded(candidate));
         }
 
         void checkOverrideOrientationIsNot(@ActivityInfo.ScreenOrientation int candidate,
                                            @ActivityInfo.ScreenOrientation int notExpected) {
             Assert.assertNotEquals(notExpected, overrideOrientationIfNeeded(candidate));
         }
+
+        void checkShouldIgnoreRequestedOrientation(boolean expected,
+                @Configuration.Orientation int requestedOrientation) {
+            assertEquals(expected, getTopAppCompatOrientationPolicy()
+                    .shouldIgnoreRequestedOrientation(requestedOrientation));
+        }
+
+        private AppCompatOrientationOverrides getTopOrientationOverrides() {
+            return activity().top().mAppCompatController.getAppCompatOverrides()
+                    .getAppCompatOrientationOverrides();
+        }
+
+        private AppCompatOrientationPolicy getTopAppCompatOrientationPolicy() {
+            return activity().top().mAppCompatController.getOrientationPolicy();
+        }
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 6957502..5739a04 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -117,8 +117,8 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.util.ArraySet;
 import android.view.Display;
@@ -2832,7 +2832,7 @@
         doReturn(true).when(() ->
                 DesktopModeLaunchParamsModifier.canEnterDesktopMode(any()));
 
-        assertNotNull(createNewDisplay().mCameraCompatFreeformPolicy);
+        assertTrue(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
     }
 
     @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
@@ -2841,14 +2841,14 @@
         doReturn(true).when(() ->
                 DesktopModeLaunchParamsModifier.canEnterDesktopMode(any()));
 
-        assertNull(createNewDisplay().mCameraCompatFreeformPolicy);
+        assertFalse(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
     }
 
     @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
     @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     @Test
     public void desktopWindowingFlagNotEnabled_cameraCompatFreeformPolicyIsNull() {
-        assertNull(createNewDisplay().mCameraCompatFreeformPolicy);
+        assertFalse(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
     }
 
     private void removeRootTaskTests(Runnable runnable) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
index e3a8542..2e488d8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
@@ -1607,6 +1607,7 @@
                     .thenReturn(mMockDeviceStateManager);
 
             mDeviceStateController = mock(DeviceStateController.class);
+            mMockDisplayContent.mAppCompatCameraPolicy = mock(AppCompatCameraPolicy.class);
             mTarget = new TestDisplayRotation(mMockDisplayContent, mMockDisplayAddress,
                     mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext,
                     mDeviceStateController);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
index 7faf2aa..8cdb574 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
@@ -497,7 +497,8 @@
     public void testValidateFlags() {
         final Session session = getTestSession();
         try {
-            session.validateDragFlags(View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION);
+            session.validateDragFlags(View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION,
+                    0 /* callingUid */);
             fail("Expected failure without permission");
         } catch (SecurityException e) {
             // Expected failure
@@ -510,7 +511,8 @@
                 .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS));
         final Session session = createTestSession(mAtm);
         try {
-            session.validateDragFlags(View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION);
+            session.validateDragFlags(View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION,
+                    0 /* callingUid */);
             // Expected pass
         } catch (SecurityException e) {
             fail("Expected no failure with permission");
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index 74e2d44..51b3c48 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -636,8 +636,9 @@
     @Test
     @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
     public void shouldOverrideMinAspectRatioForCamera_overrideEnabled_returnsTrue() {
-        doReturn(true).when(mActivity).isCameraActive();
-        mController = new LetterboxUiController(mWm, mActivity);
+        mActivity = setUpActivityWithComponent();
+        doReturn(true).when(mActivity.mAppCompatController
+                .getAppCompatCameraOverrides()).isCameraActive();
 
         assertTrue(mActivity.mAppCompatController.getAppCompatCameraOverrides()
                 .shouldOverrideMinAspectRatioForCamera());
@@ -647,9 +648,10 @@
     @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
     public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideEnabled_returnsTrue()
             throws Exception {
-        doReturn(true).when(mActivity).isCameraActive();
         mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true);
-        mController = new LetterboxUiController(mWm, mActivity);
+        mActivity = setUpActivityWithComponent();
+        doReturn(true).when(mActivity.mAppCompatController
+                .getAppCompatCameraOverrides()).isCameraActive();
 
         assertTrue(mActivity.mAppCompatController.getAppCompatCameraOverrides()
                 .shouldOverrideMinAspectRatioForCamera());
@@ -659,9 +661,10 @@
     @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
     public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideEnabled_returnsFalse()
             throws Exception {
-        doReturn(false).when(mActivity).isCameraActive();
         mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true);
-        mController = new LetterboxUiController(mWm, mActivity);
+        mActivity = setUpActivityWithComponent();
+        doReturn(false).when(mActivity.mAppCompatController
+                .getAppCompatCameraOverrides()).isCameraActive();
 
         assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides()
                 .shouldOverrideMinAspectRatioForCamera());
@@ -671,9 +674,10 @@
     @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
     public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideDisabled_returnsFalse()
             throws Exception {
-        doReturn(true).when(mActivity).isCameraActive();
         mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true);
-        mController = new LetterboxUiController(mWm, mActivity);
+        mActivity = setUpActivityWithComponent();
+        doReturn(true).when(mActivity.mAppCompatController
+                .getAppCompatCameraOverrides()).isCameraActive();
 
         assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides()
                 .shouldOverrideMinAspectRatioForCamera());
@@ -682,8 +686,9 @@
     @Test
     @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
     public void shouldOverrideMinAspectRatioForCamera_overrideDisabled_returnsFalse() {
-        doReturn(true).when(mActivity).isCameraActive();
-        mController = new LetterboxUiController(mWm, mActivity);
+        mActivity = setUpActivityWithComponent();
+        doReturn(true).when(mActivity.mAppCompatController
+                .getAppCompatCameraOverrides()).isCameraActive();
 
         assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides()
                 .shouldOverrideMinAspectRatioForCamera());
@@ -694,7 +699,7 @@
     public void shouldOverrideMinAspectRatioForCamera_propertyFalse_overrideEnabled_returnsFalse()
             throws Exception {
         mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false);
-        mController = new LetterboxUiController(mWm, mActivity);
+        mActivity = setUpActivityWithComponent();
 
         assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides()
                 .shouldOverrideMinAspectRatioForCamera());
@@ -705,8 +710,11 @@
     public void shouldOverrideMinAspectRatioForCamera_propertyFalse_noOverride_returnsFalse()
             throws Exception {
         mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false);
-        doReturn(true).when(mActivity).isCameraActive();
-        mController = new LetterboxUiController(mWm, mActivity);
+
+        mActivity = setUpActivityWithComponent();
+
+        doReturn(true).when(mActivity.mAppCompatController
+                .getAppCompatCameraOverrides()).isCameraActive();
 
         assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides()
                 .shouldOverrideMinAspectRatioForCamera());
@@ -848,8 +856,8 @@
         assertEquals(1.5f, mController.getFixedOrientationLetterboxAspectRatio(
                 mActivity.getParent().getConfiguration()), /* delta */ 0.01);
 
-        spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
-        doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
+        spyOn(mDisplayContent.mAppCompatCameraPolicy);
+        doReturn(true).when(mDisplayContent.mAppCompatCameraPolicy)
                 .isTreatmentEnabledForActivity(eq(mActivity));
 
         assertEquals(mController.getSplitScreenAspectRatio(),
@@ -980,6 +988,7 @@
                 .setComponent(ComponentName.createRelative(mContext,
                         com.android.server.wm.LetterboxUiControllerTest.class.getName()))
                 .build();
+        spyOn(activity.mAppCompatController.getAppCompatCameraOverrides());
         return activity;
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java b/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java
index 35cff7a..7efbc88 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java
@@ -25,11 +25,11 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.protolog.ProtoLog;
 import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.ProtoLogImpl;
 import com.android.internal.protolog.common.IProtoLog;
 import com.android.internal.protolog.common.LogLevel;
+import com.android.internal.protolog.ProtoLog;
 
 import org.junit.After;
 import org.junit.Ignore;
@@ -53,6 +53,9 @@
         runWith(mockedProtoLog, this::testProtoLog);
         verify(mockedProtoLog).log(eq(LogLevel.ERROR), eq(ProtoLogGroup.TEST_GROUP),
                 anyInt(), eq(0b0010010111),
+                eq(com.android.internal.protolog.ProtoLogGroup.TEST_GROUP.isLogToLogcat()
+                        ? "Test completed successfully: %b %d %x %f %% %s"
+                        : null),
                 eq(new Object[]{true, 1L, 2L, 0.3, "ok"}));
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index c962a3f..4220f31 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -442,15 +442,7 @@
                 dc.getDisplayPolicy().release();
                 // Unregister SensorEventListener (foldable device may register for hinge angle).
                 dc.getDisplayRotation().onDisplayRemoved();
-                if (dc.mDisplayRotationCompatPolicy != null) {
-                    dc.mDisplayRotationCompatPolicy.dispose();
-                }
-                if (dc.mCameraCompatFreeformPolicy != null) {
-                    dc.mCameraCompatFreeformPolicy.dispose();
-                }
-                if (dc.mCameraStateMonitor != null) {
-                    dc.mCameraStateMonitor.dispose();
-                }
+                dc.mAppCompatCameraPolicy.dispose();
             }
         }
 
diff --git a/telephony/common/android/telephony/LocationAccessPolicy.java b/telephony/common/android/telephony/LocationAccessPolicy.java
index d4b6c91..feea55b 100644
--- a/telephony/common/android/telephony/LocationAccessPolicy.java
+++ b/telephony/common/android/telephony/LocationAccessPolicy.java
@@ -32,6 +32,8 @@
 import android.util.Log;
 import android.widget.Toast;
 
+import com.android.internal.telephony.TelephonyPermissions;
+import com.android.internal.telephony.flags.Flags;
 import com.android.internal.telephony.util.TelephonyUtils;
 
 /**
@@ -310,10 +312,18 @@
         // This avoid breaking legacy code that rely on public-facing APIs to access cell location,
         // and it doesn't create an info leak risk because the cell location is stored in the phone
         // process anyway, and the system server already has location access.
-        if (query.callingUid == Process.PHONE_UID || query.callingUid == Process.SYSTEM_UID
-                || query.callingUid == Process.NETWORK_STACK_UID
-                || query.callingUid == Process.ROOT_UID) {
-            return LocationPermissionResult.ALLOWED;
+        if (Flags.supportPhoneUidCheckForMultiuser()) {
+            if (TelephonyPermissions.isSystemOrPhone(query.callingUid)
+                    || UserHandle.isSameApp(query.callingUid, Process.NETWORK_STACK_UID)
+                    || UserHandle.isSameApp(query.callingUid, Process.ROOT_UID)) {
+                return LocationPermissionResult.ALLOWED;
+            }
+        } else {
+            if (query.callingUid == Process.PHONE_UID || query.callingUid == Process.SYSTEM_UID
+                    || query.callingUid == Process.NETWORK_STACK_UID
+                    || query.callingUid == Process.ROOT_UID) {
+                return LocationPermissionResult.ALLOWED;
+            }
         }
 
         // Check the system-wide requirements. If the location main switch is off and the caller is
diff --git a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.java b/telephony/java/android/telephony/satellite/ProvisionSubscriberId.java
index 796c82d..3e6f743 100644
--- a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.java
+++ b/telephony/java/android/telephony/satellite/ProvisionSubscriberId.java
@@ -43,12 +43,17 @@
     /** carrier id */
     private int mCarrierId;
 
+    /** apn */
+    private String mNiddApn;
+
     /**
      * @hide
      */
-    public ProvisionSubscriberId(@NonNull String subscriberId, @NonNull int carrierId) {
+    public ProvisionSubscriberId(@NonNull String subscriberId, @NonNull int carrierId,
+            @NonNull String niddApn) {
         this.mCarrierId = carrierId;
         this.mSubscriberId = subscriberId;
+        this.mNiddApn = niddApn;
     }
 
     private ProvisionSubscriberId(Parcel in) {
@@ -63,6 +68,7 @@
     public void writeToParcel(@NonNull Parcel out, int flags) {
         out.writeString(mSubscriberId);
         out.writeInt(mCarrierId);
+        out.writeString(mNiddApn);
     }
 
     @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
@@ -89,7 +95,7 @@
     }
 
     /**
-     * @return token.
+     * @return provision subscriberId.
      * @hide
      */
     @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
@@ -106,6 +112,15 @@
         return mCarrierId;
     }
 
+    /**
+     * @return niddApn.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public String getNiddApn() {
+        return mNiddApn;
+    }
+
     @NonNull
     @Override
     public String toString() {
@@ -117,12 +132,16 @@
 
         sb.append("CarrierId:");
         sb.append(mCarrierId);
+        sb.append(",");
+
+        sb.append("NiddApn:");
+        sb.append(mNiddApn);
         return sb.toString();
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mSubscriberId, mCarrierId);
+        return Objects.hash(mSubscriberId, mCarrierId, mNiddApn);
     }
 
     @Override
@@ -131,11 +150,12 @@
         if (o == null || getClass() != o.getClass()) return false;
         ProvisionSubscriberId that = (ProvisionSubscriberId) o;
         return mSubscriberId.equals(that.mSubscriberId) && mCarrierId
-                == that.mCarrierId;
+                == that.mCarrierId && mNiddApn.equals(that.mNiddApn);
     }
 
     private void readFromParcel(Parcel in) {
         mSubscriberId = in.readString();
         mCarrierId = in.readInt();
+        mNiddApn = in.readString();
     }
 }
diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
index 5a48327..5a27593 100644
--- a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
@@ -59,6 +59,7 @@
 import java.io.InputStream;
 import java.io.PrintWriter;
 import java.util.LinkedList;
+import java.util.TreeMap;
 
 /**
  * Test class for {@link ProtoLogImpl}.
@@ -89,7 +90,7 @@
         //noinspection ResultOfMethodCallIgnored
         mFile.delete();
         mProtoLog = new LegacyProtoLogImpl(mFile, mViewerConfigFilename,
-                1024 * 1024, mReader, 1024, () -> {});
+                1024 * 1024, mReader, 1024, new TreeMap<>(), () -> {});
     }
 
     @After
@@ -141,7 +142,7 @@
         TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
 
         implSpy.log(
-                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
                 new Object[]{true, 10000, 30000, "test", 0.000003});
 
         verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
@@ -158,7 +159,7 @@
         TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
 
         implSpy.log(
-                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
                 new Object[]{true, 10000, 0.0001, 0.00002, "test"});
 
         verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
@@ -175,7 +176,7 @@
         TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
 
         implSpy.log(
-                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
                 new Object[]{5});
 
         verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
@@ -191,7 +192,7 @@
         TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
 
         implSpy.log(
-                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
                 new Object[]{5});
 
         verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
@@ -207,7 +208,7 @@
         TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
 
         implSpy.log(
-                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
                 new Object[]{5});
 
         verify(implSpy, never()).passToLogcat(any(), any(), any());
@@ -276,7 +277,7 @@
         long before = SystemClock.elapsedRealtimeNanos();
         mProtoLog.log(
                 LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
-                0b1110101001010100,
+                0b1110101001010100, null,
                 new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true});
         long after = SystemClock.elapsedRealtimeNanos();
         mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
@@ -301,7 +302,7 @@
         long before = SystemClock.elapsedRealtimeNanos();
         mProtoLog.log(
                 LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
-                0b01100100,
+                0b01100100, null,
                 new Object[]{"test", 1, 0.1, true});
         long after = SystemClock.elapsedRealtimeNanos();
         mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
@@ -325,7 +326,7 @@
         TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
         mProtoLog.startProtoLog(mock(PrintWriter.class));
         mProtoLog.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
-                0b11, new Object[]{true});
+                0b11, null, new Object[]{true});
         mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
         try (InputStream is = new FileInputStream(mFile)) {
             ProtoInputStream ip = new ProtoInputStream(is);
diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
index b6672a0..1d7b6b3 100644
--- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
@@ -60,9 +60,6 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
-import perfetto.protos.Protolog;
-import perfetto.protos.ProtologCommon;
-
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
@@ -70,6 +67,9 @@
 import java.util.TreeMap;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import perfetto.protos.Protolog;
+import perfetto.protos.ProtologCommon;
+
 /**
  * Test class for {@link ProtoLogImpl}.
  */
@@ -111,9 +111,6 @@
         //noinspection ResultOfMethodCallIgnored
         mFile.delete();
 
-        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
-        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
-
         mViewerConfigBuilder = Protolog.ProtoLogViewerConfig.newBuilder()
                 .addGroups(
                         Protolog.ProtoLogViewerConfig.Group.newBuilder()
@@ -160,9 +157,8 @@
         mCacheUpdater = () -> {};
         mReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider));
         mProtoLog = new PerfettoProtoLogImpl(
-                viewerConfigInputStreamProvider, mReader,
+                viewerConfigInputStreamProvider, mReader, new TreeMap<>(),
                 () -> mCacheUpdater.run());
-        mProtoLog.registerGroups(TestProtoLogGroup.values());
     }
 
     @After
@@ -214,15 +210,15 @@
             // Shouldn't be logging anything except WTF unless explicitly requested in the group
             // override.
             mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
         } finally {
             traceMonitor.stop(mWriter);
         }
@@ -244,15 +240,15 @@
         try {
             traceMonitor.start();
             mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
         } finally {
             traceMonitor.stop(mWriter);
         }
@@ -278,15 +274,15 @@
         try {
             traceMonitor.start();
             mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
         } finally {
             traceMonitor.stop(mWriter);
         }
@@ -308,15 +304,15 @@
         try {
             traceMonitor.start();
             mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
             mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5,
-                    LogDataType.BOOLEAN, new Object[]{true});
+                    LogDataType.BOOLEAN, null, new Object[]{true});
         } finally {
             traceMonitor.stop(mWriter);
         }
@@ -333,14 +329,14 @@
     }
 
     @Test
-    public void log_logcatEnabled() {
+    public void log_logcatEnabledExternalMessage() {
         when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% 0x%x %s %f");
         PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog);
         TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
         TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
 
         implSpy.log(
-                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
                 new Object[]{true, 10000, 30000, "test", 0.000003});
 
         verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
@@ -357,17 +353,32 @@
         TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
 
         implSpy.log(
-                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
                 new Object[]{true, 10000, 0.0001, 0.00002, "test"});
 
         verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
                 LogLevel.INFO),
-                eq("FORMAT_ERROR \"test %b %d %% %x %s %f\", "
-                        + "args=(true, 10000, 1.0E-4, 2.0E-5, test)"));
+                eq("UNKNOWN MESSAGE (1234) true 10000 1.0E-4 2.0E-5 test"));
         verify(mReader).getViewerString(eq(1234L));
     }
 
     @Test
+    public void log_logcatEnabledInlineMessage() {
+        when(mReader.getViewerString(anyLong())).thenReturn("test %d");
+        PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+        implSpy.log(
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
+                new Object[]{5});
+
+        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+                LogLevel.INFO), eq("test 5"));
+        verify(mReader, never()).getViewerString(anyLong());
+    }
+
+    @Test
     public void log_logcatEnabledNoMessage() {
         when(mReader.getViewerString(anyLong())).thenReturn(null);
         PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog);
@@ -375,11 +386,11 @@
         TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
 
         implSpy.log(
-                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
                 new Object[]{5});
 
         verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
-                LogLevel.INFO), eq("UNKNOWN MESSAGE#1234 (5)"));
+                LogLevel.INFO), eq("UNKNOWN MESSAGE (1234) 5"));
         verify(mReader).getViewerString(eq(1234L));
     }
 
@@ -390,7 +401,7 @@
         TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
 
         implSpy.log(
-                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
                 new Object[]{5});
 
         verify(implSpy, never()).passToLogcat(any(), any(), any());
@@ -414,7 +425,7 @@
             before = SystemClock.elapsedRealtimeNanos();
             mProtoLog.log(
                     LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash,
-                    0b1110101001010100,
+                    0b1110101001010100, null,
                     new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true});
             after = SystemClock.elapsedRealtimeNanos();
         } finally {
@@ -433,38 +444,6 @@
                 .isEqualTo("My test message :: test, 2, 4, 6, 0.400000, 5.000000e-01, 0.6, true");
     }
 
-    @Test
-    public void log_noProcessing() throws IOException {
-        PerfettoTraceMonitor traceMonitor =
-                PerfettoTraceMonitor.newBuilder().enableProtoLog().build();
-        long before;
-        long after;
-        try {
-            traceMonitor.start();
-            assertTrue(mProtoLog.isProtoEnabled());
-
-            before = SystemClock.elapsedRealtimeNanos();
-            mProtoLog.log(
-                    LogLevel.INFO, TestProtoLogGroup.TEST_GROUP,
-                    "My test message :: %s, %d, %o, %x, %f, %b",
-                    "test", 1, 2, 3, 0.4, true);
-            after = SystemClock.elapsedRealtimeNanos();
-        } finally {
-            traceMonitor.stop(mWriter);
-        }
-
-        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
-        final ProtoLogTrace protolog = reader.readProtoLogTrace();
-
-        Truth.assertThat(protolog.messages).hasSize(1);
-        Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos())
-                .isAtLeast(before);
-        Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos())
-                .isAtMost(after);
-        Truth.assertThat(protolog.messages.getFirst().getMessage())
-                .isEqualTo("My test message :: test, 2, 4, 6, 0.400000, true");
-    }
-
     private long addMessageToConfig(ProtologCommon.ProtoLogLevel logLevel, String message) {
         final long messageId = new Random().nextLong();
         mViewerConfigBuilder.addMessages(Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
@@ -491,7 +470,7 @@
             before = SystemClock.elapsedRealtimeNanos();
             mProtoLog.log(
                     LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash,
-                    0b01100100,
+                    0b01100100, null,
                     new Object[]{"test", 1, 0.1, true});
             after = SystemClock.elapsedRealtimeNanos();
         } finally {
@@ -509,7 +488,7 @@
         try {
             traceMonitor.start();
             mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
-                    0b11, new Object[]{true});
+                    0b11, null, new Object[]{true});
         } finally {
             traceMonitor.stop(mWriter);
         }
@@ -533,7 +512,7 @@
 
             ProtoLogImpl.setSingleInstance(mProtoLog);
             ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1,
-                    0b11, true);
+                    0b11, null, true);
         } finally {
             traceMonitor.stop(mWriter);
         }
@@ -607,7 +586,7 @@
                 .isFalse();
         Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR))
                 .isFalse();
-        Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)).isFalse();
+        Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)).isTrue();
 
         PerfettoTraceMonitor traceMonitor1 =
                 PerfettoTraceMonitor.newBuilder().enableProtoLog(true,
@@ -685,53 +664,7 @@
         Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR))
                 .isFalse();
         Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF))
-                .isFalse();
-    }
-
-    @Test
-    public void supportsNullString() throws IOException {
-        PerfettoTraceMonitor traceMonitor =
-                PerfettoTraceMonitor.newBuilder().enableProtoLog(true)
-                        .build();
-
-        try {
-            traceMonitor.start();
-
-            mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP,
-                    "My test null string: %s", null);
-        } finally {
-            traceMonitor.stop(mWriter);
-        }
-
-        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
-        final ProtoLogTrace protolog = reader.readProtoLogTrace();
-
-        Truth.assertThat(protolog.messages).hasSize(1);
-        Truth.assertThat(protolog.messages.get(0).getMessage())
-                .isEqualTo("My test null string: null");
-    }
-
-    @Test
-    public void supportNullParams() throws IOException {
-        PerfettoTraceMonitor traceMonitor =
-                PerfettoTraceMonitor.newBuilder().enableProtoLog(true)
-                        .build();
-
-        try {
-            traceMonitor.start();
-
-            mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP,
-                    "My null args: %d, %f, %b", null, null, null);
-        } finally {
-            traceMonitor.stop(mWriter);
-        }
-
-        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
-        final ProtoLogTrace protolog = reader.readProtoLogTrace();
-
-        Truth.assertThat(protolog.messages).hasSize(1);
-        Truth.assertThat(protolog.messages.get(0).getMessage())
-                .isEqualTo("My null args: 0, 0, false");
+                .isTrue();
     }
 
     private enum TestProtoLogGroup implements IProtoLogGroup {
diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java
index 0496240..60456f9 100644
--- a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java
@@ -58,50 +58,51 @@
     public void d_logCalled() {
         IProtoLog mockedProtoLog = mock(IProtoLog.class);
         ProtoLogImpl.setSingleInstance(mockedProtoLog);
-        ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1234, 4321);
+        ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d");
         verify(mockedProtoLog).log(eq(LogLevel.DEBUG), eq(
                 TestProtoLogGroup.TEST_GROUP),
-                eq(1234L), eq(4321), eq(new Object[]{}));
+                eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
     }
 
     @Test
     public void v_logCalled() {
         IProtoLog mockedProtoLog = mock(IProtoLog.class);
         ProtoLogImpl.setSingleInstance(mockedProtoLog);
-        ProtoLogImpl.v(TestProtoLogGroup.TEST_GROUP, 1234, 4321);
+        ProtoLogImpl.v(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d");
         verify(mockedProtoLog).log(eq(LogLevel.VERBOSE), eq(
                 TestProtoLogGroup.TEST_GROUP),
-                eq(1234L), eq(4321), eq(new Object[]{}));
+                eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
     }
 
     @Test
     public void i_logCalled() {
         IProtoLog mockedProtoLog = mock(IProtoLog.class);
         ProtoLogImpl.setSingleInstance(mockedProtoLog);
-        ProtoLogImpl.i(TestProtoLogGroup.TEST_GROUP, 1234, 4321);
+        ProtoLogImpl.i(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d");
         verify(mockedProtoLog).log(eq(LogLevel.INFO), eq(
                 TestProtoLogGroup.TEST_GROUP),
-                eq(1234L), eq(4321), eq(new Object[]{}));
+                eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
     }
 
     @Test
     public void w_logCalled() {
         IProtoLog mockedProtoLog = mock(IProtoLog.class);
         ProtoLogImpl.setSingleInstance(mockedProtoLog);
-        ProtoLogImpl.w(TestProtoLogGroup.TEST_GROUP, 1234, 4321);
+        ProtoLogImpl.w(TestProtoLogGroup.TEST_GROUP, 1234,
+                4321, "test %d");
         verify(mockedProtoLog).log(eq(LogLevel.WARN), eq(
                 TestProtoLogGroup.TEST_GROUP),
-                eq(1234L), eq(4321), eq(new Object[]{}));
+                eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
     }
 
     @Test
     public void e_logCalled() {
         IProtoLog mockedProtoLog = mock(IProtoLog.class);
         ProtoLogImpl.setSingleInstance(mockedProtoLog);
-        ProtoLogImpl.e(TestProtoLogGroup.TEST_GROUP, 1234, 4321);
+        ProtoLogImpl.e(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d");
         verify(mockedProtoLog).log(eq(LogLevel.ERROR), eq(
                 TestProtoLogGroup.TEST_GROUP),
-                eq(1234L), eq(4321), eq(new Object[]{}));
+                eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
     }
 
     @Test
@@ -109,10 +110,10 @@
         IProtoLog mockedProtoLog = mock(IProtoLog.class);
         ProtoLogImpl.setSingleInstance(mockedProtoLog);
         ProtoLogImpl.wtf(TestProtoLogGroup.TEST_GROUP,
-                1234, 4321);
+                1234, 4321, "test %d");
         verify(mockedProtoLog).log(eq(LogLevel.WTF), eq(
                 TestProtoLogGroup.TEST_GROUP),
-                eq(1234L), eq(4321), eq(new Object[]{}));
+                eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
     }
 
     private enum TestProtoLogGroup implements IProtoLogGroup {
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt
index 3c99e68..1087ae6 100644
--- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt
@@ -120,8 +120,6 @@
 
                         logCallVisitor?.processCall(call, messageString, getLevelForMethodName(
                             call.name.toString(), call, context), groupMap.getValue(groupName))
-                    } else if (call.name.id == "initialize") {
-                        // No processing
                     } else {
                         // Process non-log message calls
                         otherCallVisitor?.processCall(call)
diff --git a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt
index c478f58..6a8a071 100644
--- a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt
@@ -130,27 +130,28 @@
         val hash = CodeUtils.hash(packagePath, messageString, level, group)
 
         val newCall = call.clone()
-        // Remove message string.
-        // Out: ProtoLog.e(GROUP, args)
-        newCall.arguments.removeAt(1)
+        if (!group.textEnabled) {
+            // Remove message string if text logging is not enabled by default.
+            // Out: ProtoLog.e(GROUP, null, arg)
+            newCall.arguments[1].replace(NameExpr("null"))
+        }
         // Insert message string hash as a second argument.
-        // Out: ProtoLog.e(GROUP, 1234, args)
+        // Out: ProtoLog.e(GROUP, 1234, null, arg)
         newCall.arguments.add(1, LongLiteralExpr("" + hash + "L"))
         val argTypes = LogDataType.parseFormatString(messageString)
         val typeMask = LogDataType.logDataTypesToBitMask(argTypes)
         // Insert bitmap representing which Number parameters are to be considered as
         // floating point numbers.
-        // Out: ProtoLog.e(GROUP, 1234, 0, args)
+        // Out: ProtoLog.e(GROUP, 1234, 0, null, arg)
         newCall.arguments.add(2, IntegerLiteralExpr(typeMask))
         // Replace call to a stub method with an actual implementation.
-        // Out: ProtoLogImpl.e(GROUP, 1234, 0, args)
+        // Out: ProtoLogImpl.e(GROUP, 1234, null, arg)
         newCall.setScope(protoLogImplClassNode)
         if (argTypes.size != call.arguments.size - 2) {
             throw InvalidProtoLogCallException(
                 "Number of arguments (${argTypes.size} does not match format" +
                         " string in: $call", ParsingContext(path, call))
         }
-        val argsOffset = 3
         val blockStmt = BlockStmt()
         if (argTypes.isNotEmpty()) {
             // Assign every argument to a variable to check its type in compile time
@@ -159,9 +160,9 @@
             argTypes.forEachIndexed { idx, type ->
                 val varName = "protoLogParam$idx"
                 val declaration = VariableDeclarator(getASTTypeForDataType(type), varName,
-                    getConversionForType(type)(newCall.arguments[idx + argsOffset].clone()))
+                    getConversionForType(type)(newCall.arguments[idx + 4].clone()))
                 blockStmt.addStatement(ExpressionStmt(VariableDeclarationExpr(declaration)))
-                newCall.setArgument(idx + argsOffset, NameExpr(SimpleName(varName)))
+                newCall.setArgument(idx + 4, NameExpr(SimpleName(varName)))
             }
         } else {
             // Assign (Object[])null as the vararg parameter to prevent allocating an empty
diff --git a/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
index 0cbbd48..2a83677 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
@@ -60,7 +60,7 @@
                 .containsMatch(Pattern.compile("\\{ String protoLogParam0 = " +
                         "String\\.valueOf\\(argString\\); long protoLogParam1 = argInt; " +
                         "com\\.android\\.internal\\.protolog.ProtoLogImpl_.*\\.d\\(" +
-                        "GROUP, -6872339441335321086L, 4, protoLogParam0, protoLogParam1" +
+                        "GROUP, -6872339441335321086L, 4, null, protoLogParam0, protoLogParam1" +
                         "\\); \\}"))
     }
 
diff --git a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
index 6cde7a7..82aa93d 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
@@ -76,7 +76,7 @@
 
             class Test {
                 void test() {
-                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, protoLogParam0, protoLogParam1); }
+                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); }
                 }
             }
             """.trimIndent()
@@ -86,7 +86,7 @@
 
             class Test {
                 void test() {
-                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, protoLogParam0, protoLogParam1, protoLogParam2); 
+                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, "test %d %f " + "abc %s\n test", protoLogParam0, protoLogParam1, protoLogParam2); 
             
             }
                 }
@@ -98,8 +98,8 @@
 
             class Test {
                 void test() {
-                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, protoLogParam0, protoLogParam1); } /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, protoLogParam0, protoLogParam1); }
-                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, protoLogParam0, protoLogParam1); }
+                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); }
+                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); }
                 }
             }
             """.trimIndent()
@@ -109,7 +109,7 @@
 
             class Test {
                 void test() {
-                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { org.example.ProtoLogImpl.w(TEST_GROUP, 3218600869538902408L, 0, (Object[]) null); }
+                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { org.example.ProtoLogImpl.w(TEST_GROUP, 3218600869538902408L, 0, "test", (Object[]) null); }
                 }
             }
             """.trimIndent()
@@ -119,7 +119,7 @@
 
             class Test {
                 void test() {
-                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, protoLogParam0, protoLogParam1); }
+                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, null, protoLogParam0, protoLogParam1); }
                 }
             }
             """.trimIndent()
@@ -129,7 +129,7 @@
 
             class Test {
                 void test() {
-                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, protoLogParam0, protoLogParam1, protoLogParam2); 
+                    if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, null, protoLogParam0, protoLogParam1, protoLogParam2); 
             
             }
                 }
@@ -172,12 +172,13 @@
         Truth.assertThat(protoLogCalls).hasSize(1)
         val methodCall = protoLogCalls[0] as MethodCallExpr
         assertEquals("w", methodCall.name.asString())
-        assertEquals(5, methodCall.arguments.size)
+        assertEquals(6, methodCall.arguments.size)
         assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
         assertEquals("-1473209266730422156L", methodCall.arguments[1].toString())
         assertEquals(0b1001.toString(), methodCall.arguments[2].toString())
-        assertEquals("protoLogParam0", methodCall.arguments[3].toString())
-        assertEquals("protoLogParam1", methodCall.arguments[4].toString())
+        assertEquals("\"test %d %f\"", methodCall.arguments[3].toString())
+        assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+        assertEquals("protoLogParam1", methodCall.arguments[5].toString())
         assertEquals(TRANSFORMED_CODE_TEXT_ENABLED, out)
     }
 
@@ -213,12 +214,13 @@
         Truth.assertThat(protoLogCalls).hasSize(3)
         val methodCall = protoLogCalls[0] as MethodCallExpr
         assertEquals("w", methodCall.name.asString())
-        assertEquals(5, methodCall.arguments.size)
+        assertEquals(6, methodCall.arguments.size)
         assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
         assertEquals("-1473209266730422156L", methodCall.arguments[1].toString())
         assertEquals(0b1001.toString(), methodCall.arguments[2].toString())
-        assertEquals("protoLogParam0", methodCall.arguments[3].toString())
-        assertEquals("protoLogParam1", methodCall.arguments[4].toString())
+        assertEquals("\"test %d %f\"", methodCall.arguments[3].toString())
+        assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+        assertEquals("protoLogParam1", methodCall.arguments[5].toString())
         assertEquals(TRANSFORMED_CODE_MULTICALL_TEXT, out)
     }
 
@@ -250,13 +252,13 @@
         Truth.assertThat(protoLogCalls).hasSize(1)
         val methodCall = protoLogCalls[0] as MethodCallExpr
         assertEquals("w", methodCall.name.asString())
-        assertEquals(6, methodCall.arguments.size)
+        assertEquals(7, methodCall.arguments.size)
         assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
         assertEquals("-4447034859795564700L", methodCall.arguments[1].toString())
         assertEquals(0b001001.toString(), methodCall.arguments[2].toString())
-        assertEquals("protoLogParam0", methodCall.arguments[3].toString())
-        assertEquals("protoLogParam1", methodCall.arguments[4].toString())
-        assertEquals("protoLogParam2", methodCall.arguments[5].toString())
+        assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+        assertEquals("protoLogParam1", methodCall.arguments[5].toString())
+        assertEquals("protoLogParam2", methodCall.arguments[6].toString())
         assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_ENABLED, out)
     }
 
@@ -287,7 +289,7 @@
         Truth.assertThat(protoLogCalls).hasSize(1)
         val methodCall = protoLogCalls[0] as MethodCallExpr
         assertEquals("w", methodCall.name.asString())
-        assertEquals(4, methodCall.arguments.size)
+        assertEquals(5, methodCall.arguments.size)
         assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
         assertEquals("3218600869538902408L", methodCall.arguments[1].toString())
         assertEquals(0.toString(), methodCall.arguments[2].toString())
@@ -321,12 +323,13 @@
         Truth.assertThat(protoLogCalls).hasSize(1)
         val methodCall = protoLogCalls[0] as MethodCallExpr
         assertEquals("w", methodCall.name.asString())
-        assertEquals(5, methodCall.arguments.size)
+        assertEquals(6, methodCall.arguments.size)
         assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
         assertEquals("-1473209266730422156L", methodCall.arguments[1].toString())
         assertEquals(0b1001.toString(), methodCall.arguments[2].toString())
-        assertEquals("protoLogParam0", methodCall.arguments[3].toString())
-        assertEquals("protoLogParam1", methodCall.arguments[4].toString())
+        assertEquals("null", methodCall.arguments[3].toString())
+        assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+        assertEquals("protoLogParam1", methodCall.arguments[5].toString())
         assertEquals(TRANSFORMED_CODE_TEXT_DISABLED, out)
     }
 
@@ -358,13 +361,14 @@
         Truth.assertThat(protoLogCalls).hasSize(1)
         val methodCall = protoLogCalls[0] as MethodCallExpr
         assertEquals("w", methodCall.name.asString())
-        assertEquals(6, methodCall.arguments.size)
+        assertEquals(7, methodCall.arguments.size)
         assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
         assertEquals("-4447034859795564700L", methodCall.arguments[1].toString())
         assertEquals(0b001001.toString(), methodCall.arguments[2].toString())
-        assertEquals("protoLogParam0", methodCall.arguments[3].toString())
-        assertEquals("protoLogParam1", methodCall.arguments[4].toString())
-        assertEquals("protoLogParam2", methodCall.arguments[5].toString())
+        assertEquals("null", methodCall.arguments[3].toString())
+        assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+        assertEquals("protoLogParam1", methodCall.arguments[5].toString())
+        assertEquals("protoLogParam2", methodCall.arguments[6].toString())
         assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_DISABLED, out)
     }
 }