Merge "Add new verbose vendor logging strings" into main
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 5bc0ddc..e2bee64 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -122,6 +122,7 @@
     boolean onlyHasDefaultChannel(String pkg, int uid);
     boolean areChannelsBypassingDnd();
     ParceledListSlice getNotificationChannelsBypassingDnd(String pkg, int uid);
+    List<String> getPackagesBypassingDnd(int userId, boolean includeConversationChannels);
     boolean isPackagePaused(String pkg);
     void deleteNotificationHistoryItem(String pkg, int uid, long postedTime);
     boolean isPermissionFixed(String pkg, int userId);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index a1fa404..a1c4267 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -6222,6 +6222,7 @@
                 if (appIconRes != 0) {
                     mN.mAppIcon = Icon.createWithResource(mContext, appIconRes);
                     contentView.setImageViewIcon(R.id.icon, mN.mAppIcon);
+                    contentView.setBoolean(R.id.icon, "setShouldShowAppIcon", true);
                     usingAppIcon = true;
                 } else {
                     Log.w(TAG, "bindSmallIcon: could not get the app icon");
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 67752f2..fb0ce0d 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -13725,6 +13725,13 @@
      * {@link #EXTRA_PROVISIONING_SENSORS_PERMISSION_GRANT_OPT_OUT} in the provisioning parameters.
      * In that case the device owner's control will be limited to denying these permissions.
      * <p>
+     * When sensor-related permissions aren't grantable due to the above cases, calling this method
+     * to grant these permissions will silently fail, if device admins are built with
+     * {@code targetSdkVersion} &lt; {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}. If
+     * they are built with {@code targetSdkVersion} &gt;=
+     * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, this method will throw a
+     * {@link SecurityException}.
+     * <p>
      * NOTE: On devices running {@link android.os.Build.VERSION_CODES#S} and above, control over
      * the following permissions are restricted for managed profile owners:
      * <ul>
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 7d5806a..8227112 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -383,3 +383,13 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+    name: "management_mode_policy_metrics"
+    namespace: "enterprise"
+    description: "Enabling management mode and password complexity policy metrics collection"
+    bug: "293091314"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 1ebced5..52000d9 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -2954,6 +2954,15 @@
      * There is a second caption drawn underneath it that will be fast enough. By default the
      * caption is constructed from the theme. You can provide a drawable, that will be drawn instead
      * to better match your application.
+     *
+     * Starting in Android 15, this API is a no-op. New window decorations introduced in Android 14
+     * are drawn in SystemUI process, and OEMs are responsible to make them responsive to resizing.
+     * There is no need to set a background drawable to improve UX anymore since then. Additionally,
+     * the foremost activity can draw in caption areas starting in Android 15. Check
+     * {@link WindowInsetsController#APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND},
+     * {@link WindowInsetsController#APPEARANCE_LIGHT_CAPTION_BARS},
+     * {@link WindowInsetsController#setSystemBarsAppearance(int, int)} and
+     * {@link WindowInsets#getBoundingRects(int)}.
      */
     public abstract void setResizingCaptionDrawable(Drawable drawable);
 
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index d74867c..724e8fa 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -18,7 +18,6 @@
 import static android.view.contentcapture.ContentCaptureHelper.sDebug;
 import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
 import static android.view.contentcapture.ContentCaptureHelper.toSet;
-import static android.view.contentcapture.flags.Flags.runOnBackgroundThreadEnabled;
 
 import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
@@ -602,26 +601,16 @@
     public ContentCaptureSession getMainContentCaptureSession() {
         synchronized (mLock) {
             if (mMainSession == null) {
-                mMainSession = prepareMainSession();
-                if (sVerbose) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession);
-            }
-            return mMainSession;
-        }
-    }
-
-    @NonNull
-    @GuardedBy("mLock")
-    private ContentCaptureSession prepareMainSession() {
-        if (runOnBackgroundThreadEnabled()) {
-            return new MainContentCaptureSessionV2(
+                mMainSession = new MainContentCaptureSession(
                     mContext,
                     this,
                     prepareUiHandler(),
                     prepareContentCaptureHandler(),
                     mService
-            );
-        } else {
-            return new MainContentCaptureSession(mContext, this, prepareUiHandler(), mService);
+                );
+                if (sVerbose) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession);
+            }
+            return mMainSession;
         }
     }
 
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index a90c94e..eb827dd 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -69,16 +69,13 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.NoSuchElementException;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
-// TODO(b/309411951): Replace V2 as the only main session once the experiment is done.
 /**
  * Main session associated with a context.
  *
- * <p>This session is created when the activity starts and finished when it stops; clients can use
- * it to create children activities.
- *
  * @hide
  */
 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@@ -107,7 +104,10 @@
     private final ContentCaptureManager mManager;
 
     @NonNull
-    private final Handler mHandler;
+    private final Handler mUiHandler;
+
+    @NonNull
+    private final Handler mContentCaptureHandler;
 
     /**
      * Interface to the system_server binder object - it's only used to start the session (and
@@ -142,6 +142,18 @@
     public ComponentName mComponentName;
 
     /**
+     * Thread-safe queue of events held to be processed as a batch.
+     *
+     * Because it is not guaranteed that the events will be enqueued from a single thread, the
+     * implementation must be thread-safe to prevent unexpected behaviour.
+     *
+     * @hide
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    @NonNull
+    public final ConcurrentLinkedQueue<ContentCaptureEvent> mEventProcessQueue;
+
+    /**
      * List of events held to be sent to the {@link ContentCaptureService} as a batch.
      *
      * @hide
@@ -200,14 +212,14 @@
                 binder = resultData.getBinder(EXTRA_BINDER);
                 if (binder == null) {
                     Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result");
-                    mainSession.mHandler.post(() -> mainSession.resetSession(
+                    mainSession.runOnContentCaptureThread(() -> mainSession.resetSession(
                             STATE_DISABLED | STATE_INTERNAL_ERROR));
                     return;
                 }
             } else {
                 binder = null;
             }
-            mainSession.mHandler.post(() ->
+            mainSession.runOnContentCaptureThread(() ->
                     mainSession.onSessionStarted(resultCode, binder));
         }
     }
@@ -217,17 +229,21 @@
     public MainContentCaptureSession(
             @NonNull ContentCaptureManager.StrippedContext context,
             @NonNull ContentCaptureManager manager,
-            @NonNull Handler handler,
+            @NonNull Handler uiHandler,
+            @NonNull Handler contentCaptureHandler,
             @NonNull IContentCaptureManager systemServerInterface) {
         mContext = context;
         mManager = manager;
-        mHandler = handler;
+        mUiHandler = uiHandler;
+        mContentCaptureHandler = contentCaptureHandler;
         mSystemServerInterface = systemServerInterface;
 
         final int logHistorySize = mManager.mOptions.logHistorySize;
         mFlushHistory = logHistorySize > 0 ? new LocalLog(logHistorySize) : null;
 
         mSessionStateReceiver = new SessionStateReceiver(this);
+
+        mEventProcessQueue = new ConcurrentLinkedQueue<>();
     }
 
     @Override
@@ -248,7 +264,13 @@
     @Override
     void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken,
             @NonNull ComponentName component, int flags) {
-        checkOnUiThread();
+        runOnContentCaptureThread(
+                () -> startImpl(token, shareableActivityToken, component, flags));
+    }
+
+    private void startImpl(@NonNull IBinder token, @NonNull IBinder shareableActivityToken,
+               @NonNull ComponentName component, int flags) {
+        checkOnContentCaptureThread();
         if (!isContentCaptureEnabled()) return;
 
         if (sVerbose) {
@@ -282,17 +304,15 @@
             Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e);
         }
     }
-
     @Override
     void onDestroy() {
-        mHandler.removeMessages(MSG_FLUSH);
-        mHandler.post(() -> {
+        clearAndRunOnContentCaptureThread(() -> {
             try {
                 flush(FLUSH_REASON_SESSION_FINISHED);
             } finally {
                 destroySession();
             }
-        });
+        }, MSG_FLUSH);
     }
 
     /**
@@ -305,7 +325,7 @@
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
     public void onSessionStarted(int resultCode, @Nullable IBinder binder) {
-        checkOnUiThread();
+        checkOnContentCaptureThread();
         if (binder != null) {
             mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
             mDirectServiceVulture = () -> {
@@ -324,7 +344,7 @@
             mContentProtectionEventProcessor =
                     new ContentProtectionEventProcessor(
                             mManager.getContentProtectionEventBuffer(),
-                            mHandler,
+                            mContentCaptureHandler,
                             mSystemServerInterface,
                             mComponentName.getPackageName(),
                             mManager.mOptions.contentProtectionOptions);
@@ -354,7 +374,7 @@
     }
 
     private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
-        checkOnUiThread();
+        checkOnContentCaptureThread();
         final int eventType = event.getType();
         if (sVerbose) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event);
         if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED
@@ -398,14 +418,14 @@
     }
 
     private void sendContentProtectionEvent(@NonNull ContentCaptureEvent event) {
-        checkOnUiThread();
+        checkOnContentCaptureThread();
         if (mContentProtectionEventProcessor != null) {
             mContentProtectionEventProcessor.processEvent(event);
         }
     }
 
     private void sendContentCaptureEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
-        checkOnUiThread();
+        checkOnContentCaptureThread();
         final int eventType = event.getType();
         final int maxBufferSize = mManager.mOptions.maxBufferSize;
         if (mEvents == null) {
@@ -540,12 +560,12 @@
     }
 
     private boolean hasStarted() {
-        checkOnUiThread();
+        checkOnContentCaptureThread();
         return mState != UNKNOWN_STATE;
     }
 
     private void scheduleFlush(@FlushReason int reason, boolean checkExisting) {
-        checkOnUiThread();
+        checkOnContentCaptureThread();
         if (sVerbose) {
             Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason)
                     + ", checkExisting=" + checkExisting);
@@ -562,9 +582,9 @@
                     + "when disabled. events=" + (mEvents == null ? null : mEvents.size()));
             return;
         }
-        if (checkExisting && mHandler.hasMessages(MSG_FLUSH)) {
+        if (checkExisting && mContentCaptureHandler.hasMessages(MSG_FLUSH)) {
             // "Renew" the flush message by removing the previous one
-            mHandler.removeMessages(MSG_FLUSH);
+            mContentCaptureHandler.removeMessages(MSG_FLUSH);
         }
 
         final int flushFrequencyMs;
@@ -586,12 +606,12 @@
                     + flushFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush));
         }
         // Post using a Runnable directly to trim a few μs from PooledLambda.obtainMessage()
-        mHandler.postDelayed(() ->
+        mContentCaptureHandler.postDelayed(() ->
                 flushIfNeeded(reason), MSG_FLUSH, flushFrequencyMs);
     }
 
     private void flushIfNeeded(@FlushReason int reason) {
-        checkOnUiThread();
+        checkOnContentCaptureThread();
         if (mEvents == null || mEvents.isEmpty()) {
             if (sVerbose) Log.v(TAG, "Nothing to flush");
             return;
@@ -603,7 +623,11 @@
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     @Override
     public void flush(@FlushReason int reason) {
-        checkOnUiThread();
+        runOnContentCaptureThread(() -> flushImpl(reason));
+    }
+
+    private void flushImpl(@FlushReason int reason) {
+        checkOnContentCaptureThread();
         if (mEvents == null || mEvents.size() == 0) {
             if (sVerbose) {
                 Log.v(TAG, "Don't flush for empty event buffer.");
@@ -626,7 +650,7 @@
                 Log.v(TAG, "handleForceFlush(" + getDebugState(reason) + "): hold your horses, "
                         + "client not ready: " + mEvents);
             }
-            if (!mHandler.hasMessages(MSG_FLUSH)) {
+            if (!mContentCaptureHandler.hasMessages(MSG_FLUSH)) {
                 scheduleFlush(reason, /* checkExisting= */ false);
             }
             return;
@@ -652,7 +676,7 @@
             mFlushHistory.log(logRecord);
         }
         try {
-            mHandler.removeMessages(MSG_FLUSH);
+            mContentCaptureHandler.removeMessages(MSG_FLUSH);
 
             final ParceledListSlice<ContentCaptureEvent> events = clearEvents();
             mDirectServiceInterface.sendEvents(events, reason, mManager.mOptions);
@@ -672,7 +696,7 @@
      */
     @NonNull
     private ParceledListSlice<ContentCaptureEvent> clearEvents() {
-        checkOnUiThread();
+        checkOnContentCaptureThread();
         // NOTE: we must save a reference to the current mEvents and then set it to to null,
         // otherwise clearing it would clear it in the receiving side if the service is also local.
         if (mEvents == null) {
@@ -687,7 +711,7 @@
     /** hide */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
     public void destroySession() {
-        checkOnUiThread();
+        checkOnContentCaptureThread();
         if (sDebug) {
             Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
                     + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
@@ -707,6 +731,7 @@
         }
         mDirectServiceInterface = null;
         mContentProtectionEventProcessor = null;
+        mEventProcessQueue.clear();
     }
 
     // TODO(b/122454205): once we support multiple sessions, we might need to move some of these
@@ -714,7 +739,7 @@
     /** @hide */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
     public void resetSession(int newState) {
-        checkOnUiThread();
+        checkOnContentCaptureThread();
         if (sVerbose) {
             Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
                     + getStateAsString(mState) + " to " + getStateAsString(newState));
@@ -735,21 +760,21 @@
         }
         mDirectServiceInterface = null;
         mContentProtectionEventProcessor = null;
-        mHandler.removeMessages(MSG_FLUSH);
+        mContentCaptureHandler.removeMessages(MSG_FLUSH);
     }
 
     @Override
     void internalNotifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node) {
         final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
                 .setViewNode(node.mNode);
-        mHandler.post(() -> sendEvent(event));
+        enqueueEvent(event);
     }
 
     @Override
     void internalNotifyViewDisappeared(int sessionId, @NonNull AutofillId id) {
         final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED)
                 .setAutofillId(id);
-        mHandler.post(() -> sendEvent(event));
+        enqueueEvent(event);
     }
 
     @Override
@@ -780,7 +805,7 @@
                 .setAutofillId(id).setText(eventText)
                 .setComposingIndex(composingStart, composingEnd)
                 .setSelectionIndex(startIndex, endIndex);
-        mHandler.post(() -> sendEvent(event));
+        enqueueEvent(event);
     }
 
     @Override
@@ -788,7 +813,7 @@
         final ContentCaptureEvent event =
                 new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED)
                         .setInsets(viewInsets);
-        mHandler.post(() -> sendEvent(event));
+        enqueueEvent(event);
     }
 
     @Override
@@ -798,19 +823,19 @@
         final boolean forceFlush = disableFlush ? !started : FORCE_FLUSH;
 
         final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, type);
-        mHandler.post(() -> sendEvent(event, FORCE_FLUSH));
+        enqueueEvent(event, forceFlush);
     }
 
     @Override
     public void internalNotifySessionResumed() {
         final ContentCaptureEvent event = new ContentCaptureEvent(mId, TYPE_SESSION_RESUMED);
-        mHandler.post(() -> sendEvent(event, FORCE_FLUSH));
+        enqueueEvent(event, FORCE_FLUSH);
     }
 
     @Override
     public void internalNotifySessionPaused() {
         final ContentCaptureEvent event = new ContentCaptureEvent(mId, TYPE_SESSION_PAUSED);
-        mHandler.post(() -> sendEvent(event, FORCE_FLUSH));
+        enqueueEvent(event, FORCE_FLUSH);
     }
 
     @Override
@@ -818,12 +843,16 @@
         return super.isContentCaptureEnabled() && mManager.isContentCaptureEnabled();
     }
 
-    @Override
+    // Called by ContentCaptureManager.isContentCaptureEnabled
     boolean isDisabled() {
         return mDisabled.get();
     }
 
-    @Override
+    /**
+     * Sets the disabled state of content capture.
+     *
+     * @return whether disabled state was changed.
+     */
     boolean setDisabled(boolean disabled) {
         return mDisabled.compareAndSet(!disabled, disabled);
     }
@@ -835,7 +864,7 @@
                 new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
                         .setParentSessionId(parentSessionId)
                         .setClientContext(clientContext);
-        mHandler.post(() -> sendEvent(event, FORCE_FLUSH));
+        enqueueEvent(event, FORCE_FLUSH);
     }
 
     @Override
@@ -843,14 +872,14 @@
         final ContentCaptureEvent event =
                 new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
                         .setParentSessionId(parentSessionId);
-        mHandler.post(() -> sendEvent(event, FORCE_FLUSH));
+        enqueueEvent(event, FORCE_FLUSH);
     }
 
     @Override
     void internalNotifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context) {
         final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
                 .setClientContext(context);
-        mHandler.post(() -> sendEvent(event, FORCE_FLUSH));
+        enqueueEvent(event, FORCE_FLUSH);
     }
 
     @Override
@@ -858,18 +887,97 @@
         final ContentCaptureEvent event =
                 new ContentCaptureEvent(sessionId, TYPE_WINDOW_BOUNDS_CHANGED)
                         .setBounds(bounds);
-        mHandler.post(() -> sendEvent(event));
+        enqueueEvent(event);
+    }
+
+    private List<ContentCaptureEvent> clearBufferEvents() {
+        final ArrayList<ContentCaptureEvent> bufferEvents = new ArrayList<>();
+        ContentCaptureEvent event;
+        while ((event = mEventProcessQueue.poll()) != null) {
+            bufferEvents.add(event);
+        }
+        return bufferEvents;
+    }
+
+    private void enqueueEvent(@NonNull final ContentCaptureEvent event) {
+        enqueueEvent(event, /* forceFlush */ false);
+    }
+
+    /**
+     * Enqueue the event into {@code mEventProcessBuffer} if it is not an urgent request. Otherwise,
+     * clear the buffer events then starting sending out current event.
+     */
+    private void enqueueEvent(@NonNull final ContentCaptureEvent event, boolean forceFlush) {
+        if (forceFlush || mEventProcessQueue.size() >= mManager.mOptions.maxBufferSize - 1) {
+            // The buffer events are cleared in the same thread first to prevent new events
+            // being added during the time of context switch. This would disrupt the sequence
+            // of events.
+            final List<ContentCaptureEvent> batchEvents = clearBufferEvents();
+            runOnContentCaptureThread(() -> {
+                for (int i = 0; i < batchEvents.size(); i++) {
+                    sendEvent(batchEvents.get(i));
+                }
+                sendEvent(event, /* forceFlush= */ true);
+            });
+        } else {
+            mEventProcessQueue.offer(event);
+        }
     }
 
     @Override
     public void notifyContentCaptureEvents(
             @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
-        notifyContentCaptureEventsImpl(contentCaptureEvents);
+        runOnUiThread(() -> {
+            prepareViewStructures(contentCaptureEvents);
+            runOnContentCaptureThread(() ->
+                    notifyContentCaptureEventsImpl(contentCaptureEvents));
+        });
+    }
+
+    /**
+     * Traverse events and pre-process {@link View} events to {@link ViewStructureSession} events.
+     * If a {@link View} event is invalid, an empty {@link ViewStructureSession} will still be
+     * provided.
+     */
+    private void prepareViewStructures(
+            @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
+        for (int i = 0; i < contentCaptureEvents.size(); i++) {
+            int sessionId = contentCaptureEvents.keyAt(i);
+            ArrayList<Object> events = contentCaptureEvents.valueAt(i);
+            for_each_event: for (int j = 0; j < events.size(); j++) {
+                Object event = events.get(j);
+                if (event instanceof View) {
+                    View view = (View) event;
+                    ContentCaptureSession session = view.getContentCaptureSession();
+                    ViewStructureSession structureSession = new ViewStructureSession();
+
+                    // Replace the View event with ViewStructureSession no matter the data is
+                    // available or not. This is to ensure the sequence of the events are still
+                    // the same. Calls to notifyViewAppeared will check the availability later.
+                    events.set(j, structureSession);
+                    if (session == null) {
+                        Log.w(TAG, "no content capture session on view: " + view);
+                        continue for_each_event;
+                    }
+                    int actualId = session.getId();
+                    if (actualId != sessionId) {
+                        Log.w(TAG, "content capture session mismatch for view (" + view
+                                + "): was " + sessionId + " before, it's " + actualId + " now");
+                        continue for_each_event;
+                    }
+                    ViewStructure structure = session.newViewStructure(view);
+                    view.onProvideContentCaptureStructure(structure, /* flags= */ 0);
+
+                    structureSession.setSession(session);
+                    structureSession.setStructure(structure);
+                }
+            }
+        }
     }
 
     private void notifyContentCaptureEventsImpl(
             @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
-        checkOnUiThread();
+        checkOnContentCaptureThread();
         try {
             if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
                 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents");
@@ -882,22 +990,8 @@
                     Object event = events.get(j);
                     if (event instanceof AutofillId) {
                         internalNotifyViewDisappeared(sessionId, (AutofillId) event);
-                    } else if (event instanceof View) {
-                        View view = (View) event;
-                        ContentCaptureSession session = view.getContentCaptureSession();
-                        if (session == null) {
-                            Log.w(TAG, "no content capture session on view: " + view);
-                            continue for_each_event;
-                        }
-                        int actualId = session.getId();
-                        if (actualId != sessionId) {
-                            Log.w(TAG, "content capture session mismatch for view (" + view
-                                    + "): was " + sessionId + " before, it's " + actualId + " now");
-                            continue for_each_event;
-                        }
-                        ViewStructure structure = session.newViewStructure(view);
-                        view.onProvideContentCaptureStructure(structure, /* flags= */ 0);
-                        session.notifyViewAppeared(structure);
+                    } else if (event instanceof ViewStructureSession viewStructureSession) {
+                        viewStructureSession.notifyViewAppeared();
                     } else if (event instanceof Insets) {
                         internalNotifyViewInsetsChanged(sessionId, (Insets) event);
                     } else {
@@ -1015,9 +1109,9 @@
      * Therefore, accessing internal properties in {@link MainContentCaptureSession} should
      * always delegate to the assigned thread from {@code mHandler} for synchronization.</p>
      */
-    private void checkOnUiThread() {
-        final boolean onUiThread = mHandler.getLooper().isCurrentThread();
-        if (!onUiThread) {
+    private void checkOnContentCaptureThread() {
+        final boolean onContentCaptureThread = mContentCaptureHandler.getLooper().isCurrentThread();
+        if (!onContentCaptureThread) {
             mWrongThreadCount.incrementAndGet();
             Log.e(TAG, "MainContentCaptureSession running on " + Thread.currentThread());
         }
@@ -1028,4 +1122,63 @@
         Counter.logIncrement(
                 CONTENT_CAPTURE_WRONG_THREAD_METRIC_ID, mWrongThreadCount.getAndSet(0));
     }
+
+    /**
+     * Ensures that {@code r} will be running on the assigned thread.
+     *
+     * <p>This is to prevent unnecessary delegation to Handler that results in fragmented runnable.
+     * </p>
+     */
+    private void runOnContentCaptureThread(@NonNull Runnable r) {
+        if (!mContentCaptureHandler.getLooper().isCurrentThread()) {
+            mContentCaptureHandler.post(r);
+        } else {
+            r.run();
+        }
+    }
+
+    private void clearAndRunOnContentCaptureThread(@NonNull Runnable r, int what) {
+        if (!mContentCaptureHandler.getLooper().isCurrentThread()) {
+            mContentCaptureHandler.removeMessages(what);
+            mContentCaptureHandler.post(r);
+        } else {
+            r.run();
+        }
+    }
+
+    private void runOnUiThread(@NonNull Runnable r) {
+        if (mUiHandler.getLooper().isCurrentThread()) {
+            r.run();
+        } else {
+            mUiHandler.post(r);
+        }
+    }
+
+    /**
+     * Holds {@link ContentCaptureSession} and related {@link ViewStructure} for processing.
+     */
+    private static final class ViewStructureSession {
+        @Nullable private ContentCaptureSession mSession;
+        @Nullable private ViewStructure mStructure;
+
+        ViewStructureSession() {}
+
+        void setSession(@Nullable ContentCaptureSession session) {
+            this.mSession = session;
+        }
+
+        void setStructure(@Nullable ViewStructure struct) {
+            this.mStructure = struct;
+        }
+
+        /**
+         * Calls {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)} if the session and
+         * the view structure are available.
+         */
+        void notifyViewAppeared() {
+            if (mSession != null && mStructure != null) {
+                mSession.notifyViewAppeared(mStructure);
+            }
+        }
+    }
 }
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSessionV2.java b/core/java/android/view/contentcapture/MainContentCaptureSessionV2.java
deleted file mode 100644
index fbb66d1..0000000
--- a/core/java/android/view/contentcapture/MainContentCaptureSessionV2.java
+++ /dev/null
@@ -1,1187 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.view.contentcapture;
-
-import static android.view.contentcapture.ContentCaptureEvent.TYPE_CONTEXT_UPDATED;
-import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_FINISHED;
-import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_PAUSED;
-import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_RESUMED;
-import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
-import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED;
-import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
-import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_INSETS_CHANGED;
-import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
-import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARED;
-import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARING;
-import static android.view.contentcapture.ContentCaptureEvent.TYPE_WINDOW_BOUNDS_CHANGED;
-import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString;
-import static android.view.contentcapture.ContentCaptureHelper.sDebug;
-import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
-import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALSE;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.ComponentName;
-import android.content.pm.ParceledListSlice;
-import android.graphics.Insets;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.IBinder.DeathRecipient;
-import android.os.RemoteException;
-import android.os.Trace;
-import android.service.contentcapture.ContentCaptureService;
-import android.text.Selection;
-import android.text.Spannable;
-import android.text.TextUtils;
-import android.util.LocalLog;
-import android.util.Log;
-import android.util.SparseArray;
-import android.util.TimeUtils;
-import android.view.View;
-import android.view.ViewStructure;
-import android.view.autofill.AutofillId;
-import android.view.contentcapture.ViewNode.ViewStructureImpl;
-import android.view.contentprotection.ContentProtectionEventProcessor;
-import android.view.inputmethod.BaseInputConnection;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.os.IResultReceiver;
-import com.android.modules.expresslog.Counter;
-
-import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Main session associated with a context.
- *
- * <p>This is forked from {@link MainContentCaptureSession} to hold the logic of running operations
- * in the background thread.</p>
- *
- * @hide
- */
-@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-public final class MainContentCaptureSessionV2 extends ContentCaptureSession {
-
-    private static final String TAG = MainContentCaptureSession.class.getSimpleName();
-
-    private static final String CONTENT_CAPTURE_WRONG_THREAD_METRIC_ID =
-            "content_capture.value_content_capture_wrong_thread_count";
-
-    // For readability purposes...
-    private static final boolean FORCE_FLUSH = true;
-
-    /**
-     * Handler message used to flush the buffer.
-     */
-    private static final int MSG_FLUSH = 1;
-
-    @NonNull
-    private final AtomicBoolean mDisabled = new AtomicBoolean(false);
-
-    @NonNull
-    private final ContentCaptureManager.StrippedContext mContext;
-
-    @NonNull
-    private final ContentCaptureManager mManager;
-
-    @NonNull
-    private final Handler mUiHandler;
-
-    @NonNull
-    private final Handler mContentCaptureHandler;
-
-    /**
-     * Interface to the system_server binder object - it's only used to start the session (and
-     * notify when the session is finished).
-     */
-    @NonNull
-    private final IContentCaptureManager mSystemServerInterface;
-
-    /**
-     * Direct interface to the service binder object - it's used to send the events, including the
-     * last ones (when the session is finished)
-     *
-     * @hide
-     */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    @Nullable
-    public IContentCaptureDirectManager mDirectServiceInterface;
-
-    @Nullable
-    private DeathRecipient mDirectServiceVulture;
-
-    private int mState = UNKNOWN_STATE;
-
-    @Nullable
-    private IBinder mApplicationToken;
-    @Nullable
-    private IBinder mShareableActivityToken;
-
-    /** @hide */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    @Nullable
-    public ComponentName mComponentName;
-
-    /**
-     * Thread-safe queue of events held to be processed as a batch.
-     *
-     * Because it is not guaranteed that the events will be enqueued from a single thread, the
-     * implementation must be thread-safe to prevent unexpected behaviour.
-     *
-     * @hide
-     */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    @NonNull
-    public final ConcurrentLinkedQueue<ContentCaptureEvent> mEventProcessQueue;
-
-    /**
-     * List of events held to be sent to the {@link ContentCaptureService} as a batch.
-     *
-     * @hide
-     */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    @Nullable
-    public ArrayList<ContentCaptureEvent> mEvents;
-
-    // Used just for debugging purposes (on dump)
-    private long mNextFlush;
-
-    /**
-     * Whether the next buffer flush is queued by a text changed event.
-     */
-    private boolean mNextFlushForTextChanged = false;
-
-    @Nullable
-    private final LocalLog mFlushHistory;
-
-    private final AtomicInteger mWrongThreadCount = new AtomicInteger(0);
-
-    /**
-     * Binder object used to update the session state.
-     */
-    @NonNull
-    private final SessionStateReceiver mSessionStateReceiver;
-
-    /** @hide */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    @Nullable
-    public ContentProtectionEventProcessor mContentProtectionEventProcessor;
-
-    private static class SessionStateReceiver extends IResultReceiver.Stub {
-        private final WeakReference<MainContentCaptureSessionV2> mMainSession;
-
-        SessionStateReceiver(MainContentCaptureSessionV2 session) {
-            mMainSession = new WeakReference<>(session);
-        }
-
-        @Override
-        public void send(int resultCode, Bundle resultData) {
-            final MainContentCaptureSessionV2 mainSession = mMainSession.get();
-            if (mainSession == null) {
-                Log.w(TAG, "received result after mina session released");
-                return;
-            }
-            final IBinder binder;
-            if (resultData != null) {
-                // Change in content capture enabled.
-                final boolean hasEnabled = resultData.getBoolean(EXTRA_ENABLED_STATE);
-                if (hasEnabled) {
-                    final boolean disabled = (resultCode == RESULT_CODE_FALSE);
-                    mainSession.mDisabled.set(disabled);
-                    return;
-                }
-                binder = resultData.getBinder(EXTRA_BINDER);
-                if (binder == null) {
-                    Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result");
-                    mainSession.runOnContentCaptureThread(() -> mainSession.resetSession(
-                            STATE_DISABLED | STATE_INTERNAL_ERROR));
-                    return;
-                }
-            } else {
-                binder = null;
-            }
-            mainSession.runOnContentCaptureThread(() ->
-                    mainSession.onSessionStarted(resultCode, binder));
-        }
-    }
-
-    /** @hide */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
-    public MainContentCaptureSessionV2(
-            @NonNull ContentCaptureManager.StrippedContext context,
-            @NonNull ContentCaptureManager manager,
-            @NonNull Handler uiHandler,
-            @NonNull Handler contentCaptureHandler,
-            @NonNull IContentCaptureManager systemServerInterface) {
-        mContext = context;
-        mManager = manager;
-        mUiHandler = uiHandler;
-        mContentCaptureHandler = contentCaptureHandler;
-        mSystemServerInterface = systemServerInterface;
-
-        final int logHistorySize = mManager.mOptions.logHistorySize;
-        mFlushHistory = logHistorySize > 0 ? new LocalLog(logHistorySize) : null;
-
-        mSessionStateReceiver = new SessionStateReceiver(this);
-
-        mEventProcessQueue = new ConcurrentLinkedQueue<>();
-    }
-
-    @Override
-    ContentCaptureSession getMainCaptureSession() {
-        return this;
-    }
-
-    @Override
-    ContentCaptureSession newChild(@NonNull ContentCaptureContext clientContext) {
-        final ContentCaptureSession child = new ChildContentCaptureSession(this, clientContext);
-        internalNotifyChildSessionStarted(mId, child.mId, clientContext);
-        return child;
-    }
-
-    /**
-     * Starts this session.
-     */
-    @Override
-    void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken,
-            @NonNull ComponentName component, int flags) {
-        runOnContentCaptureThread(
-                () -> startImpl(token, shareableActivityToken, component, flags));
-    }
-
-    private void startImpl(@NonNull IBinder token, @NonNull IBinder shareableActivityToken,
-               @NonNull ComponentName component, int flags) {
-        checkOnContentCaptureThread();
-        if (!isContentCaptureEnabled()) return;
-
-        if (sVerbose) {
-            Log.v(TAG, "start(): token=" + token + ", comp="
-                    + ComponentName.flattenToShortString(component));
-        }
-
-        if (hasStarted()) {
-            // TODO(b/122959591): make sure this is expected (and when), or use Log.w
-            if (sDebug) {
-                Log.d(TAG, "ignoring handleStartSession(" + token + "/"
-                        + ComponentName.flattenToShortString(component) + " while on state "
-                        + getStateAsString(mState));
-            }
-            return;
-        }
-        mState = STATE_WAITING_FOR_SERVER;
-        mApplicationToken = token;
-        mShareableActivityToken = shareableActivityToken;
-        mComponentName = component;
-
-        if (sVerbose) {
-            Log.v(TAG, "handleStartSession(): token=" + token + ", act="
-                    + getDebugState() + ", id=" + mId);
-        }
-
-        try {
-            mSystemServerInterface.startSession(mApplicationToken, mShareableActivityToken,
-                    component, mId, flags, mSessionStateReceiver);
-        } catch (RemoteException e) {
-            Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e);
-        }
-    }
-    @Override
-    void onDestroy() {
-        clearAndRunOnContentCaptureThread(() -> {
-            try {
-                flush(FLUSH_REASON_SESSION_FINISHED);
-            } finally {
-                destroySession();
-            }
-        }, MSG_FLUSH);
-    }
-
-    /**
-     * Callback from {@code system_server} after call to {@link
-     * IContentCaptureManager#startSession(IBinder, ComponentName, String, int, IResultReceiver)}.
-     *
-     * @param resultCode session state
-     * @param binder handle to {@code IContentCaptureDirectManager}
-     * @hide
-     */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    public void onSessionStarted(int resultCode, @Nullable IBinder binder) {
-        checkOnContentCaptureThread();
-        if (binder != null) {
-            mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
-            mDirectServiceVulture = () -> {
-                Log.w(TAG, "Keeping session " + mId + " when service died");
-                mState = STATE_SERVICE_DIED;
-                mDisabled.set(true);
-            };
-            try {
-                binder.linkToDeath(mDirectServiceVulture, 0);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed to link to death on " + binder + ": " + e);
-            }
-        }
-
-        if (isContentProtectionEnabled()) {
-            mContentProtectionEventProcessor =
-                    new ContentProtectionEventProcessor(
-                            mManager.getContentProtectionEventBuffer(),
-                            mContentCaptureHandler,
-                            mSystemServerInterface,
-                            mComponentName.getPackageName(),
-                            mManager.mOptions.contentProtectionOptions);
-        } else {
-            mContentProtectionEventProcessor = null;
-        }
-
-        if ((resultCode & STATE_DISABLED) != 0) {
-            resetSession(resultCode);
-        } else {
-            mState = resultCode;
-            mDisabled.set(false);
-            // Flush any pending data immediately as buffering forced until now.
-            flushIfNeeded(FLUSH_REASON_SESSION_CONNECTED);
-        }
-        if (sVerbose) {
-            Log.v(TAG, "handleSessionStarted() result: id=" + mId + " resultCode=" + resultCode
-                    + ", state=" + getStateAsString(mState) + ", disabled=" + mDisabled.get()
-                    + ", binder=" + binder + ", events=" + (mEvents == null ? 0 : mEvents.size()));
-        }
-    }
-
-    /** @hide */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    public void sendEvent(@NonNull ContentCaptureEvent event) {
-        sendEvent(event, /* forceFlush= */ false);
-    }
-
-    private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
-        checkOnContentCaptureThread();
-        final int eventType = event.getType();
-        if (sVerbose) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event);
-        if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED
-                && eventType != ContentCaptureEvent.TYPE_CONTEXT_UPDATED) {
-            // TODO(b/120494182): comment when this could happen (dialogs?)
-            if (sVerbose) {
-                Log.v(TAG, "handleSendEvent(" + getDebugState() + ", "
-                        + ContentCaptureEvent.getTypeAsString(eventType)
-                        + "): dropping because session not started yet");
-            }
-            return;
-        }
-        if (mDisabled.get()) {
-            // This happens when the event was queued in the handler before the sesison was ready,
-            // then handleSessionStarted() returned and set it as disabled - we need to drop it,
-            // otherwise it will keep triggering handleScheduleFlush()
-            if (sVerbose) Log.v(TAG, "handleSendEvent(): ignoring when disabled");
-            return;
-        }
-
-        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
-            if (eventType == TYPE_VIEW_TREE_APPEARING) {
-                Trace.asyncTraceBegin(
-                        Trace.TRACE_TAG_VIEW, /* methodName= */ "sendEventAsync", /* cookie= */ 0);
-            }
-        }
-
-        if (isContentProtectionReceiverEnabled()) {
-            sendContentProtectionEvent(event);
-        }
-        if (isContentCaptureReceiverEnabled()) {
-            sendContentCaptureEvent(event, forceFlush);
-        }
-
-        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
-            if (eventType == TYPE_VIEW_TREE_APPEARED) {
-                Trace.asyncTraceEnd(
-                        Trace.TRACE_TAG_VIEW, /* methodName= */ "sendEventAsync", /* cookie= */ 0);
-            }
-        }
-    }
-
-    private void sendContentProtectionEvent(@NonNull ContentCaptureEvent event) {
-        checkOnContentCaptureThread();
-        if (mContentProtectionEventProcessor != null) {
-            mContentProtectionEventProcessor.processEvent(event);
-        }
-    }
-
-    private void sendContentCaptureEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
-        checkOnContentCaptureThread();
-        final int eventType = event.getType();
-        final int maxBufferSize = mManager.mOptions.maxBufferSize;
-        if (mEvents == null) {
-            if (sVerbose) {
-                Log.v(TAG, "handleSendEvent(): creating buffer for " + maxBufferSize + " events");
-            }
-            mEvents = new ArrayList<>(maxBufferSize);
-        }
-
-        // Some type of events can be merged together
-        boolean addEvent = true;
-
-        if (eventType == TYPE_VIEW_TEXT_CHANGED) {
-            // We determine whether to add or merge the current event by following criteria:
-            // 1. Don't have composing span: always add.
-            // 2. Have composing span:
-            //    2.1 either last or current text is empty: add.
-            //    2.2 last event doesn't have composing span: add.
-            // Otherwise, merge.
-            final CharSequence text = event.getText();
-            final boolean hasComposingSpan = event.hasComposingSpan();
-            if (hasComposingSpan) {
-                ContentCaptureEvent lastEvent = null;
-                for (int index = mEvents.size() - 1; index >= 0; index--) {
-                    final ContentCaptureEvent tmpEvent = mEvents.get(index);
-                    if (event.getId().equals(tmpEvent.getId())) {
-                        lastEvent = tmpEvent;
-                        break;
-                    }
-                }
-                if (lastEvent != null && lastEvent.hasComposingSpan()) {
-                    final CharSequence lastText = lastEvent.getText();
-                    final boolean bothNonEmpty = !TextUtils.isEmpty(lastText)
-                            && !TextUtils.isEmpty(text);
-                    boolean equalContent =
-                            TextUtils.equals(lastText, text)
-                            && lastEvent.hasSameComposingSpan(event)
-                            && lastEvent.hasSameSelectionSpan(event);
-                    if (equalContent) {
-                        addEvent = false;
-                    } else if (bothNonEmpty) {
-                        lastEvent.mergeEvent(event);
-                        addEvent = false;
-                    }
-                    if (!addEvent && sVerbose) {
-                        Log.v(TAG, "Buffering VIEW_TEXT_CHANGED event, updated text="
-                                + getSanitizedString(text));
-                    }
-                }
-            }
-        }
-
-        if (!mEvents.isEmpty() && eventType == TYPE_VIEW_DISAPPEARED) {
-            final ContentCaptureEvent lastEvent = mEvents.get(mEvents.size() - 1);
-            if (lastEvent.getType() == TYPE_VIEW_DISAPPEARED
-                    && event.getSessionId() == lastEvent.getSessionId()) {
-                if (sVerbose) {
-                    Log.v(TAG, "Buffering TYPE_VIEW_DISAPPEARED events for session "
-                            + lastEvent.getSessionId());
-                }
-                lastEvent.mergeEvent(event);
-                addEvent = false;
-            }
-        }
-
-        if (addEvent) {
-            mEvents.add(event);
-        }
-
-        // TODO: we need to change when the flush happens so that we don't flush while the
-        //  composing span hasn't changed. But we might need to keep flushing the events for the
-        //  non-editable views and views that don't have the composing state; otherwise some other
-        //  Content Capture features may be delayed.
-
-        final int numberEvents = mEvents.size();
-
-        final boolean bufferEvent = numberEvents < maxBufferSize;
-
-        if (bufferEvent && !forceFlush) {
-            final int flushReason;
-            if (eventType == TYPE_VIEW_TEXT_CHANGED) {
-                mNextFlushForTextChanged = true;
-                flushReason = FLUSH_REASON_TEXT_CHANGE_TIMEOUT;
-            } else {
-                if (mNextFlushForTextChanged) {
-                    if (sVerbose) {
-                        Log.i(TAG, "Not scheduling flush because next flush is for text changed");
-                    }
-                    return;
-                }
-
-                flushReason = FLUSH_REASON_IDLE_TIMEOUT;
-            }
-            scheduleFlush(flushReason, /* checkExisting= */ true);
-            return;
-        }
-
-        if (mState != STATE_ACTIVE && numberEvents >= maxBufferSize) {
-            // Callback from startSession hasn't been called yet - typically happens on system
-            // apps that are started before the system service
-            // TODO(b/122959591): try to ignore session while system is not ready / boot
-            // not complete instead. Similarly, the manager service should return right away
-            // when the user does not have a service set
-            if (sDebug) {
-                Log.d(TAG, "Closing session for " + getDebugState()
-                        + " after " + numberEvents + " delayed events");
-            }
-            resetSession(STATE_DISABLED | STATE_NO_RESPONSE);
-            // TODO(b/111276913): denylist activity / use special flag to indicate that
-            // when it's launched again
-            return;
-        }
-        final int flushReason;
-        switch (eventType) {
-            case ContentCaptureEvent.TYPE_SESSION_STARTED:
-                flushReason = FLUSH_REASON_SESSION_STARTED;
-                break;
-            case ContentCaptureEvent.TYPE_SESSION_FINISHED:
-                flushReason = FLUSH_REASON_SESSION_FINISHED;
-                break;
-            case ContentCaptureEvent.TYPE_VIEW_TREE_APPEARING:
-                flushReason = FLUSH_REASON_VIEW_TREE_APPEARING;
-                break;
-            case ContentCaptureEvent.TYPE_VIEW_TREE_APPEARED:
-                flushReason = FLUSH_REASON_VIEW_TREE_APPEARED;
-                break;
-            default:
-                flushReason = forceFlush ? FLUSH_REASON_FORCE_FLUSH : FLUSH_REASON_FULL;
-        }
-
-        flush(flushReason);
-    }
-
-    private boolean hasStarted() {
-        checkOnContentCaptureThread();
-        return mState != UNKNOWN_STATE;
-    }
-
-    private void scheduleFlush(@FlushReason int reason, boolean checkExisting) {
-        checkOnContentCaptureThread();
-        if (sVerbose) {
-            Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason)
-                    + ", checkExisting=" + checkExisting);
-        }
-        if (!hasStarted()) {
-            if (sVerbose) Log.v(TAG, "handleScheduleFlush(): session not started yet");
-            return;
-        }
-
-        if (mDisabled.get()) {
-            // Should not be called on this state, as handleSendEvent checks.
-            // But we rather add one if check and log than re-schedule and keep the session alive...
-            Log.e(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): should not be called "
-                    + "when disabled. events=" + (mEvents == null ? null : mEvents.size()));
-            return;
-        }
-        if (checkExisting && mContentCaptureHandler.hasMessages(MSG_FLUSH)) {
-            // "Renew" the flush message by removing the previous one
-            mContentCaptureHandler.removeMessages(MSG_FLUSH);
-        }
-
-        final int flushFrequencyMs;
-        if (reason == FLUSH_REASON_TEXT_CHANGE_TIMEOUT) {
-            flushFrequencyMs = mManager.mOptions.textChangeFlushingFrequencyMs;
-        } else {
-            if (reason != FLUSH_REASON_IDLE_TIMEOUT) {
-                if (sDebug) {
-                    Log.d(TAG, "handleScheduleFlush(" + getDebugState(reason) + "): not a timeout "
-                            + "reason because mDirectServiceInterface is not ready yet");
-                }
-            }
-            flushFrequencyMs = mManager.mOptions.idleFlushingFrequencyMs;
-        }
-
-        mNextFlush = System.currentTimeMillis() + flushFrequencyMs;
-        if (sVerbose) {
-            Log.v(TAG, "handleScheduleFlush(): scheduled to flush in "
-                    + flushFrequencyMs + "ms: " + TimeUtils.logTimeOfDay(mNextFlush));
-        }
-        // Post using a Runnable directly to trim a few μs from PooledLambda.obtainMessage()
-        mContentCaptureHandler.postDelayed(() ->
-                flushIfNeeded(reason), MSG_FLUSH, flushFrequencyMs);
-    }
-
-    private void flushIfNeeded(@FlushReason int reason) {
-        checkOnContentCaptureThread();
-        if (mEvents == null || mEvents.isEmpty()) {
-            if (sVerbose) Log.v(TAG, "Nothing to flush");
-            return;
-        }
-        flush(reason);
-    }
-
-    /** @hide */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-    @Override
-    public void flush(@FlushReason int reason) {
-        runOnContentCaptureThread(() -> flushImpl(reason));
-    }
-
-    private void flushImpl(@FlushReason int reason) {
-        checkOnContentCaptureThread();
-        if (mEvents == null || mEvents.size() == 0) {
-            if (sVerbose) {
-                Log.v(TAG, "Don't flush for empty event buffer.");
-            }
-            return;
-        }
-
-        if (mDisabled.get()) {
-            Log.e(TAG, "handleForceFlush(" + getDebugState(reason) + "): should not be when "
-                    + "disabled");
-            return;
-        }
-
-        if (!isContentCaptureReceiverEnabled()) {
-            return;
-        }
-
-        if (mDirectServiceInterface == null) {
-            if (sVerbose) {
-                Log.v(TAG, "handleForceFlush(" + getDebugState(reason) + "): hold your horses, "
-                        + "client not ready: " + mEvents);
-            }
-            if (!mContentCaptureHandler.hasMessages(MSG_FLUSH)) {
-                scheduleFlush(reason, /* checkExisting= */ false);
-            }
-            return;
-        }
-
-        mNextFlushForTextChanged = false;
-
-        final int numberEvents = mEvents.size();
-        final String reasonString = getFlushReasonAsString(reason);
-
-        if (sVerbose) {
-            ContentCaptureEvent event = mEvents.get(numberEvents - 1);
-            String forceString = (reason == FLUSH_REASON_FORCE_FLUSH) ? ". The force flush event "
-                    + ContentCaptureEvent.getTypeAsString(event.getType()) : "";
-            Log.v(TAG, "Flushing " + numberEvents + " event(s) for " + getDebugState(reason)
-                    + forceString);
-        }
-        if (mFlushHistory != null) {
-            // Logs reason, size, max size, idle timeout
-            final String logRecord = "r=" + reasonString + " s=" + numberEvents
-                    + " m=" + mManager.mOptions.maxBufferSize
-                    + " i=" + mManager.mOptions.idleFlushingFrequencyMs;
-            mFlushHistory.log(logRecord);
-        }
-        try {
-            mContentCaptureHandler.removeMessages(MSG_FLUSH);
-
-            final ParceledListSlice<ContentCaptureEvent> events = clearEvents();
-            mDirectServiceInterface.sendEvents(events, reason, mManager.mOptions);
-        } catch (RemoteException e) {
-            Log.w(TAG, "Error sending " + numberEvents + " for " + getDebugState()
-                    + ": " + e);
-        }
-    }
-
-    @Override
-    public void updateContentCaptureContext(@Nullable ContentCaptureContext context) {
-        internalNotifyContextUpdated(mId, context);
-    }
-
-    /**
-     * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
-     */
-    @NonNull
-    private ParceledListSlice<ContentCaptureEvent> clearEvents() {
-        checkOnContentCaptureThread();
-        // NOTE: we must save a reference to the current mEvents and then set it to to null,
-        // otherwise clearing it would clear it in the receiving side if the service is also local.
-        if (mEvents == null) {
-            return new ParceledListSlice<>(Collections.EMPTY_LIST);
-        }
-
-        final List<ContentCaptureEvent> events = new ArrayList<>(mEvents);
-        mEvents.clear();
-        return new ParceledListSlice<>(events);
-    }
-
-    /** hide */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    public void destroySession() {
-        checkOnContentCaptureThread();
-        if (sDebug) {
-            Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
-                    + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
-                    + getDebugState());
-        }
-
-        reportWrongThreadMetric();
-        try {
-            mSystemServerInterface.finishSession(mId);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Error destroying system-service session " + mId + " for "
-                    + getDebugState() + ": " + e);
-        }
-
-        if (mDirectServiceInterface != null) {
-            mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
-        }
-        mDirectServiceInterface = null;
-        mContentProtectionEventProcessor = null;
-        mEventProcessQueue.clear();
-    }
-
-    // TODO(b/122454205): once we support multiple sessions, we might need to move some of these
-    // clearings out.
-    /** @hide */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    public void resetSession(int newState) {
-        checkOnContentCaptureThread();
-        if (sVerbose) {
-            Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
-                    + getStateAsString(mState) + " to " + getStateAsString(newState));
-        }
-        mState = newState;
-        mDisabled.set((newState & STATE_DISABLED) != 0);
-        // TODO(b/122454205): must reset children (which currently is owned by superclass)
-        mApplicationToken = null;
-        mShareableActivityToken = null;
-        mComponentName = null;
-        mEvents = null;
-        if (mDirectServiceInterface != null) {
-            try {
-                mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
-            } catch (NoSuchElementException e) {
-                Log.w(TAG, "IContentCaptureDirectManager does not exist");
-            }
-        }
-        mDirectServiceInterface = null;
-        mContentProtectionEventProcessor = null;
-        mContentCaptureHandler.removeMessages(MSG_FLUSH);
-    }
-
-    @Override
-    void internalNotifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node) {
-        final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
-                .setViewNode(node.mNode);
-        enqueueEvent(event);
-    }
-
-    @Override
-    void internalNotifyViewDisappeared(int sessionId, @NonNull AutofillId id) {
-        final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED)
-                .setAutofillId(id);
-        enqueueEvent(event);
-    }
-
-    @Override
-    void internalNotifyViewTextChanged(
-            int sessionId, @NonNull AutofillId id, @Nullable CharSequence text) {
-        // Since the same CharSequence instance may be reused in the TextView, we need to make
-        // a copy of its content so that its value will not be changed by subsequent updates
-        // in the TextView.
-        CharSequence trimmed = TextUtils.trimToParcelableSize(text);
-        final CharSequence eventText = trimmed != null && trimmed == text
-                ? trimmed.toString()
-                : trimmed;
-
-        final int composingStart;
-        final int composingEnd;
-        if (text instanceof Spannable) {
-            composingStart = BaseInputConnection.getComposingSpanStart((Spannable) text);
-            composingEnd = BaseInputConnection.getComposingSpanEnd((Spannable) text);
-        } else {
-            composingStart = ContentCaptureEvent.MAX_INVALID_VALUE;
-            composingEnd = ContentCaptureEvent.MAX_INVALID_VALUE;
-        }
-
-        final int startIndex = Selection.getSelectionStart(text);
-        final int endIndex = Selection.getSelectionEnd(text);
-
-        final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED)
-                .setAutofillId(id).setText(eventText)
-                .setComposingIndex(composingStart, composingEnd)
-                .setSelectionIndex(startIndex, endIndex);
-        enqueueEvent(event);
-    }
-
-    @Override
-    void internalNotifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) {
-        final ContentCaptureEvent event =
-                new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED)
-                        .setInsets(viewInsets);
-        enqueueEvent(event);
-    }
-
-    @Override
-    public void internalNotifyViewTreeEvent(int sessionId, boolean started) {
-        final int type = started ? TYPE_VIEW_TREE_APPEARING : TYPE_VIEW_TREE_APPEARED;
-        final boolean disableFlush = mManager.getFlushViewTreeAppearingEventDisabled();
-        final boolean forceFlush = disableFlush ? !started : FORCE_FLUSH;
-
-        final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, type);
-        enqueueEvent(event, forceFlush);
-    }
-
-    @Override
-    public void internalNotifySessionResumed() {
-        final ContentCaptureEvent event = new ContentCaptureEvent(mId, TYPE_SESSION_RESUMED);
-        enqueueEvent(event, FORCE_FLUSH);
-    }
-
-    @Override
-    public void internalNotifySessionPaused() {
-        final ContentCaptureEvent event = new ContentCaptureEvent(mId, TYPE_SESSION_PAUSED);
-        enqueueEvent(event, FORCE_FLUSH);
-    }
-
-    @Override
-    boolean isContentCaptureEnabled() {
-        return super.isContentCaptureEnabled() && mManager.isContentCaptureEnabled();
-    }
-
-    // Called by ContentCaptureManager.isContentCaptureEnabled
-    boolean isDisabled() {
-        return mDisabled.get();
-    }
-
-    /**
-     * Sets the disabled state of content capture.
-     *
-     * @return whether disabled state was changed.
-     */
-    boolean setDisabled(boolean disabled) {
-        return mDisabled.compareAndSet(!disabled, disabled);
-    }
-
-    @Override
-    void internalNotifyChildSessionStarted(int parentSessionId, int childSessionId,
-            @NonNull ContentCaptureContext clientContext) {
-        final ContentCaptureEvent event =
-                new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
-                        .setParentSessionId(parentSessionId)
-                        .setClientContext(clientContext);
-        enqueueEvent(event, FORCE_FLUSH);
-    }
-
-    @Override
-    void internalNotifyChildSessionFinished(int parentSessionId, int childSessionId) {
-        final ContentCaptureEvent event =
-                new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
-                        .setParentSessionId(parentSessionId);
-        enqueueEvent(event, FORCE_FLUSH);
-    }
-
-    @Override
-    void internalNotifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context) {
-        final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
-                .setClientContext(context);
-        enqueueEvent(event, FORCE_FLUSH);
-    }
-
-    @Override
-    public void notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds) {
-        final ContentCaptureEvent event =
-                new ContentCaptureEvent(sessionId, TYPE_WINDOW_BOUNDS_CHANGED)
-                        .setBounds(bounds);
-        enqueueEvent(event);
-    }
-
-    private List<ContentCaptureEvent> clearBufferEvents() {
-        final ArrayList<ContentCaptureEvent> bufferEvents = new ArrayList<>();
-        ContentCaptureEvent event;
-        while ((event = mEventProcessQueue.poll()) != null) {
-            bufferEvents.add(event);
-        }
-        return bufferEvents;
-    }
-
-    private void enqueueEvent(@NonNull final ContentCaptureEvent event) {
-        enqueueEvent(event, /* forceFlush */ false);
-    }
-
-    /**
-     * Enqueue the event into {@code mEventProcessBuffer} if it is not an urgent request. Otherwise,
-     * clear the buffer events then starting sending out current event.
-     */
-    private void enqueueEvent(@NonNull final ContentCaptureEvent event, boolean forceFlush) {
-        if (forceFlush || mEventProcessQueue.size() >= mManager.mOptions.maxBufferSize - 1) {
-            // The buffer events are cleared in the same thread first to prevent new events
-            // being added during the time of context switch. This would disrupt the sequence
-            // of events.
-            final List<ContentCaptureEvent> batchEvents = clearBufferEvents();
-            runOnContentCaptureThread(() -> {
-                for (int i = 0; i < batchEvents.size(); i++) {
-                    sendEvent(batchEvents.get(i));
-                }
-                sendEvent(event, /* forceFlush= */ true);
-            });
-        } else {
-            mEventProcessQueue.offer(event);
-        }
-    }
-
-    @Override
-    public void notifyContentCaptureEvents(
-            @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
-        runOnUiThread(() -> {
-            prepareViewStructures(contentCaptureEvents);
-            runOnContentCaptureThread(() ->
-                    notifyContentCaptureEventsImpl(contentCaptureEvents));
-        });
-    }
-
-    /**
-     * Traverse events and pre-process {@link View} events to {@link ViewStructureSession} events.
-     * If a {@link View} event is invalid, an empty {@link ViewStructureSession} will still be
-     * provided.
-     */
-    private void prepareViewStructures(
-            @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
-        for (int i = 0; i < contentCaptureEvents.size(); i++) {
-            int sessionId = contentCaptureEvents.keyAt(i);
-            ArrayList<Object> events = contentCaptureEvents.valueAt(i);
-            for_each_event: for (int j = 0; j < events.size(); j++) {
-                Object event = events.get(j);
-                if (event instanceof View) {
-                    View view = (View) event;
-                    ContentCaptureSession session = view.getContentCaptureSession();
-                    ViewStructureSession structureSession = new ViewStructureSession();
-
-                    // Replace the View event with ViewStructureSession no matter the data is
-                    // available or not. This is to ensure the sequence of the events are still
-                    // the same. Calls to notifyViewAppeared will check the availability later.
-                    events.set(j, structureSession);
-                    if (session == null) {
-                        Log.w(TAG, "no content capture session on view: " + view);
-                        continue for_each_event;
-                    }
-                    int actualId = session.getId();
-                    if (actualId != sessionId) {
-                        Log.w(TAG, "content capture session mismatch for view (" + view
-                                + "): was " + sessionId + " before, it's " + actualId + " now");
-                        continue for_each_event;
-                    }
-                    ViewStructure structure = session.newViewStructure(view);
-                    view.onProvideContentCaptureStructure(structure, /* flags= */ 0);
-
-                    structureSession.setSession(session);
-                    structureSession.setStructure(structure);
-                }
-            }
-        }
-    }
-
-    private void notifyContentCaptureEventsImpl(
-            @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
-        checkOnContentCaptureThread();
-        try {
-            if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
-                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents");
-            }
-            for (int i = 0; i < contentCaptureEvents.size(); i++) {
-                int sessionId = contentCaptureEvents.keyAt(i);
-                internalNotifyViewTreeEvent(sessionId, /* started= */ true);
-                ArrayList<Object> events = contentCaptureEvents.valueAt(i);
-                for_each_event: for (int j = 0; j < events.size(); j++) {
-                    Object event = events.get(j);
-                    if (event instanceof AutofillId) {
-                        internalNotifyViewDisappeared(sessionId, (AutofillId) event);
-                    } else if (event instanceof ViewStructureSession viewStructureSession) {
-                        viewStructureSession.notifyViewAppeared();
-                    } else if (event instanceof Insets) {
-                        internalNotifyViewInsetsChanged(sessionId, (Insets) event);
-                    } else {
-                        Log.w(TAG, "invalid content capture event: " + event);
-                    }
-                }
-                internalNotifyViewTreeEvent(sessionId, /* started= */ false);
-            }
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
-        }
-    }
-
-    @Override
-    void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
-        super.dump(prefix, pw);
-
-        pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
-        pw.print(prefix); pw.print("user: "); pw.println(mContext.getUserId());
-        if (mDirectServiceInterface != null) {
-            pw.print(prefix); pw.print("mDirectServiceInterface: ");
-            pw.println(mDirectServiceInterface);
-        }
-        pw.print(prefix); pw.print("mDisabled: "); pw.println(mDisabled.get());
-        pw.print(prefix); pw.print("isEnabled(): "); pw.println(isContentCaptureEnabled());
-        pw.print(prefix); pw.print("state: "); pw.println(getStateAsString(mState));
-        if (mApplicationToken != null) {
-            pw.print(prefix); pw.print("app token: "); pw.println(mApplicationToken);
-        }
-        if (mShareableActivityToken != null) {
-            pw.print(prefix); pw.print("sharable activity token: ");
-            pw.println(mShareableActivityToken);
-        }
-        if (mComponentName != null) {
-            pw.print(prefix); pw.print("component name: ");
-            pw.println(mComponentName.flattenToShortString());
-        }
-        if (mEvents != null && !mEvents.isEmpty()) {
-            final int numberEvents = mEvents.size();
-            pw.print(prefix); pw.print("buffered events: "); pw.print(numberEvents);
-            pw.print('/'); pw.println(mManager.mOptions.maxBufferSize);
-            if (sVerbose && numberEvents > 0) {
-                final String prefix3 = prefix + "  ";
-                for (int i = 0; i < numberEvents; i++) {
-                    final ContentCaptureEvent event = mEvents.get(i);
-                    pw.print(prefix3); pw.print(i); pw.print(": "); event.dump(pw);
-                    pw.println();
-                }
-            }
-            pw.print(prefix); pw.print("mNextFlushForTextChanged: ");
-            pw.println(mNextFlushForTextChanged);
-            pw.print(prefix); pw.print("flush frequency: ");
-            if (mNextFlushForTextChanged) {
-                pw.println(mManager.mOptions.textChangeFlushingFrequencyMs);
-            } else {
-                pw.println(mManager.mOptions.idleFlushingFrequencyMs);
-            }
-            pw.print(prefix); pw.print("next flush: ");
-            TimeUtils.formatDuration(mNextFlush - System.currentTimeMillis(), pw);
-            pw.print(" ("); pw.print(TimeUtils.logTimeOfDay(mNextFlush)); pw.println(")");
-        }
-        if (mFlushHistory != null) {
-            pw.print(prefix); pw.println("flush history:");
-            mFlushHistory.reverseDump(/* fd= */ null, pw, /* args= */ null); pw.println();
-        } else {
-            pw.print(prefix); pw.println("not logging flush history");
-        }
-
-        super.dump(prefix, pw);
-    }
-
-    /**
-     * Gets a string that can be used to identify the activity on logging statements.
-     */
-    private String getActivityName() {
-        return mComponentName == null
-                ? "pkg:" + mContext.getPackageName()
-                : "act:" + mComponentName.flattenToShortString();
-    }
-
-    @NonNull
-    private String getDebugState() {
-        return getActivityName() + " [state=" + getStateAsString(mState) + ", disabled="
-                + mDisabled.get() + "]";
-    }
-
-    @NonNull
-    private String getDebugState(@FlushReason int reason) {
-        return getDebugState() + ", reason=" + getFlushReasonAsString(reason);
-    }
-
-    private boolean isContentProtectionReceiverEnabled() {
-        return mManager.mOptions.contentProtectionOptions.enableReceiver;
-    }
-
-    private boolean isContentCaptureReceiverEnabled() {
-        return mManager.mOptions.enableReceiver;
-    }
-
-    private boolean isContentProtectionEnabled() {
-        // Should not be possible for mComponentName to be null here but check anyway
-        // Should not be possible for groups to be empty if receiver is enabled but check anyway
-        return mManager.mOptions.contentProtectionOptions.enableReceiver
-                && mManager.getContentProtectionEventBuffer() != null
-                && mComponentName != null
-                && (!mManager.mOptions.contentProtectionOptions.requiredGroups.isEmpty()
-                        || !mManager.mOptions.contentProtectionOptions.optionalGroups.isEmpty());
-    }
-
-    /**
-     * Checks that the current work is running on the assigned thread from {@code mHandler} and
-     * count the number of times running on the wrong thread.
-     *
-     * <p>It is not guaranteed that the callers always invoke function from a single thread.
-     * Therefore, accessing internal properties in {@link MainContentCaptureSession} should
-     * always delegate to the assigned thread from {@code mHandler} for synchronization.</p>
-     */
-    private void checkOnContentCaptureThread() {
-        final boolean onContentCaptureThread = mContentCaptureHandler.getLooper().isCurrentThread();
-        if (!onContentCaptureThread) {
-            mWrongThreadCount.incrementAndGet();
-            Log.e(TAG, "MainContentCaptureSession running on " + Thread.currentThread());
-        }
-    }
-
-    /** Reports number of times running on the wrong thread. */
-    private void reportWrongThreadMetric() {
-        Counter.logIncrement(
-                CONTENT_CAPTURE_WRONG_THREAD_METRIC_ID, mWrongThreadCount.getAndSet(0));
-    }
-
-    /**
-     * Ensures that {@code r} will be running on the assigned thread.
-     *
-     * <p>This is to prevent unnecessary delegation to Handler that results in fragmented runnable.
-     * </p>
-     */
-    private void runOnContentCaptureThread(@NonNull Runnable r) {
-        if (!mContentCaptureHandler.getLooper().isCurrentThread()) {
-            mContentCaptureHandler.post(r);
-        } else {
-            r.run();
-        }
-    }
-
-    private void clearAndRunOnContentCaptureThread(@NonNull Runnable r, int what) {
-        if (!mContentCaptureHandler.getLooper().isCurrentThread()) {
-            mContentCaptureHandler.removeMessages(what);
-            mContentCaptureHandler.post(r);
-        } else {
-            r.run();
-        }
-    }
-
-    private void runOnUiThread(@NonNull Runnable r) {
-        if (mUiHandler.getLooper().isCurrentThread()) {
-            r.run();
-        } else {
-            mUiHandler.post(r);
-        }
-    }
-
-    /**
-     * Holds {@link ContentCaptureSession} and related {@link ViewStructure} for processing.
-     */
-    private static final class ViewStructureSession {
-        @Nullable private ContentCaptureSession mSession;
-        @Nullable private ViewStructure mStructure;
-
-        ViewStructureSession() {}
-
-        void setSession(@Nullable ContentCaptureSession session) {
-            this.mSession = session;
-        }
-
-        void setStructure(@Nullable ViewStructure struct) {
-            this.mStructure = struct;
-        }
-
-        /**
-         * Calls {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)} if the session and
-         * the view structure are available.
-         */
-        void notifyViewAppeared() {
-            if (mSession != null && mStructure != null) {
-                mSession.notifyViewAppeared(mStructure);
-            }
-        }
-    }
-}
diff --git a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
index f1ed3be..b7e68ba 100644
--- a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
+++ b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
@@ -36,11 +36,13 @@
     private Context mContext;
     private Runnable mOnChangeRunnable;
     private Handler mMainHandler;
+    private Handler mBgHandler;
 
-    public GestureNavigationSettingsObserver(Handler handler, Context context,
-            Runnable onChangeRunnable) {
-        super(handler);
-        mMainHandler = handler;
+    public GestureNavigationSettingsObserver(
+            Handler mainHandler, Handler bgHandler, Context context, Runnable onChangeRunnable) {
+        super(mainHandler);
+        mMainHandler = mainHandler;
+        mBgHandler = bgHandler;
         mContext = context;
         mOnChangeRunnable = onChangeRunnable;
     }
@@ -60,45 +62,51 @@
      * Registers the observer for all users.
      */
     public void register() {
-        ContentResolver r = mContext.getContentResolver();
-        r.registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT),
-                false, this, UserHandle.USER_ALL);
-        r.registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT),
-                false, this, UserHandle.USER_ALL);
-        r.registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE),
-                false, this, UserHandle.USER_ALL);
-        DeviceConfig.addOnPropertiesChangedListener(
-                DeviceConfig.NAMESPACE_SYSTEMUI,
-                runnable -> mMainHandler.post(runnable),
-                mOnPropertiesChangedListener);
+        mBgHandler.post(() -> {
+            ContentResolver r = mContext.getContentResolver();
+            r.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT),
+                    false, this, UserHandle.USER_ALL);
+            r.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT),
+                    false, this, UserHandle.USER_ALL);
+            r.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE),
+                    false, this, UserHandle.USER_ALL);
+            DeviceConfig.addOnPropertiesChangedListener(
+                    DeviceConfig.NAMESPACE_SYSTEMUI,
+                    runnable -> mMainHandler.post(runnable),
+                    mOnPropertiesChangedListener);
+        });
     }
 
     /**
      * Registers the observer for the calling user.
      */
     public void registerForCallingUser() {
-        ContentResolver r = mContext.getContentResolver();
-        r.registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT),
-                false, this);
-        r.registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT),
-                false, this);
-        r.registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE),
-                false, this);
-        DeviceConfig.addOnPropertiesChangedListener(
-                DeviceConfig.NAMESPACE_SYSTEMUI,
-                runnable -> mMainHandler.post(runnable),
-                mOnPropertiesChangedListener);
+        mBgHandler.post(() -> {
+            ContentResolver r = mContext.getContentResolver();
+            r.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT),
+                    false, this);
+            r.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT),
+                    false, this);
+            r.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE),
+                    false, this);
+            DeviceConfig.addOnPropertiesChangedListener(
+                    DeviceConfig.NAMESPACE_SYSTEMUI,
+                    runnable -> mMainHandler.post(runnable),
+                    mOnPropertiesChangedListener);
+        });
     }
 
     public void unregister() {
-        mContext.getContentResolver().unregisterContentObserver(this);
-        DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener);
+        mBgHandler.post(() -> {
+            mContext.getContentResolver().unregisterContentObserver(this);
+            DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener);
+        });
     }
 
     @Override
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 40d760e..42be4fc 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -4069,10 +4069,7 @@
     }
 
     @Override
-    public void setResizingCaptionDrawable(Drawable drawable) {
-        // TODO(b/333724879): Deprecate this public API. The new caption in WM shell allows the app
-        // content to draw behind it directly if requested.
-    }
+    public void setResizingCaptionDrawable(Drawable drawable) {}
 
     @Override
     public void setDecorCaptionShade(int decorCaptionShade) {
diff --git a/core/java/com/android/internal/widget/NotificationRowIconView.java b/core/java/com/android/internal/widget/NotificationRowIconView.java
index 58bddae..f5f04a7 100644
--- a/core/java/com/android/internal/widget/NotificationRowIconView.java
+++ b/core/java/com/android/internal/widget/NotificationRowIconView.java
@@ -22,7 +22,12 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapShader;
 import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
 import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
@@ -36,6 +41,13 @@
 @RemoteViews.RemoteView
 public class NotificationRowIconView extends CachingIconView {
     private boolean mApplyCircularCrop = false;
+    private boolean mShouldShowAppIcon = false;
+
+    // Padding and background set on the view prior to being changed by setShouldShowAppIcon(true),
+    // to be restored if shouldShowAppIcon becomes false again.
+    private Rect mOriginalPadding = null;
+    private Drawable mOriginalBackground = null;
+
 
     public NotificationRowIconView(Context context) {
         super(context);
@@ -59,7 +71,7 @@
     @Override
     protected void onFinishInflate() {
         // If showing the app icon, we don't need background or padding.
-        if (Flags.notificationsUseAppIcon() || Flags.notificationsUseAppIconInRow()) {
+        if (Flags.notificationsUseAppIcon()) {
             setPadding(0, 0, 0, 0);
             setBackground(null);
         }
@@ -67,6 +79,42 @@
         super.onFinishInflate();
     }
 
+    /** Whether the icon represents the app icon (instead of the small icon). */
+    @RemotableViewMethod
+    public void setShouldShowAppIcon(boolean shouldShowAppIcon) {
+        if (Flags.notificationsUseAppIconInRow()) {
+            if (mShouldShowAppIcon == shouldShowAppIcon) {
+                return; // no change
+            }
+
+            mShouldShowAppIcon = shouldShowAppIcon;
+            if (mShouldShowAppIcon) {
+                if (mOriginalPadding == null && mOriginalBackground == null) {
+                    mOriginalPadding = new Rect(getPaddingLeft(), getPaddingTop(),
+                            getPaddingRight(), getPaddingBottom());
+                    mOriginalBackground = getBackground();
+                }
+
+                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,
+                        PorterDuff.Mode.SRC_ATOP);
+                getBackground().mutate().setColorFilter(colorFilter);
+            } else {
+                // Restore original padding and background if needed
+                if (mOriginalPadding != null) {
+                    setPadding(mOriginalPadding.left, mOriginalPadding.top, mOriginalPadding.right,
+                            mOriginalPadding.bottom);
+                    mOriginalPadding = null;
+                }
+                setBackground(mOriginalBackground);
+                mOriginalBackground = null;
+            }
+        }
+    }
+
     @Nullable
     @Override
     Drawable loadSizeRestrictedIcon(@Nullable Icon icon) {
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index fba95a5..e983427 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -312,6 +312,15 @@
     <integer name="config_oem_enabled_satellite_location_fresh_duration">300</integer>
     <java-symbol type="integer" name="config_oem_enabled_satellite_location_fresh_duration" />
 
+    <!-- The time duration in seconds which is used to decide whether satellite is in emergency
+         mode.
+
+         If the duration from the last time when an emergency call is made to the current time is
+         less than or equal to this duration, satellite is considered as in emergency mode.
+         -->
+    <integer name="config_satellite_emergency_mode_duration">300</integer>
+    <java-symbol type="integer" name="config_satellite_emergency_mode_duration" />
+
     <!-- Whether enhanced IWLAN handover check is enabled. If enabled, telephony frameworks
          will not perform handover if the target transport is out of service, or VoPS not
          supported. The network will be torn down on the source transport, and will be
diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
index 4a4c693..5a4561d 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
@@ -198,7 +198,7 @@
         }
 
         @Override
-        MainContentCaptureSession getMainCaptureSession() {
+        ContentCaptureSession getMainCaptureSession() {
             throw new UnsupportedOperationException("should not have been called");
         }
 
diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
index 1cdcb37..b42bcee 100644
--- a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
@@ -433,6 +433,72 @@
         assertThat(session.mEvents).isEmpty();
     }
 
+    @Test
+    public void notifyViewAppearedBelowMaximumBufferSize() throws RemoteException {
+        ContentCaptureOptions options =
+                createOptions(
+                        /* enableContentCaptureReceiver= */ true,
+                        /* enableContentProtectionReceiver= */ true);
+        MainContentCaptureSession session = createSession(options);
+        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+        session.onSessionStarted(0x2, null);
+        for (int i = 0; i < BUFFER_SIZE - 1; i++) {
+            View view = prepareView(session);
+            session.notifyViewAppeared(session.newViewStructure(view));
+        }
+        mTestableLooper.processAllMessages();
+
+        verify(mMockContentCaptureDirectManager, times(0))
+                .sendEvents(any(), anyInt(), any());
+        assertThat(session.mEvents).isNull();
+        assertThat(session.mEventProcessQueue).hasSize(BUFFER_SIZE - 1);
+    }
+
+    @Test
+    public void notifyViewAppearedExactAsMaximumBufferSize() throws RemoteException {
+        ContentCaptureOptions options =
+                createOptions(
+                        /* enableContentCaptureReceiver= */ true,
+                        /* enableContentProtectionReceiver= */ true);
+        MainContentCaptureSession session = createSession(options);
+        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+        session.onSessionStarted(0x2, null);
+        for (int i = 0; i < BUFFER_SIZE; i++) {
+            View view = prepareView(session);
+            session.notifyViewAppeared(session.newViewStructure(view));
+        }
+        mTestableLooper.processAllMessages();
+
+        verify(mMockContentCaptureDirectManager, times(1))
+                .sendEvents(any(), anyInt(), any());
+        assertThat(session.mEvents).isEmpty();
+        assertThat(session.mEventProcessQueue).isEmpty();
+    }
+
+    @Test
+    public void notifyViewAppearedAboveMaximumBufferSize() throws RemoteException {
+        ContentCaptureOptions options =
+                createOptions(
+                        /* enableContentCaptureReceiver= */ true,
+                        /* enableContentProtectionReceiver= */ true);
+        MainContentCaptureSession session = createSession(options);
+        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+        session.onSessionStarted(0x2, null);
+        for (int i = 0; i < BUFFER_SIZE * 2 + 1; i++) {
+            View view = prepareView(session);
+            session.notifyViewAppeared(session.newViewStructure(view));
+        }
+        mTestableLooper.processAllMessages();
+
+        verify(mMockContentCaptureDirectManager, times(2))
+                .sendEvents(any(), anyInt(), any());
+        assertThat(session.mEvents).isEmpty();
+        assertThat(session.mEventProcessQueue).hasSize(1);
+    }
+
     /** Simulates the regular content capture events sequence. */
     private void notifyContentCaptureEvents(final MainContentCaptureSession session) {
         final ArrayList<Object> events = new ArrayList<>(
@@ -489,11 +555,13 @@
     }
 
     private MainContentCaptureSession createSession(ContentCaptureManager manager) {
+        final Handler testHandler = Handler.createAsync(mTestableLooper.getLooper());
         MainContentCaptureSession session =
                 new MainContentCaptureSession(
                         sStrippedContext,
                         manager,
-                        Handler.createAsync(mTestableLooper.getLooper()),
+                        testHandler,
+                        testHandler,
                         mMockSystemServerInterface);
         session.mComponentName = COMPONENT_NAME;
         return session;
diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionV2Test.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionV2Test.java
deleted file mode 100644
index 0075128..0000000
--- a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionV2Test.java
+++ /dev/null
@@ -1,596 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.view.contentcapture;
-
-import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
-import static android.view.contentcapture.ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARED;
-import static android.view.contentcapture.ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARING;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-
-import android.content.ComponentName;
-import android.content.ContentCaptureOptions;
-import android.content.Context;
-import android.content.pm.ParceledListSlice;
-import android.graphics.Insets;
-import android.os.Handler;
-import android.os.RemoteException;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.util.SparseArray;
-import android.view.View;
-import android.view.autofill.AutofillId;
-import android.view.contentprotection.ContentProtectionEventProcessor;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SmallTest;
-
-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.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Test for {@link MainContentCaptureSessionV2}.
- *
- * <p>Run with: {@code atest
- * FrameworksCoreTests:android.view.contentcapture.MainContentCaptureSessionV2Test}
- */
-@RunWith(AndroidTestingRunner.class)
-@SmallTest
-@TestableLooper.RunWithLooper
-public class MainContentCaptureSessionV2Test {
-
-    private static final int BUFFER_SIZE = 100;
-
-    private static final int REASON = 123;
-
-    private static final ContentCaptureEvent EVENT =
-            new ContentCaptureEvent(/* sessionId= */ 0, TYPE_SESSION_STARTED);
-
-    private static final ComponentName COMPONENT_NAME =
-            new ComponentName("com.test.package", "TestClass");
-
-    private static final Context sContext = ApplicationProvider.getApplicationContext();
-
-    private static final ContentCaptureManager.StrippedContext sStrippedContext =
-            new ContentCaptureManager.StrippedContext(sContext);
-
-    private TestableLooper mTestableLooper;
-
-    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
-
-    @Mock private IContentCaptureManager mMockSystemServerInterface;
-
-    @Mock private ContentProtectionEventProcessor mMockContentProtectionEventProcessor;
-
-    @Mock private IContentCaptureDirectManager mMockContentCaptureDirectManager;
-
-    @Before
-    public void setup() {
-        mTestableLooper = TestableLooper.get(this);
-    }
-
-    @Test
-    public void onSessionStarted_contentProtectionEnabled_processorCreated() {
-        MainContentCaptureSessionV2 session = createSession();
-        assertThat(session.mContentProtectionEventProcessor).isNull();
-
-        session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
-        mTestableLooper.processAllMessages();
-
-        assertThat(session.mContentProtectionEventProcessor).isNotNull();
-    }
-
-    @Test
-    public void onSessionStarted_contentProtectionDisabled_processorNotCreated() {
-        MainContentCaptureSessionV2 session =
-                createSession(
-                        /* enableContentCaptureReceiver= */ true,
-                        /* enableContentProtectionReceiver= */ false);
-        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
-
-        session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
-        mTestableLooper.processAllMessages();
-
-        assertThat(session.mContentProtectionEventProcessor).isNull();
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-    }
-
-    @Test
-    public void onSessionStarted_contentProtectionNoBuffer_processorNotCreated() {
-        ContentCaptureOptions options =
-                createOptions(
-                        /* enableContentCaptureReceiver= */ true,
-                        new ContentCaptureOptions.ContentProtectionOptions(
-                                /* enableReceiver= */ true,
-                                -BUFFER_SIZE,
-                                /* requiredGroups= */ List.of(List.of("a")),
-                                /* optionalGroups= */ Collections.emptyList(),
-                                /* optionalGroupsThreshold= */ 0));
-        MainContentCaptureSessionV2 session = createSession(options);
-        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
-
-        session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
-        mTestableLooper.processAllMessages();
-
-        assertThat(session.mContentProtectionEventProcessor).isNull();
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-    }
-
-    @Test
-    public void onSessionStarted_contentProtectionNoGroups_processorNotCreated() {
-        ContentCaptureOptions options =
-                createOptions(
-                        /* enableContentCaptureReceiver= */ true,
-                        new ContentCaptureOptions.ContentProtectionOptions(
-                                /* enableReceiver= */ true,
-                                BUFFER_SIZE,
-                                /* requiredGroups= */ Collections.emptyList(),
-                                /* optionalGroups= */ Collections.emptyList(),
-                                /* optionalGroupsThreshold= */ 0));
-        MainContentCaptureSessionV2 session = createSession(options);
-        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
-
-        session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
-        mTestableLooper.processAllMessages();
-
-        assertThat(session.mContentProtectionEventProcessor).isNull();
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-    }
-
-    @Test
-    public void onSessionStarted_noComponentName_processorNotCreated() {
-        MainContentCaptureSessionV2 session = createSession();
-        session.mComponentName = null;
-
-        session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
-        mTestableLooper.processAllMessages();
-
-        assertThat(session.mContentProtectionEventProcessor).isNull();
-    }
-
-    @Test
-    public void sendEvent_contentCaptureDisabled_contentProtectionDisabled() {
-        MainContentCaptureSessionV2 session =
-                createSession(
-                        /* enableContentCaptureReceiver= */ false,
-                        /* enableContentProtectionReceiver= */ false);
-        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
-
-        session.sendEvent(EVENT);
-        mTestableLooper.processAllMessages();
-
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-        assertThat(session.mEvents).isNull();
-    }
-
-    @Test
-    public void sendEvent_contentCaptureDisabled_contentProtectionEnabled() {
-        MainContentCaptureSessionV2 session =
-                createSession(
-                        /* enableContentCaptureReceiver= */ false,
-                        /* enableContentProtectionReceiver= */ true);
-        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
-
-        session.sendEvent(EVENT);
-        mTestableLooper.processAllMessages();
-
-        verify(mMockContentProtectionEventProcessor).processEvent(EVENT);
-        assertThat(session.mEvents).isNull();
-    }
-
-    @Test
-    public void sendEvent_contentCaptureEnabled_contentProtectionDisabled() {
-        MainContentCaptureSessionV2 session =
-                createSession(
-                        /* enableContentCaptureReceiver= */ true,
-                        /* enableContentProtectionReceiver= */ false);
-        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
-
-        session.sendEvent(EVENT);
-        mTestableLooper.processAllMessages();
-
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-        assertThat(session.mEvents).isNotNull();
-        assertThat(session.mEvents).containsExactly(EVENT);
-    }
-
-    @Test
-    public void sendEvent_contentCaptureEnabled_contentProtectionEnabled() {
-        MainContentCaptureSessionV2 session = createSession();
-        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
-
-        session.sendEvent(EVENT);
-        mTestableLooper.processAllMessages();
-
-        verify(mMockContentProtectionEventProcessor).processEvent(EVENT);
-        assertThat(session.mEvents).isNotNull();
-        assertThat(session.mEvents).containsExactly(EVENT);
-    }
-
-    @Test
-    public void sendEvent_contentProtectionEnabled_processorNotCreated() {
-        MainContentCaptureSessionV2 session =
-                createSession(
-                        /* enableContentCaptureReceiver= */ false,
-                        /* enableContentProtectionReceiver= */ true);
-
-        session.sendEvent(EVENT);
-        mTestableLooper.processAllMessages();
-
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-        assertThat(session.mEvents).isNull();
-    }
-
-    @Test
-    public void flush_contentCaptureDisabled_contentProtectionDisabled() throws Exception {
-        ContentCaptureOptions options =
-                createOptions(
-                        /* enableContentCaptureReceiver= */ false,
-                        /* enableContentProtectionReceiver= */ false);
-        MainContentCaptureSessionV2 session = createSession(options);
-        session.mEvents = new ArrayList<>(Arrays.asList(EVENT));
-        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
-
-        session.flush(REASON);
-        mTestableLooper.processAllMessages();
-
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-        verifyZeroInteractions(mMockContentCaptureDirectManager);
-        assertThat(session.mEvents).containsExactly(EVENT);
-    }
-
-    @Test
-    public void flush_contentCaptureDisabled_contentProtectionEnabled() {
-        MainContentCaptureSessionV2 session =
-                createSession(
-                        /* enableContentCaptureReceiver= */ false,
-                        /* enableContentProtectionReceiver= */ true);
-        session.mEvents = new ArrayList<>(Arrays.asList(EVENT));
-        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
-
-        session.flush(REASON);
-        mTestableLooper.processAllMessages();
-
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-        verifyZeroInteractions(mMockContentCaptureDirectManager);
-        assertThat(session.mEvents).containsExactly(EVENT);
-    }
-
-    @Test
-    public void flush_contentCaptureEnabled_contentProtectionDisabled() throws Exception {
-        ContentCaptureOptions options =
-                createOptions(
-                        /* enableContentCaptureReceiver= */ true,
-                        /* enableContentProtectionReceiver= */ false);
-        MainContentCaptureSessionV2 session = createSession(options);
-        session.mEvents = new ArrayList<>(Arrays.asList(EVENT));
-        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
-
-        session.flush(REASON);
-        mTestableLooper.processAllMessages();
-
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-        assertThat(session.mEvents).isEmpty();
-        assertEventFlushedContentCapture(options);
-    }
-
-    @Test
-    public void flush_contentCaptureEnabled_contentProtectionEnabled() throws Exception {
-        ContentCaptureOptions options =
-                createOptions(
-                        /* enableContentCaptureReceiver= */ true,
-                        /* enableContentProtectionReceiver= */ true);
-        MainContentCaptureSessionV2 session = createSession(options);
-        session.mEvents = new ArrayList<>(Arrays.asList(EVENT));
-        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
-
-        session.flush(REASON);
-        mTestableLooper.processAllMessages();
-
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-        assertThat(session.mEvents).isEmpty();
-        assertEventFlushedContentCapture(options);
-    }
-
-    @Test
-    public void destroySession() throws Exception {
-        MainContentCaptureSessionV2 session = createSession();
-        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
-
-        session.destroySession();
-        mTestableLooper.processAllMessages();
-
-        verify(mMockSystemServerInterface).finishSession(anyInt());
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-        assertThat(session.mDirectServiceInterface).isNull();
-        assertThat(session.mContentProtectionEventProcessor).isNull();
-    }
-
-    @Test
-    public void resetSession() {
-        MainContentCaptureSessionV2 session = createSession();
-        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
-
-        session.resetSession(/* newState= */ 0);
-        mTestableLooper.processAllMessages();
-
-        verifyZeroInteractions(mMockSystemServerInterface);
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-        assertThat(session.mDirectServiceInterface).isNull();
-        assertThat(session.mContentProtectionEventProcessor).isNull();
-    }
-
-    @Test
-    @SuppressWarnings("GuardedBy")
-    public void notifyContentCaptureEvents_notStarted_ContentCaptureDisabled_ProtectionDisabled() {
-        ContentCaptureOptions options =
-                createOptions(
-                        /* enableContentCaptureReceiver= */ false,
-                        /* enableContentProtectionReceiver= */ false);
-        MainContentCaptureSessionV2 session = createSession(options);
-
-        notifyContentCaptureEvents(session);
-        mTestableLooper.processAllMessages();
-
-        verifyZeroInteractions(mMockContentCaptureDirectManager);
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-        assertThat(session.mEvents).isNull();
-    }
-
-    @Test
-    @SuppressWarnings("GuardedBy")
-    public void notifyContentCaptureEvents_started_ContentCaptureDisabled_ProtectionDisabled() {
-        ContentCaptureOptions options =
-                createOptions(
-                        /* enableContentCaptureReceiver= */ false,
-                        /* enableContentProtectionReceiver= */ false);
-        MainContentCaptureSessionV2 session = createSession(options);
-
-        session.onSessionStarted(0x2, null);
-        notifyContentCaptureEvents(session);
-        mTestableLooper.processAllMessages();
-
-        verifyZeroInteractions(mMockContentCaptureDirectManager);
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-        assertThat(session.mEvents).isNull();
-    }
-
-    @Test
-    @SuppressWarnings("GuardedBy")
-    public void notifyContentCaptureEvents_notStarted_ContentCaptureEnabled_ProtectionEnabled() {
-        ContentCaptureOptions options =
-                createOptions(
-                        /* enableContentCaptureReceiver= */ true,
-                        /* enableContentProtectionReceiver= */ true);
-        MainContentCaptureSessionV2 session = createSession(options);
-        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
-        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
-
-        notifyContentCaptureEvents(session);
-        mTestableLooper.processAllMessages();
-
-        verifyZeroInteractions(mMockContentCaptureDirectManager);
-        verifyZeroInteractions(mMockContentProtectionEventProcessor);
-        assertThat(session.mEvents).isNull();
-    }
-
-    @Test
-    @SuppressWarnings("GuardedBy")
-    public void notifyContentCaptureEvents_started_ContentCaptureEnabled_ProtectionEnabled()
-            throws RemoteException {
-        ContentCaptureOptions options =
-                createOptions(
-                        /* enableContentCaptureReceiver= */ true,
-                        /* enableContentProtectionReceiver= */ true);
-        MainContentCaptureSessionV2 session = createSession(options);
-        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
-
-        session.onSessionStarted(0x2, null);
-        // Override the processor for interaction verification.
-        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
-        notifyContentCaptureEvents(session);
-        mTestableLooper.processAllMessages();
-
-        // Force flush will happen twice.
-        verify(mMockContentCaptureDirectManager, times(1))
-                .sendEvents(any(), eq(FLUSH_REASON_VIEW_TREE_APPEARING), any());
-        verify(mMockContentCaptureDirectManager, times(1))
-                .sendEvents(any(), eq(FLUSH_REASON_VIEW_TREE_APPEARED), any());
-        // Other than the five view events, there will be two additional tree appearing events.
-        verify(mMockContentProtectionEventProcessor, times(7)).processEvent(any());
-        assertThat(session.mEvents).isEmpty();
-    }
-
-    @Test
-    public void notifyViewAppearedBelowMaximumBufferSize() throws RemoteException {
-        ContentCaptureOptions options =
-                createOptions(
-                        /* enableContentCaptureReceiver= */ true,
-                        /* enableContentProtectionReceiver= */ true);
-        MainContentCaptureSessionV2 session = createSession(options);
-        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
-
-        session.onSessionStarted(0x2, null);
-        for (int i = 0; i < BUFFER_SIZE - 1; i++) {
-            View view = prepareView(session);
-            session.notifyViewAppeared(session.newViewStructure(view));
-        }
-        mTestableLooper.processAllMessages();
-
-        verify(mMockContentCaptureDirectManager, times(0))
-                .sendEvents(any(), anyInt(), any());
-        assertThat(session.mEvents).isNull();
-        assertThat(session.mEventProcessQueue).hasSize(BUFFER_SIZE - 1);
-    }
-
-    @Test
-    public void notifyViewAppearedExactAsMaximumBufferSize() throws RemoteException {
-        ContentCaptureOptions options =
-                createOptions(
-                        /* enableContentCaptureReceiver= */ true,
-                        /* enableContentProtectionReceiver= */ true);
-        MainContentCaptureSessionV2 session = createSession(options);
-        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
-
-        session.onSessionStarted(0x2, null);
-        for (int i = 0; i < BUFFER_SIZE; i++) {
-            View view = prepareView(session);
-            session.notifyViewAppeared(session.newViewStructure(view));
-        }
-        mTestableLooper.processAllMessages();
-
-        verify(mMockContentCaptureDirectManager, times(1))
-                .sendEvents(any(), anyInt(), any());
-        assertThat(session.mEvents).isEmpty();
-        assertThat(session.mEventProcessQueue).isEmpty();
-    }
-
-    @Test
-    public void notifyViewAppearedAboveMaximumBufferSize() throws RemoteException {
-        ContentCaptureOptions options =
-                createOptions(
-                        /* enableContentCaptureReceiver= */ true,
-                        /* enableContentProtectionReceiver= */ true);
-        MainContentCaptureSessionV2 session = createSession(options);
-        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
-
-        session.onSessionStarted(0x2, null);
-        for (int i = 0; i < BUFFER_SIZE * 2 + 1; i++) {
-            View view = prepareView(session);
-            session.notifyViewAppeared(session.newViewStructure(view));
-        }
-        mTestableLooper.processAllMessages();
-
-        verify(mMockContentCaptureDirectManager, times(2))
-                .sendEvents(any(), anyInt(), any());
-        assertThat(session.mEvents).isEmpty();
-        assertThat(session.mEventProcessQueue).hasSize(1);
-    }
-
-    /** Simulates the regular content capture events sequence. */
-    private void notifyContentCaptureEvents(final MainContentCaptureSessionV2 session) {
-        final ArrayList<Object> events = new ArrayList<>(
-                List.of(
-                        prepareView(session),
-                        prepareView(session),
-                        new AutofillId(0),
-                        prepareView(session),
-                        Insets.of(0, 0, 0, 0)
-                )
-        );
-
-        final SparseArray<ArrayList<Object>> contentCaptureEvents = new SparseArray<>();
-        contentCaptureEvents.set(session.getId(), events);
-
-        session.notifyContentCaptureEvents(contentCaptureEvents);
-    }
-
-    private View prepareView(final MainContentCaptureSessionV2 session) {
-        final View view = new View(sContext);
-        view.setContentCaptureSession(session);
-        return view;
-    }
-
-    private static ContentCaptureOptions createOptions(
-            boolean enableContentCaptureReceiver,
-            ContentCaptureOptions.ContentProtectionOptions contentProtectionOptions) {
-        return new ContentCaptureOptions(
-                /* loggingLevel= */ 0,
-                BUFFER_SIZE,
-                /* idleFlushingFrequencyMs= */ 0,
-                /* textChangeFlushingFrequencyMs= */ 0,
-                /* logHistorySize= */ 0,
-                /* disableFlushForViewTreeAppearing= */ false,
-                enableContentCaptureReceiver,
-                contentProtectionOptions,
-                /* whitelistedComponents= */ null);
-    }
-
-    private static ContentCaptureOptions createOptions(
-            boolean enableContentCaptureReceiver, boolean enableContentProtectionReceiver) {
-        return createOptions(
-                enableContentCaptureReceiver,
-                new ContentCaptureOptions.ContentProtectionOptions(
-                        enableContentProtectionReceiver,
-                        BUFFER_SIZE,
-                        /* requiredGroups= */ List.of(List.of("a")),
-                        /* optionalGroups= */ Collections.emptyList(),
-                        /* optionalGroupsThreshold= */ 0));
-    }
-
-    private ContentCaptureManager createManager(ContentCaptureOptions options) {
-        return new ContentCaptureManager(sContext, mMockSystemServerInterface, options);
-    }
-
-    private MainContentCaptureSessionV2 createSession(ContentCaptureManager manager) {
-        final Handler testHandler = Handler.createAsync(mTestableLooper.getLooper());
-        MainContentCaptureSessionV2 session =
-                new MainContentCaptureSessionV2(
-                        sStrippedContext,
-                        manager,
-                        testHandler,
-                        testHandler,
-                        mMockSystemServerInterface);
-        session.mComponentName = COMPONENT_NAME;
-        return session;
-    }
-
-    private MainContentCaptureSessionV2 createSession(ContentCaptureOptions options) {
-        return createSession(createManager(options));
-    }
-
-    private MainContentCaptureSessionV2 createSession(
-            boolean enableContentCaptureReceiver, boolean enableContentProtectionReceiver) {
-        return createSession(
-                createOptions(enableContentCaptureReceiver, enableContentProtectionReceiver));
-    }
-
-    private MainContentCaptureSessionV2 createSession() {
-        return createSession(
-                /* enableContentCaptureReceiver= */ true,
-                /* enableContentProtectionReceiver= */ true);
-    }
-
-    private void assertEventFlushedContentCapture(ContentCaptureOptions options) throws Exception {
-        ArgumentCaptor<ParceledListSlice> captor = ArgumentCaptor.forClass(ParceledListSlice.class);
-        verify(mMockContentCaptureDirectManager)
-                .sendEvents(captor.capture(), eq(REASON), eq(options));
-
-        assertThat(captor.getValue()).isNotNull();
-        List<ContentCaptureEvent> actual = captor.getValue().getList();
-        assertThat(actual).isNotNull();
-        assertThat(actual).containsExactly(EVENT);
-    }
-}
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml
index 04ad572..a30cfb7 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml
@@ -20,5 +20,6 @@
     android:shape="rectangle">
     <corners
         android:radius="@dimen/desktop_mode_maximize_menu_buttons_outline_radius"/>
+    <solid android:color="?androidprv:attr/materialColorSurfaceContainerLow"/>
     <stroke android:width="1dp" android:color="?androidprv:attr/materialColorOutlineVariant"/>
 </shape>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index 9599658..7d5f9cd 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -31,23 +31,15 @@
         android:layout_height="wrap_content"
         android:orientation="vertical">
 
-        <FrameLayout
-            android:id="@+id/maximize_menu_maximize_button_layout"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:background="@drawable/desktop_mode_maximize_menu_layout_background"
-            android:padding="4dp"
+        <Button
+            android:layout_width="94dp"
+            android:layout_height="60dp"
+            android:id="@+id/maximize_menu_maximize_button"
+            style="?android:attr/buttonBarButtonStyle"
+            android:stateListAnimator="@null"
             android:layout_marginRight="8dp"
             android:layout_marginBottom="4dp"
-            android:alpha="0">
-            <Button
-                android:id="@+id/maximize_menu_maximize_button"
-                style="?android:attr/buttonBarButtonStyle"
-                android:layout_width="86dp"
-                android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
-                android:background="@drawable/desktop_mode_maximize_menu_button_background"
-                android:stateListAnimator="@null"/>
-        </FrameLayout>
+            android:alpha="0"/>
 
         <TextView
             android:id="@+id/maximize_menu_maximize_window_text"
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 89cddc3..595d346 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -482,6 +482,12 @@
 
     <!-- The radius of the layout outline around the maximize menu buttons. -->
     <dimen name="desktop_mode_maximize_menu_buttons_outline_radius">6dp</dimen>
+    <!-- The stroke width of the outline around the maximize menu buttons. -->
+    <dimen name="desktop_mode_maximize_menu_buttons_outline_stroke">1dp</dimen>
+    <!-- The radius of the inner fill of the maximize menu buttons. -->
+    <dimen name="desktop_mode_maximize_menu_buttons_fill_radius">4dp</dimen>
+    <!-- The padding between the outline and fill of the maximize menu buttons. -->
+    <dimen name="desktop_mode_maximize_menu_buttons_fill_padding">4dp</dimen>
 
     <!-- The corner radius of the maximize menu. -->
     <dimen name="desktop_mode_maximize_menu_corner_radius">8dp</dimen>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
index 299da13..9b27e41 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -133,7 +133,9 @@
      */
     public void invalidate(Transitions transitions) {
         transitions.unregisterObserver(this);
-        // Unregister the listener to ensure any registered binder death recipients are unlinked
-        mListener.unregister();
+        if (mListener != null) {
+            // Unregister the listener to ensure any registered binder death recipients are unlinked
+            mListener.unregister();
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index c903d3b..0470367 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -19,17 +19,28 @@
 import android.animation.AnimatorSet
 import android.animation.ObjectAnimator
 import android.animation.ValueAnimator
+import android.annotation.ColorInt
 import android.annotation.IdRes
 import android.app.ActivityManager.RunningTaskInfo
 import android.content.Context
+import android.content.res.ColorStateList
 import android.content.res.Resources
+import android.graphics.Paint
 import android.graphics.PixelFormat
 import android.graphics.PointF
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.LayerDrawable
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.StateListDrawable
+import android.graphics.drawable.shapes.RoundRectShape
+import android.util.StateSet
 import android.view.LayoutInflater
 import android.view.MotionEvent
 import android.view.SurfaceControl
 import android.view.SurfaceControl.Transaction
 import android.view.SurfaceControlViewHost
+import android.view.View
 import android.view.View.OnClickListener
 import android.view.View.OnGenericMotionListener
 import android.view.View.OnTouchListener
@@ -39,18 +50,21 @@
 import android.view.WindowManager
 import android.view.WindowlessWindowManager
 import android.widget.Button
-import android.widget.FrameLayout
-import android.widget.LinearLayout
 import android.widget.TextView
 import android.window.TaskConstants
-import androidx.core.content.withStyledAttributes
-import com.android.internal.R.attr.colorAccentPrimary
+import androidx.compose.material3.ColorScheme
+import androidx.compose.ui.graphics.toArgb
+import androidx.core.animation.addListener
 import com.android.wm.shell.R
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.animation.Interpolators.EMPHASIZED_DECELERATE
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer
+import com.android.wm.shell.windowdecor.common.DecorThemeUtil
+import com.android.wm.shell.windowdecor.common.OPACITY_12
+import com.android.wm.shell.windowdecor.common.OPACITY_40
+import com.android.wm.shell.windowdecor.common.withAlpha
 import java.util.function.Supplier
 
 
@@ -71,9 +85,9 @@
         private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() }
 ) {
     private var maximizeMenu: AdditionalViewHostViewContainer? = null
+    private var maximizeMenuView: MaximizeMenuView? = null
     private lateinit var viewHost: SurfaceControlViewHost
     private lateinit var leash: SurfaceControl
-    private val openMenuAnimatorSet = AnimatorSet()
     private val cornerRadius = loadDimensionPixelSize(
             R.dimen.desktop_mode_maximize_menu_corner_radius
     ).toFloat()
@@ -81,12 +95,6 @@
     private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height)
     private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding)
 
-    private lateinit var snapRightButton: Button
-    private lateinit var snapLeftButton: Button
-    private lateinit var maximizeButton: Button
-    private lateinit var maximizeButtonLayout: FrameLayout
-    private lateinit var snapButtonsLayout: LinearLayout
-
     /** Position the menu relative to the caption's position. */
     fun positionMenu(position: PointF, t: Transaction) {
         menuPosition.set(position)
@@ -97,24 +105,20 @@
     fun show() {
         if (maximizeMenu != null) return
         createMaximizeMenu()
-        setupMaximizeMenu()
-        animateOpenMenu()
+        maximizeMenuView?.animateOpenMenu()
     }
 
     /** Closes the maximize window and releases its view. */
     fun close() {
-        openMenuAnimatorSet.cancel()
+        maximizeMenuView?.cancelAnimation()
         maximizeMenu?.releaseView()
         maximizeMenu = null
+        maximizeMenuView = null
     }
 
     /** Create a maximize menu that is attached to the display area. */
     private fun createMaximizeMenu() {
         val t = transactionSupplier.get()
-        val v = LayoutInflater.from(decorWindowContext).inflate(
-                R.layout.desktop_mode_window_decor_maximize_menu,
-                null // Root
-        )
         val builder = SurfaceControl.Builder()
         rootTdaOrganizer.attachToDisplayArea(taskInfo.displayId, builder)
         leash = builder
@@ -138,7 +142,17 @@
         viewHost = SurfaceControlViewHost(decorWindowContext,
                 displayController.getDisplay(taskInfo.displayId), windowManager,
                 "MaximizeMenu")
-        viewHost.setView(v, lp)
+        maximizeMenuView = MaximizeMenuView(
+            context = decorWindowContext,
+            menuHeight = menuHeight,
+            menuPadding = menuPadding,
+            onClickListener = onClickListener,
+            onTouchListener = onTouchListener,
+            onGenericMotionListener = onGenericMotionListener,
+        ).also { menuView ->
+            menuView.bind(taskInfo)
+            viewHost.setView(menuView.rootView, lp)
+        }
 
         // Bring menu to front when open
         t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU)
@@ -154,76 +168,6 @@
         }
     }
 
-    private fun animateOpenMenu() {
-        val maximizeMenuView = maximizeMenu?.view ?: return
-        val maximizeWindowText = maximizeMenuView.requireViewById<TextView>(
-                R.id.maximize_menu_maximize_window_text)
-        val snapWindowText = maximizeMenuView.requireViewById<TextView>(
-                R.id.maximize_menu_snap_window_text)
-
-        openMenuAnimatorSet.playTogether(
-                ObjectAnimator.ofFloat(maximizeMenuView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f)
-                        .apply {
-                            duration = MENU_HEIGHT_ANIMATION_DURATION_MS
-                            interpolator = EMPHASIZED_DECELERATE
-                        },
-                ValueAnimator.ofFloat(STARTING_MENU_HEIGHT_SCALE, 1f)
-                        .apply {
-                            duration = MENU_HEIGHT_ANIMATION_DURATION_MS
-                            interpolator = EMPHASIZED_DECELERATE
-                            addUpdateListener {
-                                // Animate padding so that controls stay pinned to the bottom of
-                                // the menu.
-                                val value = animatedValue as Float
-                                val topPadding = menuPadding -
-                                        ((1 - value) * menuHeight).toInt()
-                                maximizeMenuView.setPadding(menuPadding, topPadding,
-                                        menuPadding, menuPadding)
-                            }
-                        },
-                ValueAnimator.ofFloat(1 / STARTING_MENU_HEIGHT_SCALE, 1f).apply {
-                            duration = MENU_HEIGHT_ANIMATION_DURATION_MS
-                            interpolator = EMPHASIZED_DECELERATE
-                            addUpdateListener {
-                                // Scale up the children of the maximize menu so that the menu
-                                // scale is cancelled out and only the background is scaled.
-                                val value = animatedValue as Float
-                                maximizeButtonLayout.scaleY = value
-                                snapButtonsLayout.scaleY = value
-                                maximizeWindowText.scaleY = value
-                                snapWindowText.scaleY = value
-                            }
-                        },
-                ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Y,
-                        (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply {
-                    duration = MENU_HEIGHT_ANIMATION_DURATION_MS
-                    interpolator = EMPHASIZED_DECELERATE
-                },
-                ObjectAnimator.ofInt(maximizeMenuView.background, "alpha",
-                        MAX_DRAWABLE_ALPHA_VALUE).apply {
-                    duration = ALPHA_ANIMATION_DURATION_MS
-                },
-                ValueAnimator.ofFloat(0f, 1f)
-                        .apply {
-                            duration = ALPHA_ANIMATION_DURATION_MS
-                            startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
-                            addUpdateListener {
-                                val value = animatedValue as Float
-                                maximizeButtonLayout.alpha = value
-                                snapButtonsLayout.alpha = value
-                                maximizeWindowText.alpha = value
-                                snapWindowText.alpha = value
-                            }
-                        },
-                ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Z, MENU_Z_TRANSLATION)
-                        .apply {
-                            duration = ELEVATION_ANIMATION_DURATION_MS
-                            startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
-                        }
-        )
-        openMenuAnimatorSet.start()
-    }
-
     private fun loadDimensionPixelSize(resourceId: Int): Int {
         return if (resourceId == Resources.ID_NULL) {
             0
@@ -232,31 +176,6 @@
         }
     }
 
-    private fun setupMaximizeMenu() {
-        val maximizeMenuView = maximizeMenu?.view ?: return
-
-        maximizeMenuView.setOnGenericMotionListener(onGenericMotionListener)
-        maximizeMenuView.setOnTouchListener(onTouchListener)
-
-        maximizeButtonLayout = maximizeMenuView.requireViewById(
-                R.id.maximize_menu_maximize_button_layout)
-
-        maximizeButton = maximizeMenuView.requireViewById(R.id.maximize_menu_maximize_button)
-        maximizeButton.setOnClickListener(onClickListener)
-        maximizeButton.setOnGenericMotionListener(onGenericMotionListener)
-
-        snapRightButton = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_right_button)
-        snapRightButton.setOnClickListener(onClickListener)
-        snapRightButton.setOnGenericMotionListener(onGenericMotionListener)
-
-        snapLeftButton = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_left_button)
-        snapLeftButton.setOnClickListener(onClickListener)
-        snapLeftButton.setOnGenericMotionListener(onGenericMotionListener)
-
-        snapButtonsLayout = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_menu_layout)
-        snapButtonsLayout.setOnGenericMotionListener(onGenericMotionListener)
-    }
-
     /**
      * A valid menu input is one of the following:
      * An input that happens in the menu views.
@@ -278,65 +197,435 @@
         return maximizeMenu?.view?.isLaidOut ?: false
     }
 
+    /**
+     * Called when a [MotionEvent.ACTION_HOVER_ENTER] is triggered on any of the menu's views.
+     *
+     * TODO(b/346440693): this is only needed for the left/right snap options that don't support
+     *  selector states to manage its hover state. Look into whether that can be added to avoid
+     *  manually tracking hover enter/exit motion events. Also because those button colors/states
+     *  aren't updating correctly for pressed, focused and selected states.
+     *  See also [onMaximizeMenuHoverMove] and [onMaximizeMenuHoverExit].
+     */
     fun onMaximizeMenuHoverEnter(viewId: Int, ev: MotionEvent) {
         setSnapButtonsColorOnHover(viewId, ev)
     }
 
+    /** Called when a [MotionEvent.ACTION_HOVER_MOVE] is triggered on any of the menu's views. */
     fun onMaximizeMenuHoverMove(viewId: Int, ev: MotionEvent) {
         setSnapButtonsColorOnHover(viewId, ev)
     }
 
+    /** Called when a [MotionEvent.ACTION_HOVER_EXIT] is triggered on any of the menu's views. */
     fun onMaximizeMenuHoverExit(id: Int, ev: MotionEvent) {
-        val inSnapMenuBounds = ev.x >= 0 && ev.x <= snapButtonsLayout.width &&
-                ev.y >= 0 && ev.y <= snapButtonsLayout.height
-        val colorList = decorWindowContext.getColorStateList(
-                R.color.desktop_mode_maximize_menu_button_color_selector)
+        val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return
+        val snapOptionsHeight = maximizeMenuView?.snapOptionsHeight ?: return
+        val inSnapMenuBounds = ev.x >= 0 && ev.x <= snapOptionsWidth &&
+                ev.y >= 0 && ev.y <= snapOptionsHeight
 
-        if (id == R.id.maximize_menu_maximize_button) {
-            maximizeButton.background?.setTintList(colorList)
-            maximizeButtonLayout.setBackgroundResource(
-                    R.drawable.desktop_mode_maximize_menu_layout_background)
-        } else if (id == R.id.maximize_menu_snap_menu_layout && !inSnapMenuBounds) {
+        if (id == R.id.maximize_menu_snap_menu_layout && !inSnapMenuBounds) {
             // After exiting the snap menu layout area, checks to see that user is not still
             // hovering within the snap menu layout bounds which would indicate that the user is
             // hovering over a snap button within the snap menu layout rather than having exited.
-            snapLeftButton.background?.setTintList(colorList)
-            snapLeftButton.background?.alpha = 255
-            snapRightButton.background?.setTintList(colorList)
-            snapRightButton.background?.alpha = 255
-            snapButtonsLayout.setBackgroundResource(
-                    R.drawable.desktop_mode_maximize_menu_layout_background)
+            maximizeMenuView?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.NONE)
         }
     }
 
     private fun setSnapButtonsColorOnHover(viewId: Int, ev: MotionEvent) {
-        decorWindowContext.withStyledAttributes(null, intArrayOf(colorAccentPrimary), 0, 0) {
-            val materialColor = getColor(0, 0)
-            val snapMenuCenter = snapButtonsLayout.width / 2
-            if (viewId == R.id.maximize_menu_maximize_button) {
-                // Highlight snap maximize window button
-                maximizeButton.background?.setTint(materialColor)
-                maximizeButtonLayout.setBackgroundResource(
-                        R.drawable.desktop_mode_maximize_menu_layout_background_on_hover)
-            } else if (viewId == R.id.maximize_menu_snap_left_button ||
-                    (viewId == R.id.maximize_menu_snap_menu_layout && ev.x <= snapMenuCenter)) {
-                // Highlight snap left button
-                snapRightButton.background?.setTint(materialColor)
-                snapLeftButton.background?.setTint(materialColor)
-                snapButtonsLayout.setBackgroundResource(
-                        R.drawable.desktop_mode_maximize_menu_layout_background_on_hover)
-                snapRightButton.background?.alpha = 102
-                snapLeftButton.background?.alpha = 255
-            } else if (viewId == R.id.maximize_menu_snap_right_button ||
-                    (viewId == R.id.maximize_menu_snap_menu_layout && ev.x > snapMenuCenter)) {
-                // Highlight snap right button
-                snapRightButton.background?.setTint(materialColor)
-                snapLeftButton.background?.setTint(materialColor)
-                snapButtonsLayout.setBackgroundResource(
-                        R.drawable.desktop_mode_maximize_menu_layout_background_on_hover)
-                snapRightButton.background?.alpha = 255
-                snapLeftButton.background?.alpha = 102
+        val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return
+        val snapMenuCenter = snapOptionsWidth / 2
+        when {
+            viewId == R.id.maximize_menu_snap_left_button ||
+                    (viewId == R.id.maximize_menu_snap_menu_layout && ev.x <= snapMenuCenter) -> {
+                        maximizeMenuView
+                            ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.LEFT)
             }
+            viewId == R.id.maximize_menu_snap_right_button ||
+                    (viewId == R.id.maximize_menu_snap_menu_layout && ev.x > snapMenuCenter) -> {
+                        maximizeMenuView
+                            ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.RIGHT)
+                    }
+        }
+    }
+
+    /**
+     * The view within the Maximize Menu, presents maximize, restore and snap-to-side options for
+     * resizing a Task.
+     */
+    class MaximizeMenuView(
+        context: Context,
+        private val menuHeight: Int,
+        private val menuPadding: Int,
+        onClickListener: OnClickListener,
+        onTouchListener: OnTouchListener,
+        onGenericMotionListener: OnGenericMotionListener,
+    ) {
+        val rootView: View = LayoutInflater.from(context)
+            .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */)
+        private val maximizeText =
+            requireViewById(R.id.maximize_menu_maximize_window_text) as TextView
+        private val maximizeButton =
+            requireViewById(R.id.maximize_menu_maximize_button) as Button
+        private val snapWindowText =
+            requireViewById(R.id.maximize_menu_snap_window_text) as TextView
+        private val snapRightButton =
+            requireViewById(R.id.maximize_menu_snap_right_button) as Button
+        private val snapLeftButton =
+            requireViewById(R.id.maximize_menu_snap_left_button) as Button
+        private val snapButtonsLayout =
+            requireViewById(R.id.maximize_menu_snap_menu_layout)
+
+        private val decorThemeUtil = DecorThemeUtil(context)
+
+        private val outlineRadius = context.resources
+            .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_radius)
+        private val outlineStroke = context.resources
+            .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_stroke)
+        private val fillPadding = context.resources
+            .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_padding)
+        private val fillRadius = context.resources
+            .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius)
+
+        private val openMenuAnimatorSet = AnimatorSet()
+        private lateinit var taskInfo: RunningTaskInfo
+        private lateinit var style: MenuStyle
+
+        /** The width of the snap menu option view, including both left and right snaps. */
+        val snapOptionsWidth: Int
+            get() = snapButtonsLayout.width
+        /** The height of the snap menu option view, including both left and right snaps .*/
+        val snapOptionsHeight: Int
+            get() = snapButtonsLayout.height
+
+        init {
+            // TODO(b/346441962): encapsulate menu hover enter/exit logic inside this class and
+            //  expose only what  is actually relevant to outside classes so that specific checks
+            //  against resource IDs aren't needed outside this class.
+            rootView.setOnGenericMotionListener(onGenericMotionListener)
+            rootView.setOnTouchListener(onTouchListener)
+            maximizeButton.setOnClickListener(onClickListener)
+            maximizeButton.setOnGenericMotionListener(onGenericMotionListener)
+            snapRightButton.setOnClickListener(onClickListener)
+            snapRightButton.setOnGenericMotionListener(onGenericMotionListener)
+            snapLeftButton.setOnClickListener(onClickListener)
+            snapLeftButton.setOnGenericMotionListener(onGenericMotionListener)
+            snapButtonsLayout.setOnGenericMotionListener(onGenericMotionListener)
+
+            // To prevent aliasing.
+            maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+            maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+        }
+
+        /** Bind the menu views to the new [RunningTaskInfo] data. */
+        fun bind(taskInfo: RunningTaskInfo) {
+            this.taskInfo = taskInfo
+            this.style = calculateMenuStyle(taskInfo)
+
+            rootView.background.setTint(style.backgroundColor)
+
+            // Maximize option.
+            maximizeButton.background = style.maximizeOption.drawable
+            maximizeText.setTextColor(style.textColor)
+
+            // Snap options.
+            snapWindowText.setTextColor(style.textColor)
+            updateSplitSnapSelection(SnapToHalfSelection.NONE)
+        }
+
+        /** Animate the opening of the menu */
+        fun animateOpenMenu() {
+            maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null)
+            maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null)
+            openMenuAnimatorSet.playTogether(
+                ObjectAnimator.ofFloat(rootView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f)
+                    .apply {
+                        duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+                        interpolator = EMPHASIZED_DECELERATE
+                    },
+                ValueAnimator.ofFloat(STARTING_MENU_HEIGHT_SCALE, 1f)
+                    .apply {
+                        duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+                        interpolator = EMPHASIZED_DECELERATE
+                        addUpdateListener {
+                            // Animate padding so that controls stay pinned to the bottom of
+                            // the menu.
+                            val value = animatedValue as Float
+                            val topPadding = menuPadding -
+                                    ((1 - value) * menuHeight).toInt()
+                            rootView.setPadding(menuPadding, topPadding,
+                                menuPadding, menuPadding)
+                        }
+                    },
+                ValueAnimator.ofFloat(1 / STARTING_MENU_HEIGHT_SCALE, 1f).apply {
+                    duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+                    interpolator = EMPHASIZED_DECELERATE
+                    addUpdateListener {
+                        // Scale up the children of the maximize menu so that the menu
+                        // scale is cancelled out and only the background is scaled.
+                        val value = animatedValue as Float
+                        maximizeButton.scaleY = value
+                        snapButtonsLayout.scaleY = value
+                        maximizeText.scaleY = value
+                        snapWindowText.scaleY = value
+                    }
+                },
+                ObjectAnimator.ofFloat(rootView, TRANSLATION_Y,
+                    (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply {
+                    duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+                    interpolator = EMPHASIZED_DECELERATE
+                },
+                ObjectAnimator.ofInt(rootView.background, "alpha",
+                    MAX_DRAWABLE_ALPHA_VALUE).apply {
+                    duration = ALPHA_ANIMATION_DURATION_MS
+                },
+                ValueAnimator.ofFloat(0f, 1f)
+                    .apply {
+                        duration = ALPHA_ANIMATION_DURATION_MS
+                        startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
+                        addUpdateListener {
+                            val value = animatedValue as Float
+                            maximizeButton.alpha = value
+                            snapButtonsLayout.alpha = value
+                            maximizeText.alpha = value
+                            snapWindowText.alpha = value
+                        }
+                    },
+                ObjectAnimator.ofFloat(rootView, TRANSLATION_Z, MENU_Z_TRANSLATION)
+                    .apply {
+                        duration = ELEVATION_ANIMATION_DURATION_MS
+                        startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
+                    }
+            )
+            openMenuAnimatorSet.addListener(
+                onEnd = {
+                    maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+                    maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+                }
+            )
+            openMenuAnimatorSet.start()
+        }
+
+        /** Cancel the open menu animation. */
+        fun cancelAnimation() {
+            openMenuAnimatorSet.cancel()
+        }
+
+        /** Update the view state to a new snap to half selection. */
+        fun updateSplitSnapSelection(selection: SnapToHalfSelection) {
+            when (selection) {
+                SnapToHalfSelection.NONE -> deactivateSnapOptions()
+                SnapToHalfSelection.LEFT -> activateSnapOption(activateLeft = true)
+                SnapToHalfSelection.RIGHT -> activateSnapOption(activateLeft = false)
+            }
+        }
+
+        private fun calculateMenuStyle(taskInfo: RunningTaskInfo): MenuStyle {
+            val colorScheme = decorThemeUtil.getColorScheme(taskInfo)
+            val menuBackgroundColor = colorScheme.surfaceContainerLow.toArgb()
+            return MenuStyle(
+                backgroundColor = menuBackgroundColor,
+                textColor = colorScheme.onSurface.toArgb(),
+                maximizeOption = MenuStyle.MaximizeOption(
+                    drawable = createMaximizeDrawable(menuBackgroundColor, colorScheme)
+                ),
+                snapOptions = MenuStyle.SnapOptions(
+                    inactiveSnapSideColor = colorScheme.outlineVariant.toArgb(),
+                    semiActiveSnapSideColor = colorScheme.primary.toArgb().withAlpha(OPACITY_40),
+                    activeSnapSideColor = colorScheme.primary.toArgb(),
+                    inactiveStrokeColor = colorScheme.outlineVariant.toArgb(),
+                    activeStrokeColor = colorScheme.primary.toArgb(),
+                    inactiveBackgroundColor = menuBackgroundColor,
+                    activeBackgroundColor = colorScheme.primary.toArgb().withAlpha(OPACITY_12)
+                ),
+            )
+        }
+
+        private fun deactivateSnapOptions() {
+            // TODO(b/346440693): the background/colorStateList set on these buttons is overridden
+            //  to a static resource & color on manually tracked hover events, which defeats the
+            //  point of state lists and selector states. Look into whether changing that is
+            //  possible, similar to the maximize option. Also to include support for the
+            //  semi-active state (when the "other" snap option is selected).
+            val snapSideColorList = ColorStateList(
+                arrayOf(
+                    intArrayOf(android.R.attr.state_pressed),
+                    intArrayOf(android.R.attr.state_focused),
+                    intArrayOf(android.R.attr.state_selected),
+                    intArrayOf(),
+                ),
+                intArrayOf(
+                    style.snapOptions.activeSnapSideColor,
+                    style.snapOptions.activeSnapSideColor,
+                    style.snapOptions.activeSnapSideColor,
+                    style.snapOptions.inactiveSnapSideColor
+                )
+            )
+            snapLeftButton.background?.setTintList(snapSideColorList)
+            snapRightButton.background?.setTintList(snapSideColorList)
+            with (snapButtonsLayout) {
+                setBackgroundResource(R.drawable.desktop_mode_maximize_menu_layout_background)
+                (background as GradientDrawable).apply {
+                    setColor(style.snapOptions.inactiveBackgroundColor)
+                    setStroke(outlineStroke, style.snapOptions.inactiveStrokeColor)
+                }
+            }
+        }
+
+        private fun activateSnapOption(activateLeft: Boolean) {
+            // Regardless of which side is active, the background of the snap options layout (that
+            // includes both sides) is considered "active".
+            with (snapButtonsLayout) {
+                setBackgroundResource(
+                    R.drawable.desktop_mode_maximize_menu_layout_background_on_hover)
+                (background as GradientDrawable).apply {
+                    setColor(style.snapOptions.activeBackgroundColor)
+                    setStroke(outlineStroke, style.snapOptions.activeStrokeColor)
+                }
+            }
+            if (activateLeft) {
+                // Highlight snap left button, partially highlight the other side.
+                snapLeftButton.background.setTint(style.snapOptions.activeSnapSideColor)
+                snapRightButton.background.setTint(style.snapOptions.semiActiveSnapSideColor)
+            } else {
+                // Highlight snap right button, partially highlight the other side.
+                snapRightButton.background.setTint(style.snapOptions.activeSnapSideColor)
+                snapLeftButton.background.setTint(style.snapOptions.semiActiveSnapSideColor)
+            }
+        }
+
+        private fun createMaximizeDrawable(
+            @ColorInt menuBackgroundColor: Int,
+            colorScheme: ColorScheme
+        ): StateListDrawable {
+            val activeStrokeAndFill = colorScheme.primary.toArgb()
+            val activeBackground = colorScheme.primary.toArgb().withAlpha(OPACITY_12)
+            val activeDrawable = createMaximizeButtonDrawable(
+                strokeAndFillColor = activeStrokeAndFill,
+                backgroundColor = activeBackground,
+                // Add a mask with the menu background's color because the active background color is
+                // semi transparent, otherwise the transparency will reveal the stroke/fill color
+                // behind it.
+                backgroundMask = menuBackgroundColor
+            )
+            return StateListDrawable().apply {
+                addState(intArrayOf(android.R.attr.state_pressed), activeDrawable)
+                addState(intArrayOf(android.R.attr.state_focused), activeDrawable)
+                addState(intArrayOf(android.R.attr.state_selected), activeDrawable)
+                addState(intArrayOf(android.R.attr.state_hovered), activeDrawable)
+                // Inactive drawable.
+                addState(
+                    StateSet.WILD_CARD,
+                    createMaximizeButtonDrawable(
+                        strokeAndFillColor = colorScheme.outlineVariant.toArgb(),
+                        backgroundColor = colorScheme.surfaceContainerLow.toArgb(),
+                        backgroundMask = null // not needed because the bg color is fully opaque
+                    )
+                )
+            }
+        }
+
+        private fun createMaximizeButtonDrawable(
+            @ColorInt strokeAndFillColor: Int,
+            @ColorInt backgroundColor: Int,
+            @ColorInt backgroundMask: Int?
+        ): LayerDrawable {
+            val layers = mutableListOf<Drawable>()
+            // First (bottom) layer, effectively the button's border ring once its inner shape is
+            // covered by the next layers.
+            layers.add(ShapeDrawable().apply {
+                shape = RoundRectShape(
+                    FloatArray(8) { outlineRadius.toFloat() },
+                    null /* inset */,
+                    null /* innerRadii */
+                )
+                paint.color = strokeAndFillColor
+                paint.style = Paint.Style.FILL
+            })
+            // Second layer, a mask for the next (background) layer if needed because of
+            // transparency.
+            backgroundMask?.let { color ->
+                layers.add(
+                    ShapeDrawable().apply {
+                        shape = RoundRectShape(
+                            FloatArray(8) { outlineRadius.toFloat() },
+                            null /* inset */,
+                            null /* innerRadii */
+                        )
+                        paint.color = color
+                        paint.style = Paint.Style.FILL
+                    }
+                )
+            }
+            // Third layer, the "background" padding between the border and the fill.
+            layers.add(ShapeDrawable().apply {
+                shape = RoundRectShape(
+                    FloatArray(8) { outlineRadius.toFloat() },
+                    null /* inset */,
+                    null /* innerRadii */
+                )
+                paint.color = backgroundColor
+                paint.style = Paint.Style.FILL
+            })
+            // Final layer, the inner most rounded-rect "fill".
+            layers.add(ShapeDrawable().apply {
+                shape = RoundRectShape(
+                    FloatArray(8) { fillRadius.toFloat() },
+                    null /* inset */,
+                    null /* innerRadii */
+                )
+                paint.color = strokeAndFillColor
+                paint.style = Paint.Style.FILL
+            })
+            return LayerDrawable(layers.toTypedArray()).apply {
+                when (numberOfLayers) {
+                    3 -> {
+                        setLayerInset(1, outlineStroke)
+                        setLayerInset(2, fillPadding)
+                    }
+                    4 -> {
+                        setLayerInset(intArrayOf(1, 2), outlineStroke)
+                        setLayerInset(3, fillPadding)
+                    }
+                    else -> error("Unexpected number of layers: $numberOfLayers")
+                }
+            }
+        }
+
+        private fun LayerDrawable.setLayerInset(index: IntArray, inset: Int) {
+            for (i in index) {
+                setLayerInset(i, inset, inset, inset, inset)
+            }
+        }
+
+        private fun LayerDrawable.setLayerInset(index: Int, inset: Int) {
+            setLayerInset(index, inset, inset, inset, inset)
+        }
+
+        private fun requireViewById(id: Int) = rootView.requireViewById<View>(id)
+
+        /** The style to apply to the menu. */
+        data class MenuStyle(
+            @ColorInt val backgroundColor: Int,
+            @ColorInt val textColor: Int,
+            val maximizeOption: MaximizeOption,
+            val snapOptions: SnapOptions,
+        ) {
+            data class MaximizeOption(
+                val drawable: StateListDrawable,
+            )
+            data class SnapOptions(
+                @ColorInt val inactiveSnapSideColor: Int,
+                @ColorInt val semiActiveSnapSideColor: Int,
+                @ColorInt val activeSnapSideColor: Int,
+                @ColorInt val inactiveStrokeColor: Int,
+                @ColorInt val activeStrokeColor: Int,
+                @ColorInt val inactiveBackgroundColor: Int,
+                @ColorInt val activeBackgroundColor: Int,
+            )
+        }
+
+        /** The possible selection states of the half-snap menu option. */
+        enum class SnapToHalfSelection {
+            NONE, LEFT, RIGHT
         }
     }
 
@@ -352,7 +641,6 @@
         fun isMaximizeMenuView(@IdRes viewId: Int): Boolean {
             return viewId == R.id.maximize_menu ||
                     viewId == R.id.maximize_menu_maximize_button ||
-                    viewId == R.id.maximize_menu_maximize_button_layout ||
                     viewId == R.id.maximize_menu_snap_left_button ||
                     viewId == R.id.maximize_menu_snap_right_button ||
                     viewId == R.id.maximize_menu_snap_menu_layout ||
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt
index 8e6d1ee9..f7cfbfa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt
@@ -15,11 +15,16 @@
  */
 package com.android.wm.shell.windowdecor.common
 
+import android.annotation.ColorInt
+import android.annotation.IntRange
 import android.app.ActivityManager.RunningTaskInfo
 import android.content.Context
 import android.content.res.Configuration
 import android.content.res.Configuration.UI_MODE_NIGHT_MASK
 import android.graphics.Color
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
 
 /** The theme of a window decoration. */
 internal enum class Theme { LIGHT, DARK }
@@ -30,10 +35,31 @@
 /** Whether a [Theme] is dark. */
 internal fun Theme.isDark(): Boolean = this == Theme.DARK
 
+/** Returns a copy of the color with its [alpha] component replaced with the given value. */
+@ColorInt
+internal fun @receiver:ColorInt Int.withAlpha(@IntRange(from = 0, to = 255) alpha: Int): Int =
+    Color.argb(
+        alpha,
+        Color.red(this),
+        Color.green(this),
+        Color.blue(this)
+    )
+
+/** Common opacity values used in window decoration views. */
+const val OPACITY_100 = 255
+const val OPACITY_11 = 28
+const val OPACITY_12 = 31
+const val OPACITY_15 = 38
+const val OPACITY_40 = 102
+const val OPACITY_55 = 140
+const val OPACITY_65 = 166
+
 /**
  * Utility class for determining themes based on system settings and app's [RunningTaskInfo].
  */
 internal class DecorThemeUtil(private val context: Context) {
+    private val lightColors = dynamicLightColorScheme(context)
+    private val darkColors = dynamicDarkColorScheme(context)
 
     private val systemTheme: Theme
         get() = if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) ==
@@ -56,4 +82,13 @@
             Theme.LIGHT
         }
     }
+
+    /**
+     * Returns the [ColorScheme] to use to style window decorations based on the given
+     * [RunningTaskInfo].
+     */
+    fun getColorScheme(task: RunningTaskInfo): ColorScheme = when (getAppTheme(task)) {
+        Theme.LIGHT -> lightColors
+        Theme.DARK -> darkColors
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 0650154..46127b1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -45,6 +45,11 @@
 import com.android.wm.shell.R
 import com.android.wm.shell.windowdecor.MaximizeButtonView
 import com.android.wm.shell.windowdecor.common.DecorThemeUtil
+import com.android.wm.shell.windowdecor.common.OPACITY_100
+import com.android.wm.shell.windowdecor.common.OPACITY_11
+import com.android.wm.shell.windowdecor.common.OPACITY_15
+import com.android.wm.shell.windowdecor.common.OPACITY_55
+import com.android.wm.shell.windowdecor.common.OPACITY_65
 import com.android.wm.shell.windowdecor.common.Theme
 import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance
 import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance
@@ -491,11 +496,5 @@
         private const val DARK_THEME_UNFOCUSED_OPACITY = 140 // 55%
         private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65%
         private const val FOCUSED_OPACITY = 255
-
-        private const val OPACITY_100 = 255
-        private const val OPACITY_11 = 28
-        private const val OPACITY_15 = 38
-        private const val OPACITY_55 = 140
-        private const val OPACITY_65 = 166
     }
 }
diff --git a/media/jni/Android.bp b/media/jni/Android.bp
index 8874ff5..e619e1c 100644
--- a/media/jni/Android.bp
+++ b/media/jni/Android.bp
@@ -127,6 +127,9 @@
         "-Wunused",
         "-Wunreachable-code",
     ],
+
+    // TODO(b/330503129) Workaround build breakage.
+    lto_O0: true,
 }
 
 cc_library_shared {
diff --git a/media/jni/audioeffect/Android.bp b/media/jni/audioeffect/Android.bp
index cf5059c..7caa9e4 100644
--- a/media/jni/audioeffect/Android.bp
+++ b/media/jni/audioeffect/Android.bp
@@ -44,4 +44,7 @@
         "-Wunreachable-code",
         "-DANDROID_UTILS_REF_BASE_DISABLE_IMPLICIT_CONSTRUCTION",
     ],
+
+    // TODO(b/330503129) Workaround LTO build breakage.
+    lto_O0: true,
 }
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
index dd7c036..a59a724 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
@@ -23,8 +23,8 @@
 import android.os.UserHandle
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import org.junit.Rule
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
index c60ce41..b1baa86 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
@@ -31,6 +31,8 @@
 import android.os.BadParcelableException
 import android.os.DeadObjectException
 import android.os.UserManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -76,9 +78,7 @@
     }
 
     private val mockUserManager = mock<UserManager> {
-        on { getUserInfo(ADMIN_USER_ID) } doReturn UserInfo().apply {
-            flags = UserInfo.FLAG_ADMIN
-        }
+        on { getUserInfo(ADMIN_USER_ID) } doReturn UserInfo(0, "admin", UserInfo.FLAG_ADMIN)
         on { getProfileIdsWithDisabled(ADMIN_USER_ID) } doReturn
             intArrayOf(ADMIN_USER_ID, MANAGED_PROFILE_USER_ID)
     }
@@ -281,9 +281,9 @@
         )
     }
 
+    @EnableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
     @Test
     fun loadApps_hasApkInApexInfo_shouldNotIncludeAllHiddenApps() = runTest {
-        mSetFlagsRule.enableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
         packageManager.stub {
             on { getInstalledModules(any()) } doReturn listOf(HIDDEN_MODULE)
         }
@@ -297,9 +297,9 @@
         assertThat(appList).containsExactly(NORMAL_APP)
     }
 
+    @DisableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
     @Test
     fun loadApps_noApkInApexInfo_shouldNotIncludeHiddenSystemModule() = runTest {
-        mSetFlagsRule.disableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
         packageManager.stub {
             on { getInstalledModules(any()) } doReturn listOf(HIDDEN_MODULE)
         }
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
index f5baae2..dd149ba 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
@@ -91,25 +91,29 @@
 
     private final DisplayManager.DisplayListener mDisplayListener =
             new DisplayManager.DisplayListener() {
-        int mRotation;
+                int mRotation;
 
-        @Override
-        public void onDisplayAdded(int displayId) {}
+                @Override
+                public void onDisplayAdded(int displayId) {
+                }
 
-        @Override
-        public void onDisplayRemoved(int displayId) {
-            // TODO(b/136716947): Need to reset A11yMenuOverlayLayout by display id.
-        }
+                @Override
+                public void onDisplayRemoved(int displayId) {
+                    // TODO(b/136716947): Need to reset A11yMenuOverlayLayout by display id.
+                }
 
-        @Override
-        public void onDisplayChanged(int displayId) {
-            Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
-            if (mRotation != display.getRotation()) {
-                mRotation = display.getRotation();
-                mA11yMenuLayout.updateViewLayout();
-            }
-        }
-    };
+                @Override
+                public void onDisplayChanged(int displayId) {
+                    if (mA11yMenuLayout == null) {
+                        return;
+                    }
+                    Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
+                    if (mRotation != display.getRotation()) {
+                        mRotation = display.getRotation();
+                        mA11yMenuLayout.updateViewLayout();
+                    }
+                }
+            };
 
     private final BroadcastReceiver mHideMenuReceiver = new BroadcastReceiver() {
         @Override
@@ -373,6 +377,7 @@
     public boolean onUnbind(Intent intent) {
         unregisterReceiver(mHideMenuReceiver);
         unregisterReceiver(mToggleMenuReceiver);
+        mDisplayManager.unregisterDisplayListener(mDisplayListener);
         mPrefs.unregisterOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener);
         sInitialized = false;
         if (mA11yMenuLayout != null) {
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 1cbf67e..0fdcc7a 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -884,16 +884,6 @@
 }
 
 flag {
-    name: "shade_collapse_activity_launch_fix"
-    namespace: "systemui"
-    description: "Avoid collapsing the shade on activity launch if it is already collapsed, as this causes a flicker."
-    bug: "331591373"
-    metadata {
-      purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
     name: "slice_broadcast_relay_in_background"
     namespace: "systemui"
     description: "Move handling of slice broadcast relay broadcasts to background threads"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index c4659cf..87bac83 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -226,10 +226,14 @@
 
         scene(
             CommunalScenes.Communal,
-            userActions =
-                mapOf(Swipe(SwipeDirection.Right, fromSource = Edge.Left) to CommunalScenes.Blank)
+            userActions = mapOf(Swipe(SwipeDirection.Right) to CommunalScenes.Blank)
         ) {
-            CommunalScene(backgroundType, colors, content)
+            CommunalScene(
+                backgroundType = backgroundType,
+                colors = colors,
+                content = content,
+                modifier = Modifier.horizontalNestedScrollToScene(),
+            )
         }
     }
 
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 927890e..fbfe050 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
@@ -378,16 +378,6 @@
                 onCancel = viewModel::onEnableWorkProfileDialogCancel
             )
         }
-
-        // This spacer covers the edge of the LazyHorizontalGrid and prevents it from receiving
-        // touches, so that the SceneTransitionLayout can intercept the touches and allow an edge
-        // swipe back to the blank scene.
-        Spacer(
-            Modifier.height(Dimensions.GridHeight)
-                .align(Alignment.CenterStart)
-                .width(Dimensions.Spacing)
-                .pointerInput(Unit) {}
-        )
     }
 }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 45a8393..a2426a6 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -28,6 +28,7 @@
 import androidx.compose.foundation.layout.asPaddingValues
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.systemBars
@@ -65,6 +66,7 @@
 import androidx.compose.ui.util.lerp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.LowestZIndexScenePicker
 import com.android.compose.animation.scene.NestedScrollBehavior
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.modifiers.thenIf
@@ -89,7 +91,8 @@
     object Elements {
         val NotificationScrim = ElementKey("NotificationScrim")
         val NotificationStackPlaceholder = ElementKey("NotificationStackPlaceholder")
-        val HeadsUpNotificationPlaceholder = ElementKey("HeadsUpNotificationPlaceholder")
+        val HeadsUpNotificationPlaceholder =
+            ElementKey("HeadsUpNotificationPlaceholder", scenePicker = LowestZIndexScenePicker)
         val ShelfSpace = ElementKey("ShelfSpace")
     }
 
@@ -112,10 +115,10 @@
     modifier: Modifier = Modifier,
     isPeekFromBottom: Boolean = false,
 ) {
-    Element(
-        Notifications.Elements.HeadsUpNotificationPlaceholder,
+    Box(
         modifier =
             modifier
+                .element(Notifications.Elements.HeadsUpNotificationPlaceholder)
                 .fillMaxWidth()
                 .notificationHeadsUpHeight(stackScrollView)
                 .debugBackground(viewModel, DEBUG_HUN_COLOR)
@@ -129,9 +132,7 @@
                     // Note: boundsInWindow doesn't scroll off the screen
                     stackScrollView.setHeadsUpTop(boundsInWindow.top)
                 }
-    ) {
-        content {}
-    }
+    )
 }
 
 /** Adds the space where notification stack should appear in the scene. */
@@ -155,6 +156,11 @@
             viewModel = viewModel,
             modifier = Modifier.align(Alignment.TopCenter),
         )
+        NotificationStackCutoffGuideline(
+            stackScrollView = stackScrollView,
+            viewModel = viewModel,
+            modifier = Modifier.align(Alignment.BottomCenter),
+        )
     }
 }
 
@@ -171,6 +177,7 @@
     shouldPunchHoleBehindScrim: Boolean,
     shouldFillMaxSize: Boolean = true,
     shouldReserveSpaceForNavBar: Boolean = true,
+    shouldIncludeHeadsUpSpace: Boolean = true,
     shadeMode: ShadeMode,
     modifier: Modifier = Modifier,
 ) {
@@ -187,8 +194,12 @@
         viewModel.isCurrentGestureOverscroll.collectAsStateWithLifecycle(false)
     val expansionFraction by viewModel.expandFraction.collectAsStateWithLifecycle(0f)
 
-    val navBarHeight =
-        with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() }
+    val navBarHeightPx =
+        with(density) {
+            WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx().toInt()
+        }
+    val bottomPaddingPx = if (shouldReserveSpaceForNavBar) navBarHeightPx else 0
+
     val screenHeight = LocalRawScreenHeight.current
 
     /**
@@ -352,14 +363,13 @@
                         }
                         .verticalScroll(scrollState)
                         .fillMaxWidth()
-                        .notificationStackHeight(
-                            view = stackScrollView,
-                            padding = if (shouldReserveSpaceForNavBar) navBarHeight.toInt() else 0
-                        )
+                        .notificationStackHeight(view = stackScrollView, padding = bottomPaddingPx)
                         .onSizeChanged { size -> stackHeight.intValue = size.height },
             )
         }
-        HeadsUpNotificationSpace(stackScrollView = stackScrollView, viewModel = viewModel)
+        if (shouldIncludeHeadsUpSpace) {
+            HeadsUpNotificationSpace(stackScrollView = stackScrollView, viewModel = viewModel)
+        }
     }
 }
 
@@ -395,6 +405,29 @@
     )
 }
 
+/**
+ * A 0 height horizontal spacer to be placed at the bottom-most position in the current scene, where
+ * the notification contents (stack, footer, shelf) should be drawn.
+ */
+@Composable
+fun NotificationStackCutoffGuideline(
+    stackScrollView: NotificationScrollView,
+    viewModel: NotificationsPlaceholderViewModel,
+    modifier: Modifier = Modifier,
+) {
+    Spacer(
+        modifier =
+            modifier
+                    .fillMaxWidth()
+                    .height(0.dp)
+                    .onGloballyPositioned { coordinates ->
+                val positionY = coordinates.positionInWindow().y
+                debugLog(viewModel) { "STACK cutoff onGloballyPositioned: y=$positionY" }
+                stackScrollView.setStackCutoff(positionY)
+            }
+    )
+}
+
 @Composable
 private fun SceneScope.NotificationPlaceholder(
     stackScrollView: NotificationScrollView,
@@ -417,7 +450,6 @@
                     }
                     // NOTE: positionInWindow.y scrolls off screen, but boundsInWindow.top will not
                     stackScrollView.setStackTop(positionInWindow.y)
-                    stackScrollView.setStackBottom(positionInWindow.y + coordinates.size.height)
                 }
     )
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 1b49b67..644040d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -42,6 +42,7 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.rememberScrollState
@@ -62,6 +63,7 @@
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.scene.SceneScope
@@ -82,6 +84,7 @@
 import com.android.systemui.media.controls.ui.view.MediaHost
 import com.android.systemui.media.dagger.MediaModule
 import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
+import com.android.systemui.notifications.ui.composable.NotificationScrollingStack
 import com.android.systemui.qs.footer.ui.compose.FooterActionsWithAnimatedVisibility
 import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset
 import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaOffset.InQS
@@ -90,6 +93,7 @@
 import com.android.systemui.scene.session.ui.composable.SaveableSession
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.ComposableScene
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.shade.ui.composable.CollapsedShadeHeader
 import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
 import com.android.systemui.shade.ui.composable.Shade
@@ -102,6 +106,7 @@
 import dagger.Lazy
 import javax.inject.Inject
 import javax.inject.Named
+import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.stateIn
@@ -400,5 +405,16 @@
             modifier = Modifier.align(Alignment.BottomCenter),
             isPeekFromBottom = true,
         )
+        NotificationScrollingStack(
+            shadeSession = shadeSession,
+            stackScrollView = notificationStackScrollView,
+            viewModel = notificationsPlaceholderViewModel,
+            maxScrimTop = { screenHeight },
+            shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim,
+            shouldIncludeHeadsUpSpace = false,
+            shadeMode = ShadeMode.Single,
+            modifier =
+                Modifier.fillMaxWidth().offset { IntOffset(x = 0, y = screenHeight.roundToInt()) },
+        )
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index ec81e23..edef5fb 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -35,6 +35,7 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.navigationBarsPadding
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.rememberScrollState
@@ -82,6 +83,7 @@
 import com.android.systemui.media.controls.ui.view.MediaHostState
 import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
 import com.android.systemui.notifications.ui.composable.NotificationScrollingStack
+import com.android.systemui.notifications.ui.composable.NotificationStackCutoffGuideline
 import com.android.systemui.qs.footer.ui.compose.FooterActionsWithAnimatedVisibility
 import com.android.systemui.qs.ui.composable.BrightnessMirror
 import com.android.systemui.qs.ui.composable.QSMediaMeasurePolicy
@@ -350,6 +352,11 @@
                 notificationsPlaceable.placeRelative(x = 0, y = maxNotifScrimTop.value.roundToInt())
             }
         }
+        NotificationStackCutoffGuideline(
+            stackScrollView = notificationStackScrollView,
+            viewModel = viewModel.notifications,
+            modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding()
+        )
     }
 }
 
@@ -529,6 +536,7 @@
                     viewModel = viewModel.notifications,
                     maxScrimTop = { 0f },
                     shouldPunchHoleBehindScrim = false,
+                    shouldReserveSpaceForNavBar = false,
                     shadeMode = ShadeMode.Split,
                     modifier =
                         Modifier.weight(1f)
@@ -538,5 +546,10 @@
                 )
             }
         }
+        NotificationStackCutoffGuideline(
+            stackScrollView = notificationStackScrollView,
+            viewModel = viewModel.notifications,
+            modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding()
+        )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
index ca4434d2..cc3fdc5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
@@ -32,6 +32,8 @@
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_DELAYED_STACK_FADE_IN
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationScrollViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
 import com.android.systemui.testKosmos
@@ -208,4 +210,50 @@
             assertThat(expandFraction).isEqualTo(1f)
             assertThat(isScrollable).isFalse()
         }
+
+    @Test
+    fun shadeExpansion_goneToQs() =
+        testScope.runTest {
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(currentScene = Scenes.Gone)
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            val expandFraction by collectLastValue(scrollViewModel.expandFraction)
+            assertThat(expandFraction).isEqualTo(0f)
+
+            fakeSceneDataSource.changeScene(toScene = Scenes.Gone)
+            val isScrollable by collectLastValue(scrollViewModel.isScrollable)
+            assertThat(isScrollable).isFalse()
+
+            fakeSceneDataSource.pause()
+
+            sceneInteractor.changeScene(Scenes.QuickSettings, "reason")
+            val transitionProgress = MutableStateFlow(0f)
+            transitionState.value =
+                ObservableTransitionState.Transition(
+                    fromScene = Scenes.Gone,
+                    toScene = Scenes.QuickSettings,
+                    currentScene = flowOf(Scenes.QuickSettings),
+                    progress = transitionProgress,
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                )
+            val steps = 10
+            repeat(steps) { repetition ->
+                val progress = (1f / steps) * (repetition + 1)
+                transitionProgress.value = progress
+                runCurrent()
+                assertThat(expandFraction)
+                    .isEqualTo(
+                        (progress / EXPANSION_FOR_MAX_SCRIM_ALPHA -
+                                EXPANSION_FOR_DELAYED_STACK_FADE_IN)
+                            .coerceIn(0f, 1f)
+                    )
+            }
+
+            fakeSceneDataSource.unpause(expectedScene = Scenes.QuickSettings)
+            assertThat(expandFraction).isEqualTo(1f)
+            assertThat(isScrollable).isFalse()
+        }
 }
diff --git a/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml b/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml
index c9850f2..cf9ca15 100644
--- a/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml
+++ b/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml
@@ -19,6 +19,8 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:sysui="http://schemas.android.com/apk/res-auto"
     android:id="@+id/alternate_bouncer"
+    android:focusable="true"
+    android:clickable="true"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
diff --git a/packages/SystemUI/res/layout/sidefps_view.xml b/packages/SystemUI/res/layout/sidefps_view.xml
index a960f74..fc4bf8a 100644
--- a/packages/SystemUI/res/layout/sidefps_view.xml
+++ b/packages/SystemUI/res/layout/sidefps_view.xml
@@ -22,4 +22,5 @@
     android:layout_height="wrap_content"
     app:lottie_autoPlay="true"
     app:lottie_loop="true"
-    app:lottie_rawRes="@raw/sfps_pulse" />
\ No newline at end of file
+    app:lottie_rawRes="@raw/sfps_pulse"
+    android:importantForAccessibility="no"/>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
index f52e457..9cc4650 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
@@ -24,6 +24,7 @@
 import android.view.LayoutInflater
 import android.view.View
 import android.view.WindowManager
+import android.view.accessibility.AccessibilityEvent
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.airbnb.lottie.LottieAnimationView
@@ -46,11 +47,11 @@
 import com.android.systemui.res.R
 import com.android.systemui.util.kotlin.sample
 import dagger.Lazy
+import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
-import javax.inject.Inject
 
 /** Binds the side fingerprint sensor indicator view to [SideFpsOverlayViewModel]. */
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -126,11 +127,6 @@
         }
 
         overlayView = layoutInflater.get().inflate(R.layout.sidefps_view, null, false)
-            .apply {
-                contentDescription = context.resources.getString(
-                        R.string.accessibility_side_fingerprint_indicator_label
-                )
-            }
 
         val overlayViewModel =
             SideFpsOverlayViewModel(
@@ -139,12 +135,10 @@
                 displayStateInteractor.get(),
                 sfpsSensorInteractor.get(),
             )
-        overlayView?.let { overlayView ->
-            bind(overlayView, overlayViewModel, windowManager.get())
-            overlayView.visibility = View.INVISIBLE
-            Log.d(TAG, "show(): adding overlayView $overlayView")
-            windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams)
-        }
+        bind(overlayView!!, overlayViewModel, windowManager.get())
+        overlayView!!.visibility = View.INVISIBLE
+        Log.d(TAG, "show(): adding overlayView $overlayView")
+        windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams)
     }
 
     /** Hide the side fingerprint sensor indicator */
@@ -185,6 +179,25 @@
 
                 overlayShowAnimator.start()
 
+                it.setAccessibilityDelegate(
+                    object : View.AccessibilityDelegate() {
+                        override fun dispatchPopulateAccessibilityEvent(
+                            host: View,
+                            event: AccessibilityEvent
+                        ): Boolean {
+                            return if (
+                                event.getEventType() ===
+                                    android.view.accessibility.AccessibilityEvent
+                                        .TYPE_WINDOW_STATE_CHANGED
+                            ) {
+                                true
+                            } else {
+                                super.dispatchPopulateAccessibilityEvent(host, event)
+                            }
+                        }
+                    }
+                )
+
                 repeatOnLifecycle(Lifecycle.State.STARTED) {
                     launch {
                         viewModel.lottieCallbacks.collect { callbacks ->
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
index 2644352..19ea007 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
@@ -81,9 +81,7 @@
                     WindowManager.LayoutParams.WRAP_CONTENT,
                     WindowManager.LayoutParams.WRAP_CONTENT,
                     WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
-                    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED or
-                            WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
-                            WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
+                    Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS,
                     PixelFormat.TRANSLUCENT
                 )
                 .apply {
@@ -93,8 +91,6 @@
                     layoutInDisplayCutoutMode =
                         WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
                     privateFlags = PRIVATE_FLAG_TRUSTED_OVERLAY or PRIVATE_FLAG_NO_MOVE_ANIMATION
-                    // Avoid announcing window title.
-                    accessibilityTitle = " "
                 }
 
     private val indicatorAsset: Flow<Int> =
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 04fa749..8706280 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
@@ -123,7 +123,7 @@
             resources.getFloat(R.dimen.shortcut_helper_screen_width_fraction)
         // maxWidth needs to be set before the sheet is drawn, otherwise the call will have no
         // effect.
-        val screenWidth = resources.displayMetrics.widthPixels
+        val screenWidth = windowManager.maximumWindowMetrics.bounds.width()
         bottomSheetBehavior.maxWidth = (sheetScreenWidthFraction * screenWidth).toInt()
     }
 
@@ -132,7 +132,7 @@
             val safeDrawingInsets = insets.safeDrawing
             // Make sure the bottom sheet is not covered by the status bar.
             bottomSheetBehavior.maxHeight =
-                resources.displayMetrics.heightPixels - safeDrawingInsets.top
+                windowManager.maximumWindowMetrics.bounds.height() - safeDrawingInsets.top
             // Make sure the contents inside of the bottom sheet are not hidden by system bars, or
             // cutouts.
             bottomSheet.updatePadding(
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 14890d7..1e79f42 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -364,6 +364,16 @@
         return factory.create("MediaCarouselCtlrLog", 20);
     }
 
+    /**
+     * Provides a buffer for media loading changes
+     */
+    @Provides
+    @SysUISingleton
+    @MediaLoadingLog
+    public static LogBuffer providesMediaLoadingLogBuffer(LogBufferFactory factory) {
+        return factory.create("MediaLoadingLog", 20);
+    }
+
     /** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */
     @Provides
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaLoadingLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaLoadingLog.kt
new file mode 100644
index 0000000..05e1b2e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaLoadingLog.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.log.dagger
+
+import com.android.systemui.log.LogBuffer
+import javax.inject.Qualifier
+
+/** A [LogBuffer] for [com.android.systemui.media.controls.domain.pipeline.MediaLoadingLogger] */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class MediaLoadingLog
diff --git a/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java b/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java
index 1b3b473..988fe64 100644
--- a/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java
@@ -43,7 +43,7 @@
 public class NotificationPlayer implements OnCompletionListener, OnErrorListener {
     private static final int PLAY = 1;
     private static final int STOP = 2;
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = true;
 
     private static final class Command {
         int code;
diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
index 3ab0420..e7c2a45 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
@@ -53,7 +53,7 @@
 @SysUISingleton
 public class RingtonePlayer implements CoreStartable {
     private static final String TAG = "RingtonePlayer";
-    private static final boolean LOGD = false;
+    private static final boolean LOGD = true;
     private final Context mContext;
 
     // TODO: support Uri switching under same IBinder
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
index 8d19ce8..f78a0f9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
@@ -24,6 +24,7 @@
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.logging.InstanceId
 import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.controls.data.repository.MediaFilterRepository
 import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME
@@ -56,6 +57,7 @@
  * This is added at the end of the pipeline since we may still need to handle callbacks from
  * background users (e.g. timeouts).
  */
+@SysUISingleton
 class MediaDataFilterImpl
 @Inject
 constructor(
@@ -68,11 +70,13 @@
     private val logger: MediaUiEventLogger,
     private val mediaFlags: MediaFlags,
     private val mediaFilterRepository: MediaFilterRepository,
+    private val mediaLoadingLogger: MediaLoadingLogger,
 ) : MediaDataManager.Listener {
     /** Non-UI listeners to media changes. */
     private val _listeners: MutableSet<MediaDataProcessor.Listener> = mutableSetOf()
     val listeners: Set<MediaDataProcessor.Listener>
         get() = _listeners.toSet()
+
     lateinit var mediaDataProcessor: MediaDataProcessor
 
     // Ensure the field (and associated reference) isn't removed during optimization.
@@ -114,6 +118,7 @@
 
         mediaFilterRepository.addSelectedUserMediaEntry(data)
 
+        mediaLoadingLogger.logMediaLoaded(data.instanceId, data.active, "loading media")
         mediaFilterRepository.addMediaDataLoadingState(
             MediaDataLoadingModel.Loaded(data.instanceId)
         )
@@ -167,7 +172,6 @@
             if (shouldReactivate) {
                 val lastActiveId = sorted.lastKey() // most recently active id
                 // Update loading state to consider this media active
-                Log.d(TAG, "reactivating $lastActiveId instead of smartspace")
                 mediaFilterRepository.setReactivatedId(lastActiveId)
                 val mediaData = sorted[lastActiveId]!!.copy(active = true)
                 logger.logRecommendationActivated(
@@ -178,6 +182,11 @@
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Loaded(lastActiveId)
                 )
+                mediaLoadingLogger.logMediaLoaded(
+                    mediaData.instanceId,
+                    mediaData.active,
+                    "reactivating media instead of smartspace"
+                )
                 listeners.forEach { listener ->
                     getKey(lastActiveId)?.let { lastActiveKey ->
                         listener.onMediaDataLoaded(
@@ -210,6 +219,7 @@
         mediaFilterRepository.setRecommendationsLoadingState(
             SmartspaceMediaLoadingModel.Loaded(key, shouldPrioritizeMutable)
         )
+        mediaLoadingLogger.logRecommendationLoaded(key, data.isActive, "loading recommendations")
         listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
     }
 
@@ -220,6 +230,7 @@
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Removed(instanceId)
                 )
+                mediaLoadingLogger.logMediaRemoved(instanceId, "removing media card")
                 // Only notify listeners if something actually changed
                 listeners.forEach { it.onMediaDataRemoved(key, userInitiated) }
             }
@@ -230,12 +241,16 @@
         // First check if we had reactivated media instead of forwarding smartspace
         mediaFilterRepository.reactivatedId.value?.let { lastActiveId ->
             mediaFilterRepository.setReactivatedId(null)
-            Log.d(TAG, "expiring reactivated key $lastActiveId")
             // Update loading state with actual active value
             mediaFilterRepository.selectedUserEntries.value[lastActiveId]?.let {
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Loaded(lastActiveId, immediately)
                 )
+                mediaLoadingLogger.logMediaLoaded(
+                    lastActiveId,
+                    it.active,
+                    "expiring reactivated id"
+                )
                 listeners.forEach { listener ->
                     getKey(lastActiveId)?.let { lastActiveKey ->
                         listener.onMediaDataLoaded(lastActiveKey, lastActiveKey, it, immediately)
@@ -256,6 +271,11 @@
         mediaFilterRepository.setRecommendationsLoadingState(
             SmartspaceMediaLoadingModel.Removed(key, immediately)
         )
+        mediaLoadingLogger.logRecommendationRemoved(
+            key,
+            immediately,
+            "removing recommendations card"
+        )
         listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
     }
 
@@ -265,11 +285,14 @@
         mediaFilterRepository.allUserEntries.value.forEach { (key, data) ->
             if (!lockscreenUserManager.isProfileAvailable(data.userId)) {
                 // Only remove media when the profile is unavailable.
-                if (DEBUG) Log.d(TAG, "Removing $key after profile change")
                 mediaFilterRepository.removeSelectedUserMediaEntry(data.instanceId, data)
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Removed(data.instanceId)
                 )
+                mediaLoadingLogger.logMediaRemoved(
+                    data.instanceId,
+                    "Removing $key after profile change"
+                )
                 listeners.forEach { listener -> listener.onMediaDataRemoved(key, false) }
             }
         }
@@ -283,10 +306,10 @@
         // Clear the list first and update loading state to remove media from UI.
         mediaFilterRepository.clearSelectedUserMedia()
         keyCopy.forEach { instanceId ->
-            if (DEBUG) Log.d(TAG, "Removing $instanceId after user change")
             mediaFilterRepository.addMediaDataLoadingState(
                 MediaDataLoadingModel.Removed(instanceId)
             )
+            mediaLoadingLogger.logMediaRemoved(instanceId, "Removing media after user change")
             getKey(instanceId)?.let {
                 listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it, false) }
             }
@@ -294,15 +317,15 @@
 
         mediaFilterRepository.allUserEntries.value.forEach { (key, data) ->
             if (lockscreenUserManager.isCurrentProfile(data.userId)) {
-                if (DEBUG)
-                    Log.d(
-                        TAG,
-                        "Re-adding $key with instanceId=${data.instanceId} after user change"
-                    )
                 mediaFilterRepository.addSelectedUserMediaEntry(data)
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Loaded(data.instanceId)
                 )
+                mediaLoadingLogger.logMediaLoaded(
+                    data.instanceId,
+                    data.active,
+                    "Re-adding $key after user change"
+                )
                 listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaLoadingLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaLoadingLogger.kt
new file mode 100644
index 0000000..c6cfd65
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaLoadingLogger.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.systemui.media.controls.domain.pipeline
+
+import com.android.internal.logging.InstanceId
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.MediaLoadingLog
+import javax.inject.Inject
+
+/** A buffered log for media loading events. */
+@SysUISingleton
+class MediaLoadingLogger @Inject constructor(@MediaLoadingLog private val buffer: LogBuffer) {
+
+    fun logMediaLoaded(instanceId: InstanceId, active: Boolean, reason: String) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = instanceId.toString()
+                bool1 = active
+                str2 = reason
+            },
+            { "add media $str1, active: $bool1, reason: $str2" }
+        )
+    }
+
+    fun logMediaRemoved(instanceId: InstanceId, reason: String) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = instanceId.toString()
+                str2 = reason
+            },
+            { "removing media $str1, reason: $str2" }
+        )
+    }
+
+    fun logRecommendationLoaded(key: String, isActive: Boolean, reason: String) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = key
+                bool1 = isActive
+                str2 = reason
+            },
+            { "add recommendation $str1, active $bool1, reason: $str2" }
+        )
+    }
+
+    fun logRecommendationRemoved(key: String, immediately: Boolean, reason: String) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = key
+                bool1 = immediately
+                str2 = reason
+            },
+            { "removing recommendation $str1, immediate=$bool1, reason: $str2" }
+        )
+    }
+
+    companion object {
+        private const val TAG = "MediaLoadingLog"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt
index 315a9fb..f0d8df5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt
@@ -155,13 +155,17 @@
                             mediaFlags.isPersistentSsCardEnabled(),
                     recsViewModel = recommendationsViewModel,
                     onAdded = { commonViewModel ->
-                        onMediaRecommendationAddedOrUpdated(commonViewModel)
+                        onMediaRecommendationAddedOrUpdated(
+                            commonViewModel as MediaCommonViewModel.MediaRecommendations
+                        )
                     },
                     onRemoved = { immediatelyRemove ->
                         onMediaRecommendationRemoved(commonModel, immediatelyRemove)
                     },
                     onUpdated = { commonViewModel ->
-                        onMediaRecommendationAddedOrUpdated(commonViewModel)
+                        onMediaRecommendationAddedOrUpdated(
+                            commonViewModel as MediaCommonViewModel.MediaRecommendations
+                        )
                     },
                 )
                 .also { mediaRecs = it }
@@ -185,7 +189,9 @@
         }
     }
 
-    private fun onMediaRecommendationAddedOrUpdated(commonViewModel: MediaCommonViewModel) {
+    private fun onMediaRecommendationAddedOrUpdated(
+        commonViewModel: MediaCommonViewModel.MediaRecommendations
+    ) {
         if (!interactor.isRecommendationActive()) {
             if (!mediaFlags.isPersistentSsCardEnabled()) {
                 commonViewModel.onRemoved(true)
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 99c95b5..c9be993 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -45,6 +45,7 @@
 import android.graphics.Region;
 import android.hardware.input.InputManager;
 import android.icu.text.SimpleDateFormat;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.SystemProperties;
@@ -410,6 +411,7 @@
             PluginManager pluginManager,
             @BackPanelUiThread UiThreadContext uiThreadContext,
             @Background Executor backgroundExecutor,
+            @Background Handler bgHandler,
             UserTracker userTracker,
             NavigationModeController navigationModeController,
             BackPanelController.Factory backPanelControllerFactory,
@@ -473,7 +475,8 @@
                 ViewConfiguration.getLongPressTimeout());
 
         mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver(
-                mUiThreadContext.getHandler(), mContext, this::onNavigationSettingsChanged);
+                mUiThreadContext.getHandler(), bgHandler, mContext,
+                this::onNavigationSettingsChanged);
 
         updateCurrentUserResources();
     }
@@ -1316,6 +1319,7 @@
         private final PluginManager mPluginManager;
         private final UiThreadContext mUiThreadContext;
         private final Executor mBackgroundExecutor;
+        private final Handler mBgHandler;
         private final UserTracker mUserTracker;
         private final NavigationModeController mNavigationModeController;
         private final BackPanelController.Factory mBackPanelControllerFactory;
@@ -1336,6 +1340,7 @@
                         PluginManager pluginManager,
                         @BackPanelUiThread UiThreadContext uiThreadContext,
                         @Background Executor backgroundExecutor,
+                        @Background Handler bgHandler,
                         UserTracker userTracker,
                         NavigationModeController navigationModeController,
                         BackPanelController.Factory backPanelControllerFactory,
@@ -1354,6 +1359,7 @@
             mPluginManager = pluginManager;
             mUiThreadContext = uiThreadContext;
             mBackgroundExecutor = backgroundExecutor;
+            mBgHandler = bgHandler;
             mUserTracker = userTracker;
             mNavigationModeController = navigationModeController;
             mBackPanelControllerFactory = backPanelControllerFactory;
@@ -1378,6 +1384,7 @@
                             mPluginManager,
                             mUiThreadContext,
                             mBackgroundExecutor,
+                            mBgHandler,
                             mUserTracker,
                             mNavigationModeController,
                             mBackPanelControllerFactory,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt
index be39d2e..4bf6c96 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt
@@ -18,6 +18,7 @@
 
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.ComponentInfoFlags
+import android.content.pm.PackageManager.MATCH_ANY_USER
 import android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS
 import android.view.Display
 import android.view.IWindowManager
@@ -47,7 +48,8 @@
         // Convert component names to app names.
         return components.map {
             packageManager
-                .getActivityInfo(it, ComponentInfoFlags.of(MATCH_DISABLED_COMPONENTS.toLong()))
+                .getActivityInfo(it, ComponentInfoFlags.of(
+                    (MATCH_DISABLED_COMPONENTS or MATCH_ANY_USER).toLong()))
                 .loadLabel(packageManager)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index 40d709d..3c3797b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -7,6 +7,7 @@
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
 import com.android.internal.logging.UiEventLogger
 import com.android.internal.util.ScreenshotRequest
+import com.android.systemui.Flags.screenshotShelfUi2
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.display.data.repository.DisplayRepository
@@ -139,8 +140,8 @@
 
     private suspend fun getDisplaysToScreenshot(requestType: Int): List<Display> {
         val allDisplays = displays.first()
-        return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE) {
-            // If this is a provided image, let's show the UI on the default display only.
+        return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE || screenshotShelfUi2()) {
+            // If this is a provided image or using the shelf UI, just screenshot th default display
             allDisplays.filter { it.displayId == Display.DEFAULT_DISPLAY }
         } else {
             allDisplays.filter { it.type in ALLOWED_DISPLAY_TYPES }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 262befc..8b78f54 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -25,7 +25,6 @@
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
 import static com.android.systemui.Flags.predictiveBackAnimateShade;
-import static com.android.systemui.Flags.shadeCollapseActivityLaunchFix;
 import static com.android.systemui.Flags.smartspaceRelocateToBottom;
 import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
 import static com.android.systemui.classifier.Classifier.GENERIC;
@@ -4118,11 +4117,7 @@
 
     @Override
     public boolean canBeCollapsed() {
-        return !isFullyCollapsed() && !isTracking() && !isClosing()
-                // Don't try to collapse if on keyguard, as the expansion fraction is 1 in this
-                // case.
-                && !(shadeCollapseActivityLaunchFix() && mExpandedFraction == 1f
-                && mBarState == KEYGUARD);
+        return !isFullyCollapsed() && !isTracking() && !isClosing();
     }
 
     public void instantCollapse() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 337ffa4..95cabfb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -1630,6 +1630,7 @@
                         "skip showing FACE_ERROR_TIMEOUT due to co-ex logic");
             }
         } else if (deferredFaceMessage != null) {
+            mBouncerMessageInteractor.setFaceAcquisitionMessage(deferredFaceMessage.toString());
             // Face-only: The face timeout message is not very actionable, let's ask the
             // user to manually retry.
             showBiometricMessage(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 71a0b94..60a26dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -837,8 +837,8 @@
             y = (int) mScrollViewFields.getStackTop();
             drawDebugInfo(canvas, y, Color.RED, /* label= */ "getStackTop() = " + y);
 
-            y = (int) mScrollViewFields.getStackBottom();
-            drawDebugInfo(canvas, y, Color.MAGENTA, /* label= */ "getStackBottom() = " + y);
+            y = (int) mScrollViewFields.getStackCutoff();
+            drawDebugInfo(canvas, y, Color.MAGENTA, /* label= */ "getStackCutoff() = " + y);
 
             y = (int) mScrollViewFields.getHeadsUpTop();
             drawDebugInfo(canvas, y, Color.GREEN, /* label= */ "getHeadsUpTop() = " + y);
@@ -1220,8 +1220,8 @@
     }
 
     @Override
-    public void setStackBottom(float stackBottom) {
-        mScrollViewFields.setStackBottom(stackBottom);
+    public void setStackCutoff(float stackCutoff) {
+        mScrollViewFields.setStackCutoff(stackCutoff);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
index 6afcf37..a693dd7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
@@ -38,7 +38,7 @@
      * Y coordinate in view pixels above which the bottom of the notification stack / shelf / footer
      * must be.
      */
-    var stackBottom: Float = 0f
+    var stackCutoff: Float = 0f
     /** Y coordinate in view pixels of the top of the HUN */
     var headsUpTop: Float = 0f
     /** Whether the notifications are scrolled all the way to the top (i.e. when freshly opened) */
@@ -80,7 +80,7 @@
         pw.printSection("StackViewStates") {
             pw.println("scrimClippingShape", scrimClippingShape)
             pw.println("stackTop", stackTop)
-            pw.println("stackBottom", stackBottom)
+            pw.println("stackCutoff", stackCutoff)
             pw.println("headsUpTop", headsUpTop)
             pw.println("isScrolledToTop", isScrolledToTop)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index eaaa9a1..8d1a096 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -50,8 +50,11 @@
     /** set the y position in px of the top of the stack in this view's coordinates */
     fun setStackTop(stackTop: Float)
 
-    /** set the y position in px of the bottom of the stack in this view's coordinates */
-    fun setStackBottom(stackBottom: Float)
+    /**
+     * set the bottom-most acceptable y-position of the bottom of the notification stack/ shelf /
+     * footer.
+     */
+    fun setStackCutoff(stackBottom: Float)
 
     /** set the y position in px of the top of the HUN in this view's coordinates */
     fun setHeadsUpTop(headsUpTop: Float)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index a21db12..ebb0d7d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -90,8 +90,10 @@
                             1f
                         } else if (
                             shadeMode != ShadeMode.Split &&
-                                transitionState.fromScene in SceneFamilies.Home &&
-                                transitionState.toScene in quickSettingsScene
+                                (transitionState.fromScene in SceneFamilies.Home &&
+                                    transitionState.toScene == quickSettingsScene) ||
+                                (transitionState.fromScene == quickSettingsScene &&
+                                    transitionState.toScene in SceneFamilies.Home)
                         ) {
                             // during QS expansion, increase fraction at same rate as scrim alpha,
                             // but start when scrim alpha is at EXPANSION_FOR_DELAYED_STACK_FADE_IN.
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 1fc2821..77d8f50 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
@@ -497,6 +497,8 @@
                 occludedToLockscreenTransitionViewModel.lockscreenAlpha,
                 primaryBouncerToGoneTransitionViewModel.notificationAlpha,
                 primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha,
+                glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
+                lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
             )
 
         return merge(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarTransitionsController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarTransitionsController.java
index 2235035..d2a1c44 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarTransitionsController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarTransitionsController.java
@@ -29,6 +29,7 @@
 import com.android.app.animation.Interpolators;
 import com.android.internal.policy.GestureNavigationSettingsObserver;
 import com.android.systemui.Dumpable;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.CommandQueue;
@@ -124,6 +125,7 @@
     @AssistedInject
     public LightBarTransitionsController(
             Context context,
+            @Background Handler bgHandler,
             @Assisted DarkIntensityApplier applier,
             CommandQueue commandQueue,
             KeyguardStateController keyguardStateController,
@@ -140,7 +142,7 @@
         mContext = context;
         mDisplayId = mContext.getDisplayId();
         mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver(
-                mHandler, mContext, this::onNavigationSettingsChanged);
+                mHandler, bgHandler, mContext, this::onNavigationSettingsChanged);
         mGestureNavigationSettingsObserver.register();
         onNavigationSettingsChanged();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
index c089092..ca94363 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
@@ -31,6 +31,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.BluetoothUtils;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
@@ -71,6 +72,7 @@
     private final LocalBluetoothManager mLocalBluetoothManager;
     private final UserManager mUserManager;
     private final int mCurrentUser;
+    private final Context mContext;
     @GuardedBy("mConnectedDevices")
     private final List<CachedBluetoothDevice> mConnectedDevices = new ArrayList<>();
 
@@ -99,6 +101,7 @@
             @Main Looper mainLooper,
             @Nullable LocalBluetoothManager localBluetoothManager,
             @Nullable BluetoothAdapter bluetoothAdapter) {
+        mContext = context;
         mDumpManager = dumpManager;
         mLogger = logger;
         mBluetoothRepository = bluetoothRepository;
@@ -262,9 +265,21 @@
     }
 
     private Collection<CachedBluetoothDevice> getDevices() {
-        return mLocalBluetoothManager != null
-                ? mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy()
-                : Collections.emptyList();
+        Collection<CachedBluetoothDevice> devices =
+                mLocalBluetoothManager != null
+                        ? mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy()
+                        : Collections.emptyList();
+        if (com.android.settingslib.flags.Flags.enableHideExclusivelyManagedBluetoothDevice()) {
+            // When the device is exclusively managed by its owner app it needs to be hidden.
+            devices =
+                    devices.stream()
+                            .filter(
+                                    device ->
+                                            !BluetoothUtils.isExclusivelyManagedBluetoothDevice(
+                                                    mContext, device.getDevice()))
+                            .toList();
+        }
+        return devices;
     }
 
     private void updateConnected() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
index 6f70622..4238254 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
@@ -126,7 +126,6 @@
         `when`(layoutInflater.inflate(R.layout.sidefps_view, null, false)).thenReturn(sideFpsView)
         `when`(sideFpsView.requireViewById<LottieAnimationView>(eq(R.id.sidefps_animation)))
             .thenReturn(mock(LottieAnimationView::class.java))
-        `when`(sideFpsView.context).thenReturn(mContext)
         with(mock(ViewPropertyAnimator::class.java)) {
             `when`(sideFpsView.animate()).thenReturn(this)
             `when`(alpha(Mockito.anyFloat())).thenReturn(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
index 064cf09..4da56b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
@@ -53,8 +53,10 @@
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
 import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.any
@@ -100,6 +102,7 @@
     private lateinit var dataPrivateProfile: MediaData
     private val clock = FakeSystemClock()
     private val repository: MediaFilterRepository = kosmos.mediaFilterRepository
+    private val mediaLoadingLogger = kosmos.mockMediaLoadingLogger
 
     @Before
     fun setup() {
@@ -118,6 +121,7 @@
                 logger,
                 mediaFlags,
                 repository,
+                mediaLoadingLogger,
             )
         mediaDataFilter.mediaDataProcessor = mediaDataProcessor
         mediaDataFilter.addListener(listener)
@@ -176,6 +180,8 @@
 
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataMain.instanceId), eq(dataMain.active), anyString())
             assertThat(currentMedia).containsExactly(mediaCommonModel)
         }
 
@@ -190,6 +196,7 @@
 
             verify(listener, never())
                 .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+            verify(mediaLoadingLogger, never()).logMediaLoaded(any(), anyBoolean(), anyString())
             assertThat(currentMedia).doesNotContain(mediaCommonModel)
         }
 
@@ -203,11 +210,14 @@
             // GIVEN a media was removed for main user
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
 
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataMain.instanceId), eq(dataMain.active), anyString())
             assertThat(currentMedia).containsExactly(mediaCommonModel)
 
             mediaDataFilter.onMediaDataRemoved(KEY, false)
 
             verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
+            verify(mediaLoadingLogger).logMediaRemoved(eq(dataMain.instanceId), anyString())
             assertThat(currentMedia).doesNotContain(mediaCommonModel)
         }
 
@@ -221,6 +231,8 @@
             mediaDataFilter.onMediaDataRemoved(KEY, false)
 
             verify(listener, never()).onMediaDataRemoved(eq(KEY), eq(false))
+            verify(mediaLoadingLogger, never())
+                .logMediaRemoved(eq(dataGuest.instanceId), anyString())
             assertThat(currentMedia).isEmpty()
         }
 
@@ -233,6 +245,8 @@
             // GIVEN that we have a media loaded for main user
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
 
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataMain.instanceId), eq(dataMain.active), anyString())
             assertThat(currentMedia).containsExactly(MediaCommonModel.MediaControl(mediaLoaded))
 
             // and we switch to guest user
@@ -240,6 +254,7 @@
 
             // THEN we should remove the main user's media
             verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
+            verify(mediaLoadingLogger).logMediaRemoved(eq(dataMain.instanceId), anyString())
             assertThat(currentMedia).isEmpty()
         }
 
@@ -260,6 +275,10 @@
             // THEN we should add back the guest user media
             verify(listener)
                 .onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataGuest.instanceId), eq(dataGuest.active), anyString())
+
+            reset(mediaLoadingLogger)
 
             // but not the main user's
             verify(listener, never())
@@ -271,6 +290,8 @@
                     anyInt(),
                     anyBoolean()
                 )
+            verify(mediaLoadingLogger, never())
+                .logMediaLoaded(eq(dataMain.instanceId), anyBoolean(), anyString())
             assertThat(currentMedia)
                 .containsExactly(MediaCommonModel.MediaControl(guestLoadedStatesModel))
             assertThat(currentMedia)
@@ -292,6 +313,7 @@
             val mediaLoadedStatesModel = MediaDataLoadingModel.Loaded(dataMain.instanceId)
             // THEN we should remove the private profile media
             verify(listener).onMediaDataRemoved(eq(KEY_ALT), eq(false))
+            verify(mediaLoadingLogger).logMediaRemoved(eq(dataGuest.instanceId), anyString())
             assertThat(currentMedia)
                 .containsExactly(MediaCommonModel.MediaControl(mediaLoadedStatesModel))
         }
@@ -541,6 +563,8 @@
             assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
             verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -570,6 +594,9 @@
             verify(listener, never())
                 .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
             verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+            verify(mediaLoadingLogger, never()).logMediaLoaded(any(), anyBoolean(), anyString())
+            verify(mediaLoadingLogger, never())
+                .logRecommendationLoaded(any(), anyBoolean(), anyString())
             verify(logger, never()).logRecommendationAdded(any(), any())
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -607,6 +634,8 @@
             assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
             verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -641,6 +670,8 @@
                 .isFalse()
             assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
             verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+            verify(mediaLoadingLogger, never())
+                .logRecommendationLoaded(any(), anyBoolean(), anyString())
             verify(logger, never()).logRecommendationAdded(any(), any())
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -668,6 +699,10 @@
             assertThat(currentMedia).containsExactly(controlCommonModel)
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
+
+            reset(mediaLoadingLogger)
 
             // AND we get a smartspace signal
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
@@ -686,6 +721,10 @@
             verify(listener, never())
                 .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean())
             verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+            verify(mediaLoadingLogger, never())
+                .logMediaLoaded(eq(dataCurrent.instanceId), anyBoolean(), anyString())
+            verify(mediaLoadingLogger, never())
+                .logRecommendationLoaded(any(), anyBoolean(), anyString())
             verify(logger, never()).logRecommendationAdded(any(), any())
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -711,6 +750,8 @@
             assertThat(currentMedia).containsExactly(controlCommonModel)
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
 
             // AND we get a smartspace signal
             runCurrent()
@@ -736,8 +777,16 @@
                     eq(100),
                     eq(true)
                 )
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(
+                    eq(dataCurrentAndActive.instanceId),
+                    eq(dataCurrentAndActive.active),
+                    anyString()
+                )
             // Smartspace update shouldn't be propagated for the empty rec list.
             verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+            verify(mediaLoadingLogger, never())
+                .logRecommendationLoaded(any(), anyBoolean(), anyString())
             verify(logger, never()).logRecommendationAdded(any(), any())
             verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
         }
@@ -767,6 +816,8 @@
             assertThat(currentMedia).containsExactly(controlCommonModel)
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
 
             // AND we get a smartspace signal
             runCurrent()
@@ -783,6 +834,12 @@
                     eq(100),
                     eq(true)
                 )
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(
+                    eq(dataCurrentAndActive.instanceId),
+                    eq(dataCurrentAndActive.active),
+                    anyString()
+                )
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -795,6 +852,8 @@
             assertThat(currentMedia).containsExactly(controlCommonModel, recsCommonModel)
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
             verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
             verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
         }
@@ -811,6 +870,8 @@
             mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
 
             verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+            verify(mediaLoadingLogger)
+                .logRecommendationRemoved(eq(SMARTSPACE_KEY), eq(true), anyString())
             assertThat(currentMedia).isEmpty()
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -842,6 +903,8 @@
             assertThat(currentMedia).containsExactly(controlCommonModel)
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
 
             runCurrent()
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
@@ -856,10 +919,18 @@
                     eq(100),
                     eq(true)
                 )
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(
+                    eq(dataCurrentAndActive.instanceId),
+                    eq(dataCurrentAndActive.active),
+                    anyString()
+                )
 
             mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
 
             verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+            verify(mediaLoadingLogger)
+                .logRecommendationRemoved(eq(SMARTSPACE_KEY), eq(true), anyString())
             assertThat(currentMedia).containsExactly(controlCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -890,6 +961,8 @@
 
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(false), anyString())
             assertThat(currentMedia).containsExactly(recsCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -930,16 +1003,23 @@
 
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
             assertThat(currentMedia).containsExactly(controlCommonModel)
 
+            reset(mediaLoadingLogger)
+
             // And an inactive recommendation is loaded
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
             // Smartspace is loaded but the media stays inactive
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(false), anyString())
             verify(listener, never())
                 .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+            verify(mediaLoadingLogger, never()).logMediaLoaded(any(), anyBoolean(), anyString())
             assertThat(currentMedia).containsExactly(controlCommonModel, recsCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -995,6 +1075,8 @@
 
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
             assertThat(currentMedia).containsExactly(controlCommonModel)
 
             // AND we get a smartspace signal with extra to trigger resume
@@ -1014,6 +1096,12 @@
                     eq(100),
                     eq(true)
                 )
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(
+                    eq(dataCurrentAndActive.instanceId),
+                    eq(dataCurrentAndActive.active),
+                    anyString()
+                )
             assertThat(currentMedia).containsExactly(controlCommonModel, recsCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -1026,6 +1114,8 @@
             // And update the smartspace data state, but not prioritized
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
         }
 
     @Test
@@ -1049,8 +1139,12 @@
 
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
             assertThat(currentMedia).containsExactly(controlCommonModel)
 
+            reset(mediaLoadingLogger)
+
             // AND we get a smartspace signal with extra to not trigger resume
             val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, false) }
             whenever(cardAction.extras).thenReturn(extras)
@@ -1059,9 +1153,13 @@
             // THEN listeners are not updated to show media
             verify(listener, never())
                 .onMediaDataLoaded(eq(KEY), eq(KEY), any(), eq(true), eq(100), eq(true))
+            verify(mediaLoadingLogger, never())
+                .logMediaLoaded(eq(dataCurrent.instanceId), anyBoolean(), anyString())
             // But the smartspace update is still propagated
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
             assertThat(currentMedia).containsExactly(controlCommonModel, recsCommonModel)
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
index 16091b2..1538c72 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
@@ -19,6 +19,7 @@
 import android.content.ComponentName
 import android.content.pm.ActivityInfo
 import android.content.pm.PackageManager
+import android.content.pm.PackageManager.MATCH_ANY_USER
 import android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS
 import android.testing.AndroidTestingRunner
 import android.view.Display
@@ -169,12 +170,12 @@
 
     private class ComponentInfoFlagMatcher(
         @PackageManager.ComponentInfoFlagsBits val mask: Int, val value: Int
-    ): ArgumentMatcher<PackageManager.ComponentInfoFlags> {
+    ) : ArgumentMatcher<PackageManager.ComponentInfoFlags> {
         override fun matches(flags: PackageManager.ComponentInfoFlags?): Boolean {
             return flags != null && (mask.toLong() and flags.value) == value.toLong()
         }
 
-        override fun toString(): String{
+        override fun toString(): String {
             return "mask 0x%08x == 0x%08x".format(mask, value)
         }
     }
@@ -191,16 +192,16 @@
         whenever(
             packageManager.getActivityInfo(
                 eq(component),
-                argThat(includesFlagBits(MATCH_DISABLED_COMPONENTS))
+                argThat(includesFlagBits(MATCH_DISABLED_COMPONENTS or MATCH_ANY_USER))
             )
-        ).thenReturn(activityInfo);
+        ).thenReturn(activityInfo)
 
         whenever(
             packageManager.getActivityInfo(
                 eq(component),
                 argThat(excludesFlagBits(MATCH_DISABLED_COMPONENTS))
             )
-        ).thenThrow(PackageManager.NameNotFoundException::class.java);
+        ).thenThrow(PackageManager.NameNotFoundException::class.java)
 
         whenever(windowManager.notifyScreenshotListeners(eq(Display.DEFAULT_DISPLAY)))
             .thenReturn(listOf(component))
@@ -212,5 +213,4 @@
         assertEquals(1, list.size)
         assertEquals(appName, list[0])
     }
-
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
index bf7d909..dd6ba90 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -3,6 +3,8 @@
 import android.content.ComponentName
 import android.graphics.Bitmap
 import android.net.Uri
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.testing.AndroidTestingRunner
 import android.view.Display
 import android.view.Display.TYPE_EXTERNAL
@@ -15,6 +17,7 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.internal.util.ScreenshotRequest
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.display.data.repository.FakeDisplayRepository
 import com.android.systemui.display.data.repository.display
@@ -77,6 +80,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_severalDisplays_callsControllerForEachOne() =
         testScope.runTest {
             val internalDisplay = display(TYPE_INTERNAL, id = 0)
@@ -108,6 +112,32 @@
         }
 
     @Test
+    @EnableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
+    fun executeScreenshots_severalDisplaysShelfUi_justCallsOne() =
+        testScope.runTest {
+            val internalDisplay = display(TYPE_INTERNAL, id = 0)
+            val externalDisplay = display(TYPE_EXTERNAL, id = 1)
+            setDisplays(internalDisplay, externalDisplay)
+            val onSaved = { _: Uri? -> }
+            screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
+
+            verify(controllerFactory).create(eq(internalDisplay), any())
+
+            val capturer = ArgumentCaptor<ScreenshotData>()
+
+            verify(controller0).handleScreenshot(capturer.capture(), any(), any())
+            assertThat(capturer.value.displayId).isEqualTo(0)
+
+            assertThat(eventLogger.numLogs()).isEqualTo(1)
+            assertThat(eventLogger.get(0).eventId)
+                .isEqualTo(ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER.id)
+            assertThat(eventLogger.get(0).packageName).isEqualTo(topComponent.packageName)
+
+            screenshotExecutor.onDestroy()
+        }
+
+    @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_providedImageType_callsOnlyDefaultDisplayController() =
         testScope.runTest {
             val internalDisplay = display(TYPE_INTERNAL, id = 0)
@@ -139,6 +169,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_onlyVirtualDisplays_noInteractionsWithControllers() =
         testScope.runTest {
             setDisplays(display(TYPE_VIRTUAL, id = 0), display(TYPE_VIRTUAL, id = 1))
@@ -150,6 +181,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_allowedTypes_allCaptured() =
         testScope.runTest {
             whenever(controllerFactory.create(any(), any())).thenReturn(controller0)
@@ -168,6 +200,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_reportsOnFinishedOnlyWhenBothFinished() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -193,6 +226,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_oneFinishesOtherFails_reportFailsOnlyAtTheEnd() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -220,6 +254,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_allDisplaysFail_reportsFail() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -247,6 +282,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun onDestroy_propagatedToControllers() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -259,6 +295,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun removeWindows_propagatedToControllers() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -273,6 +310,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun onCloseSystemDialogsReceived_propagatedToControllers() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -287,6 +325,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun onCloseSystemDialogsReceived_someControllerHavePendingTransitions() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -303,6 +342,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_controllerCalledWithRequestProcessorReturnValue() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0))
@@ -324,6 +364,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromProcessor_logsScreenshotRequested() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -341,6 +382,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromProcessor_logsUiError() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -358,6 +400,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromProcessorOnDefaultDisplay_showsErrorNotification() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -384,6 +427,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromScreenshotController_reportsRequested() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -404,6 +448,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromScreenshotController_reportsError() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -424,6 +469,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromScreenshotController_showsErrorNotification() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 6536405..13d44de 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -18,7 +18,6 @@
 
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
-import static com.android.systemui.Flags.FLAG_SHADE_COLLAPSE_ACTIVITY_LAUNCH_FIX;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPENING;
@@ -48,7 +47,6 @@
 import android.graphics.Point;
 import android.os.PowerManager;
 import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.MotionEvent;
@@ -679,32 +677,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_SHADE_COLLAPSE_ACTIVITY_LAUNCH_FIX)
-    public void testCanBeCollapsed_expandedInKeyguard() {
-        mStatusBarStateController.setState(KEYGUARD);
-        mNotificationPanelViewController.setExpandedFraction(1f);
-
-        assertThat(mNotificationPanelViewController.canBeCollapsed()).isFalse();
-    }
-
-    @Test
-    @EnableFlags(FLAG_SHADE_COLLAPSE_ACTIVITY_LAUNCH_FIX)
-    public void testCanBeCollapsed_expandedInShade() {
-        mStatusBarStateController.setState(SHADE);
-        mNotificationPanelViewController.setExpandedFraction(1f);
-        assertThat(mNotificationPanelViewController.canBeCollapsed()).isTrue();
-    }
-
-    @Test
-    @DisableFlags(FLAG_SHADE_COLLAPSE_ACTIVITY_LAUNCH_FIX)
-    public void testCanBeCollapsed_expandedInKeyguard_flagDisabled() {
-        mStatusBarStateController.setState(KEYGUARD);
-        mNotificationPanelViewController.setExpandedFraction(1f);
-
-        assertThat(mNotificationPanelViewController.canBeCollapsed()).isTrue();
-    }
-
-    @Test
     @Ignore("b/341163515 - fails to clean up animators correctly")
     public void testSwipeWhileLocked_notifiesKeyguardState() {
         mStatusBarStateController.setState(KEYGUARD);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
index 43c19b8..7dfdb92 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.os.Handler;
 import android.testing.TestableLooper;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -58,14 +59,16 @@
     private KeyguardStateController mKeyguardStateController;
     @Mock
     private StatusBarStateController mStatusBarStateController;
+    @Mock
+    private Handler mBgHandler;
 
     private LightBarTransitionsController mLightBarTransitionsController;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mLightBarTransitionsController = new LightBarTransitionsController(mContext, mApplier,
-                new CommandQueue(mContext, new FakeDisplayTracker(mContext)),
+        mLightBarTransitionsController = new LightBarTransitionsController(mContext,
+                mBgHandler, mApplier, new CommandQueue(mContext, new FakeDisplayTracker(mContext)),
                 mKeyguardStateController, mStatusBarStateController);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java
index a1da167..9ab64d65 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java
@@ -14,6 +14,10 @@
 
 package com.android.systemui.statusbar.policy;
 
+import static android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf;
+
+import static com.android.settingslib.flags.Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -21,6 +25,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
@@ -30,7 +35,11 @@
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
-import android.testing.AndroidTestingRunner;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
 
@@ -55,16 +64,31 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
 
-@RunWith(AndroidTestingRunner.class)
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+@RunWith(ParameterizedAndroidJunit4.class)
 @RunWithLooper
 @SmallTest
 public class BluetoothControllerImplTest extends SysuiTestCase {
 
+    @Parameters(name = "{0}")
+    public static List<FlagsParameterization> getParams() {
+        return allCombinationsOf(FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE);
+    }
+
+    private static final String TEST_EXCLUSIVE_MANAGER = "com.test.manager";
+
+    @Mock
+    private PackageManager mPackageManager;
+
     private UserTracker mUserTracker;
     private LocalBluetoothManager mMockBluetoothManager;
     private CachedBluetoothDeviceManager mMockDeviceManager;
@@ -77,14 +101,21 @@
 
     private FakeExecutor mBackgroundExecutor;
 
+    public BluetoothControllerImplTest(FlagsParameterization flags) {
+        super();
+        mSetFlagsRule.setFlagsParameterization(flags);
+    }
+
     @Before
     public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
         mTestableLooper = TestableLooper.get(this);
         mMockBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager.class);
         mDevices = new ArrayList<>();
         mUserTracker = mock(UserTracker.class);
         mMockDeviceManager = mock(CachedBluetoothDeviceManager.class);
         mMockAdapter = mock(BluetoothAdapter.class);
+        mContext.setMockPackageManager(mPackageManager);
         when(mMockDeviceManager.getCachedDevicesCopy()).thenReturn(mDevices);
         when(mMockBluetoothManager.getCachedDeviceManager()).thenReturn(mMockDeviceManager);
         mMockLocalAdapter = mock(LocalBluetoothAdapter.class);
@@ -114,6 +145,7 @@
         CachedBluetoothDevice device = mock(CachedBluetoothDevice.class);
         when(device.isConnected()).thenReturn(true);
         when(device.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_CONNECTED);
+        when(device.getDevice()).thenReturn(mock(BluetoothDevice.class));
 
         mDevices.add(device);
         when(mMockLocalAdapter.getConnectionState())
@@ -139,10 +171,12 @@
     public void getConnectedDevices_onlyReturnsConnected() {
         CachedBluetoothDevice device1Disconnected = mock(CachedBluetoothDevice.class);
         when(device1Disconnected.isConnected()).thenReturn(false);
+        when(device1Disconnected.getDevice()).thenReturn(mock(BluetoothDevice.class));
         mDevices.add(device1Disconnected);
 
         CachedBluetoothDevice device2Connected = mock(CachedBluetoothDevice.class);
         when(device2Connected.isConnected()).thenReturn(true);
+        when(device2Connected.getDevice()).thenReturn(mock(BluetoothDevice.class));
         mDevices.add(device2Connected);
 
         mBluetoothControllerImpl.onDeviceAdded(device1Disconnected);
@@ -154,6 +188,46 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
+    public void getConnectedDevice_exclusivelyManagedDevice_doNotReturn()
+            throws PackageManager.NameNotFoundException {
+        CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class);
+        when(cachedDevice.isConnected()).thenReturn(true);
+        BluetoothDevice device = mock(BluetoothDevice.class);
+        when(device.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+                TEST_EXCLUSIVE_MANAGER.getBytes());
+        when(cachedDevice.getDevice()).thenReturn(device);
+        doReturn(new ApplicationInfo()).when(mPackageManager).getApplicationInfo(
+                TEST_EXCLUSIVE_MANAGER, 0);
+
+        mDevices.add(cachedDevice);
+        mBluetoothControllerImpl.onDeviceAdded(cachedDevice);
+
+        assertThat(mBluetoothControllerImpl.getConnectedDevices()).isEmpty();
+    }
+
+    @Test
+    @DisableFlags(FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
+    public void getConnectedDevice_exclusivelyManagedDevice_returnsConnected()
+            throws PackageManager.NameNotFoundException {
+        CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class);
+        when(cachedDevice.isConnected()).thenReturn(true);
+        BluetoothDevice device = mock(BluetoothDevice.class);
+        when(device.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+                TEST_EXCLUSIVE_MANAGER.getBytes());
+        when(cachedDevice.getDevice()).thenReturn(device);
+        doReturn(new ApplicationInfo()).when(mPackageManager).getApplicationInfo(
+                TEST_EXCLUSIVE_MANAGER, 0);
+
+        mDevices.add(cachedDevice);
+        mBluetoothControllerImpl.onDeviceAdded(cachedDevice);
+
+        assertThat(mBluetoothControllerImpl.getConnectedDevices()).hasSize(1);
+        assertThat(mBluetoothControllerImpl.getConnectedDevices().get(0))
+                .isEqualTo(cachedDevice);
+    }
+
+    @Test
     public void testOnBluetoothStateChange_updatesBluetoothState() {
         mBluetoothControllerImpl.onBluetoothStateChanged(BluetoothAdapter.STATE_OFF);
 
@@ -184,6 +258,7 @@
 
         assertFalse(mBluetoothControllerImpl.isBluetoothConnected());
         CachedBluetoothDevice device = mock(CachedBluetoothDevice.class);
+        when(device.getDevice()).thenReturn(mock(BluetoothDevice.class));
         mDevices.add(device);
         when(device.isConnected()).thenReturn(true);
         when(device.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_CONNECTED);
@@ -402,6 +477,7 @@
     private CachedBluetoothDevice createBluetoothDevice(
             int profile, boolean isConnected, boolean isActive) {
         CachedBluetoothDevice device = mock(CachedBluetoothDevice.class);
+        when(device.getDevice()).thenReturn(mock(BluetoothDevice.class));
         mDevices.add(device);
         when(device.isActiveDevice(profile)).thenReturn(isActive);
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt
index d56222e..b8b0060 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt
@@ -45,5 +45,6 @@
             logger = mediaUiEventLogger,
             mediaFlags = mediaFlags,
             mediaFilterRepository = mediaFilterRepository,
+            mediaLoadingLogger = mediaLoadingLogger,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaLoadingLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaLoadingLoggerKosmos.kt
new file mode 100644
index 0000000..96886f7
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaLoadingLoggerKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain.pipeline
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+import org.mockito.Mockito.mock
+
+val Kosmos.mediaLoadingLogger by
+    Kosmos.Fixture { MediaLoadingLogger(logcatLogBuffer("MediaLoadingLogBuffer")) }
+val Kosmos.mockMediaLoadingLogger by Kosmos.Fixture { mock(MediaLoadingLogger::class.java) }
diff --git a/services/autofill/java/com/android/server/autofill/FillResponseEventLogger.java b/services/autofill/java/com/android/server/autofill/FillResponseEventLogger.java
index a69e33a..2391268 100644
--- a/services/autofill/java/com/android/server/autofill/FillResponseEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/FillResponseEventLogger.java
@@ -36,6 +36,7 @@
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_CANCELLED;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_FAILURE;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_SESSION_DESTROYED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_TRANSACTION_TOO_LARGE;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_SUCCESS;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_TIMEOUT;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_UNKNOWN;
@@ -162,6 +163,8 @@
       AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_CANCELLED;
   public static final int RESPONSE_STATUS_FAILURE =
       AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_FAILURE;
+  public static final int RESPONSE_STATUS_TRANSACTION_TOO_LARGE =
+      AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_TRANSACTION_TOO_LARGE;
   public static final int RESPONSE_STATUS_SESSION_DESTROYED =
       AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_SESSION_DESTROYED;
   public static final int RESPONSE_STATUS_SUCCESS =
diff --git a/services/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
index 7ceb3bb..07f5dcc 100644
--- a/services/autofill/java/com/android/server/autofill/RemoteFillService.java
+++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
@@ -82,9 +82,7 @@
         void onFillRequestSuccess(int requestId, @Nullable FillResponse response,
                 @NonNull String servicePackageName, int requestFlags);
 
-        void onFillRequestFailure(int requestId, @Nullable CharSequence message);
-
-        void onFillRequestTimeout(int requestId);
+        void onFillRequestFailure(int requestId, Throwable t);
 
         void onSaveRequestSuccess(@NonNull String servicePackageName,
                 @Nullable IntentSender intentSender);
@@ -345,11 +343,12 @@
                 Slog.e(TAG, "Error calling on fill request", err);
                 if (err instanceof TimeoutException) {
                     dispatchCancellationSignal(cancellationSink.get());
-                    mCallbacks.onFillRequestTimeout(request.getId());
+                    mCallbacks.onFillRequestFailure(request.getId(), err);
                 } else if (err instanceof CancellationException) {
+                    // Cancellation is a part of the user flow - don't mark as failure
                     dispatchCancellationSignal(cancellationSink.get());
                 } else {
-                    mCallbacks.onFillRequestFailure(request.getId(), err.getMessage());
+                    mCallbacks.onFillRequestFailure(request.getId(), err);
                 }
             }
         }));
@@ -413,11 +412,12 @@
                 Slog.e(TAG, "Error calling on fill request", err);
                 if (err instanceof TimeoutException) {
                     dispatchCancellationSignal(cancellationSink.get());
-                    mCallbacks.onFillRequestTimeout(request.getId());
+                    mCallbacks.onFillRequestFailure(request.getId(), err);
                 } else if (err instanceof CancellationException) {
+                    // Cancellation is a part of the user flow - don't mark as failure
                     dispatchCancellationSignal(cancellationSink.get());
                 } else {
-                    mCallbacks.onFillRequestFailure(request.getId(), err.getMessage());
+                    mCallbacks.onFillRequestFailure(request.getId(), err);
                 }
             }
         }));
diff --git a/services/autofill/java/com/android/server/autofill/RequestId.java b/services/autofill/java/com/android/server/autofill/RequestId.java
new file mode 100644
index 0000000..29ad786
--- /dev/null
+++ b/services/autofill/java/com/android/server/autofill/RequestId.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.server.autofill;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+// Helper class containing various methods to deal with FillRequest Ids.
+// For authentication flows, there needs to be a way to know whether to retrieve the Fill
+// Response from the primary provider or the secondary provider from the requestId. A simple
+// way to achieve this is by assigning odd number request ids to secondary provider and
+// even numbers to primary provider.
+public class RequestId {
+
+  private AtomicInteger sIdCounter;
+
+  // Mainly used for tests
+  RequestId(int start) {
+    sIdCounter = new AtomicInteger(start);
+  }
+
+  public RequestId() {
+    this((int) (Math.floor(Math.random() * 0xFFFF)));
+  }
+
+  public static int getLastRequestIdIndex(List<Integer> requestIds) {
+    int lastId = -1;
+    int indexOfBiggest = -1;
+    // Biggest number is usually the latest request, since IDs only increase
+    // The only exception is when the request ID wraps around back to 0
+      for (int i = requestIds.size() - 1; i >= 0; i--) {
+        if (requestIds.get(i) > lastId) {
+        lastId = requestIds.get(i);
+        indexOfBiggest = i;
+      }
+    }
+
+    // 0xFFFE + 2 == 0x1 (for secondary)
+    // 0xFFFD + 2 == 0x0 (for primary)
+    // Wrap has occurred
+    if (lastId >= 0xFFFD) {
+      // Calculate the biggest size possible
+      // If list only has one kind of request ids - we need to multiple by 2
+      // (since they skip odd ints)
+      // Also subtract one from size because at least one integer exists pre-wrap
+      int calcSize = (requestIds.size()) * 2;
+      //Biggest possible id after wrapping
+      int biggestPossible = (lastId + calcSize) % 0xFFFF;
+      lastId = -1;
+      indexOfBiggest = -1;
+      for (int i = 0; i < requestIds.size(); i++) {
+        int currentId = requestIds.get(i);
+        if (currentId <= biggestPossible && currentId > lastId) {
+          lastId = currentId;
+          indexOfBiggest = i;
+        }
+      }
+    }
+
+    return indexOfBiggest;
+  }
+
+  public int nextId(boolean isSecondary) {
+        // For authentication flows, there needs to be a way to know whether to retrieve the Fill
+        // Response from the primary provider or the secondary provider from the requestId. A simple
+        // way to achieve this is by assigning odd number request ids to secondary provider and
+        // even numbers to primary provider.
+        int requestId;
+
+        do {
+            requestId = sIdCounter.incrementAndGet() % 0xFFFF;
+            sIdCounter.set(requestId);
+        } while (isSecondaryProvider(requestId) != isSecondary);
+        return requestId;
+  }
+
+  public static boolean isSecondaryProvider(int requestId) {
+      return requestId % 2 == 1;
+  }
+}
diff --git a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java
index 044a064..a663896 100644
--- a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java
+++ b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java
@@ -75,12 +75,7 @@
     }
 
     @Override
-    public void onFillRequestFailure(int requestId, @Nullable CharSequence message) {
-
-    }
-
-    @Override
-    public void onFillRequestTimeout(int requestId) {
+    public void onFillRequestFailure(int requestId, Throwable t) {
 
     }
 
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index cdae16b..494e956 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -66,6 +66,7 @@
 import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_SESSION_DESTROYED;
 import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_SUCCESS;
 import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_TIMEOUT;
+import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_TRANSACTION_TOO_LARGE;
 import static com.android.server.autofill.Helper.containsCharsInOrder;
 import static com.android.server.autofill.Helper.createSanitizers;
 import static com.android.server.autofill.Helper.getNumericValue;
@@ -137,6 +138,7 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.SystemClock;
+import android.os.TransactionTooLargeException;
 import android.service.assist.classification.FieldClassificationRequest;
 import android.service.assist.classification.FieldClassificationResponse;
 import android.service.autofill.AutofillFieldClassificationService.Scores;
@@ -211,6 +213,7 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -263,7 +266,7 @@
 
     static final int AUGMENTED_AUTOFILL_REQUEST_ID = 1;
 
-    private static AtomicInteger sIdCounter = new AtomicInteger(2);
+    private static RequestId mRequestId = new RequestId();
 
     private static AtomicInteger sIdCounterForPcc = new AtomicInteger(2);
 
@@ -1333,7 +1336,7 @@
         }
 
         viewState.setState(newState);
-        int requestId = getRequestId(isSecondary);
+        int requestId = mRequestId.nextId(isSecondary);
 
         // Create a metrics log for the request
         final int ordinal = mRequestLogs.size() + 1;
@@ -1415,25 +1418,6 @@
         requestAssistStructureLocked(requestId, flags);
     }
 
-    private static int getRequestId(boolean isSecondary) {
-        // For authentication flows, there needs to be a way to know whether to retrieve the Fill
-        // Response from the primary provider or the secondary provider from the requestId. A simple
-        // way to achieve this is by assigning odd number request ids to secondary provider and
-        // even numbers to primary provider.
-        int requestId;
-        // TODO(b/158623971): Update this to prevent possible overflow
-        if (isSecondary) {
-            do {
-                requestId = sIdCounter.getAndIncrement();
-            } while (!isSecondaryProviderRequestId(requestId));
-        } else {
-            do {
-                requestId = sIdCounter.getAndIncrement();
-            } while (requestId == INVALID_REQUEST_ID || isSecondaryProviderRequestId(requestId));
-        }
-        return requestId;
-    }
-
     private boolean isRequestSupportFillDialog(int flags) {
         return (flags & FLAG_SUPPORTS_FILL_DIALOG) != 0;
     }
@@ -1441,7 +1425,7 @@
     @GuardedBy("mLock")
     private void requestAssistStructureForPccLocked(int flags) {
         if (!mClassificationState.shouldTriggerRequest()) return;
-        mFillRequestIdSnapshot = sIdCounter.get();
+        mFillRequestIdSnapshot = sIdCounterForPcc.get();
         mClassificationState.updatePendingRequest();
         // Get request id
         int requestId;
@@ -2365,20 +2349,9 @@
     // FillServiceCallbacks
     @Override
     @SuppressWarnings("GuardedBy")
-    public void onFillRequestFailure(int requestId, @Nullable CharSequence message) {
-        onFillRequestFailureOrTimeout(requestId, false, message);
-    }
-
-    // FillServiceCallbacks
-    @Override
-    @SuppressWarnings("GuardedBy")
-    public void onFillRequestTimeout(int requestId) {
-        onFillRequestFailureOrTimeout(requestId, true, null);
-    }
-
-    @SuppressWarnings("GuardedBy")
-    private void onFillRequestFailureOrTimeout(int requestId, boolean timedOut,
-            @Nullable CharSequence message) {
+    public void onFillRequestFailure(int requestId, Throwable t) {
+        CharSequence message = t.getMessage();
+        boolean timedOut = (t instanceof TimeoutException);
         boolean showMessage = !TextUtils.isEmpty(message);
 
         synchronized (mLock) {
@@ -2431,10 +2404,15 @@
                 }
             }
 
-            if (timedOut) {
+            if (t instanceof TimeoutException) {
                 mPresentationStatsEventLogger.maybeSetNoPresentationEventReason(
                         NOT_SHOWN_REASON_REQUEST_TIMEOUT);
                 mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_TIMEOUT);
+            } else if (t instanceof TransactionTooLargeException) {
+                mPresentationStatsEventLogger.maybeSetNoPresentationEventReason(
+                        NOT_SHOWN_REASON_REQUEST_FAILED);
+                mFillResponseEventLogger.maybeSetResponseStatus(
+                        RESPONSE_STATUS_TRANSACTION_TOO_LARGE);
             } else {
                 mPresentationStatsEventLogger.maybeSetNoPresentationEventReason(
                         NOT_SHOWN_REASON_REQUEST_FAILED);
@@ -2879,18 +2857,18 @@
             // the auth UI.
             Slog.w(TAG, "setAuthenticationResultLocked(" + authenticationId + "): no responses");
             mPresentationStatsEventLogger.maybeSetAuthenticationResult(
-                AUTHENTICATION_RESULT_FAILURE);
+                    AUTHENTICATION_RESULT_FAILURE);
             mPresentationStatsEventLogger.logAndEndEvent();
             removeFromService();
             return;
         }
-        final FillResponse authenticatedResponse = isSecondaryProviderRequestId(requestId)
+        final FillResponse authenticatedResponse = mRequestId.isSecondaryProvider(requestId)
                 ? mSecondaryResponses.get(requestId)
                 : mResponses.get(requestId);
         if (authenticatedResponse == null || data == null) {
             Slog.w(TAG, "no authenticated response");
             mPresentationStatsEventLogger.maybeSetAuthenticationResult(
-                AUTHENTICATION_RESULT_FAILURE);
+                    AUTHENTICATION_RESULT_FAILURE);
             mPresentationStatsEventLogger.logAndEndEvent();
             removeFromService();
             return;
@@ -2905,7 +2883,7 @@
             if (dataset == null) {
                 Slog.w(TAG, "no dataset with index " + datasetIdx + " on fill response");
                 mPresentationStatsEventLogger.maybeSetAuthenticationResult(
-                    AUTHENTICATION_RESULT_FAILURE);
+                        AUTHENTICATION_RESULT_FAILURE);
                 mPresentationStatsEventLogger.logAndEndEvent();
                 removeFromService();
                 return;
@@ -2946,7 +2924,7 @@
             }
             logAuthenticationStatusLocked(requestId, MetricsEvent.AUTOFILL_AUTHENTICATED);
             mPresentationStatsEventLogger.maybeSetAuthenticationResult(
-                AUTHENTICATION_RESULT_SUCCESS);
+                    AUTHENTICATION_RESULT_SUCCESS);
             replaceResponseLocked(authenticatedResponse, (FillResponse) result, newClientState);
         } else if (result instanceof GetCredentialResponse) {
             if (sDebug) {
@@ -2980,9 +2958,10 @@
                 logAuthenticationStatusLocked(requestId,
                         MetricsEvent.AUTOFILL_DATASET_AUTHENTICATED);
                 mPresentationStatsEventLogger.maybeSetAuthenticationResult(
-                    AUTHENTICATION_RESULT_SUCCESS);
+                        AUTHENTICATION_RESULT_SUCCESS);
                 if (newClientState != null) {
-                    if (sDebug) Slog.d(TAG,  "Updating client state from auth dataset");
+                    if (sDebug)
+                        Slog.d(TAG, "Updating client state from auth dataset");
                     mClientState = newClientState;
                 }
                 Dataset datasetFromResult = getEffectiveDatasetForAuthentication((Dataset) result);
@@ -2997,7 +2976,7 @@
                 logAuthenticationStatusLocked(requestId,
                         MetricsEvent.AUTOFILL_INVALID_DATASET_AUTHENTICATION);
                 mPresentationStatsEventLogger.maybeSetAuthenticationResult(
-                    AUTHENTICATION_RESULT_FAILURE);
+                        AUTHENTICATION_RESULT_FAILURE);
             }
         } else {
             if (result != null) {
@@ -3006,15 +2985,11 @@
             logAuthenticationStatusLocked(requestId,
                     MetricsEvent.AUTOFILL_INVALID_AUTHENTICATION);
             mPresentationStatsEventLogger.maybeSetAuthenticationResult(
-                AUTHENTICATION_RESULT_FAILURE);
+                    AUTHENTICATION_RESULT_FAILURE);
             processNullResponseLocked(requestId, 0);
         }
     }
 
-    private static boolean isSecondaryProviderRequestId(int requestId) {
-        return requestId % 2 == 1;
-    }
-
     private Dataset getDatasetFromCredentialResponse(GetCredentialResponse result) {
         if (result == null) {
             return null;
@@ -6929,22 +6904,15 @@
 
     @GuardedBy("mLock")
     private int getLastResponseIndexLocked() {
-        // The response ids are monotonically increasing so
-        // we just find the largest id which is the last. We
-        // do not rely on the internal ordering in sparse
-        // array to avoid - wow this stopped working!?
-        int lastResponseIdx = -1;
-        int lastResponseId = -1;
         if (mResponses != null) {
+            List<Integer> requestIdList = new ArrayList<>();
             final int responseCount = mResponses.size();
             for (int i = 0; i < responseCount; i++) {
-                if (mResponses.keyAt(i) > lastResponseId) {
-                    lastResponseIdx = i;
-                    lastResponseId = mResponses.keyAt(i);
-                }
+                requestIdList.add(mResponses.keyAt(i));
             }
+            return mRequestId.getLastRequestIdIndex(requestIdList);
         }
-        return lastResponseIdx;
+        return -1;
     }
 
     private LogMaker newLogMaker(int category) {
diff --git a/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java
index 76191bb..14eae8d 100644
--- a/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java
+++ b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java
@@ -16,10 +16,20 @@
 
 package com.android.server.audio;
 
+import static android.Manifest.permission.ACCESS_ULTRASOUND;
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
 import static android.Manifest.permission.CALL_AUDIO_INTERCEPTION;
+import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
+import static android.Manifest.permission.CAPTURE_AUDIO_OUTPUT;
+import static android.Manifest.permission.CAPTURE_MEDIA_OUTPUT;
+import static android.Manifest.permission.CAPTURE_TUNER_AUDIO_INPUT;
+import static android.Manifest.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT;
 import static android.Manifest.permission.MODIFY_AUDIO_ROUTING;
+import static android.Manifest.permission.MODIFY_AUDIO_SETTINGS;
+import static android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS;
 import static android.Manifest.permission.MODIFY_PHONE_STATE;
 import static android.Manifest.permission.RECORD_AUDIO;
+import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
 
 import android.annotation.Nullable;
 import android.os.RemoteException;
@@ -52,10 +62,21 @@
     static final String[] MONITORED_PERMS = new String[PermissionEnum.ENUM_SIZE];
 
     static {
-        MONITORED_PERMS[PermissionEnum.MODIFY_AUDIO_ROUTING] = MODIFY_AUDIO_ROUTING;
-        MONITORED_PERMS[PermissionEnum.MODIFY_PHONE_STATE] = MODIFY_PHONE_STATE;
         MONITORED_PERMS[PermissionEnum.RECORD_AUDIO] = RECORD_AUDIO;
+        MONITORED_PERMS[PermissionEnum.MODIFY_AUDIO_ROUTING] = MODIFY_AUDIO_ROUTING;
+        MONITORED_PERMS[PermissionEnum.MODIFY_AUDIO_SETTINGS] = MODIFY_AUDIO_SETTINGS;
+        MONITORED_PERMS[PermissionEnum.MODIFY_PHONE_STATE] = MODIFY_PHONE_STATE;
+        MONITORED_PERMS[PermissionEnum.MODIFY_DEFAULT_AUDIO_EFFECTS] = MODIFY_DEFAULT_AUDIO_EFFECTS;
+        MONITORED_PERMS[PermissionEnum.WRITE_SECURE_SETTINGS] = WRITE_SECURE_SETTINGS;
         MONITORED_PERMS[PermissionEnum.CALL_AUDIO_INTERCEPTION] = CALL_AUDIO_INTERCEPTION;
+        MONITORED_PERMS[PermissionEnum.ACCESS_ULTRASOUND] = ACCESS_ULTRASOUND;
+        MONITORED_PERMS[PermissionEnum.CAPTURE_AUDIO_OUTPUT] = CAPTURE_AUDIO_OUTPUT;
+        MONITORED_PERMS[PermissionEnum.CAPTURE_MEDIA_OUTPUT] = CAPTURE_MEDIA_OUTPUT;
+        MONITORED_PERMS[PermissionEnum.CAPTURE_AUDIO_HOTWORD] = CAPTURE_AUDIO_HOTWORD;
+        MONITORED_PERMS[PermissionEnum.CAPTURE_TUNER_AUDIO_INPUT] = CAPTURE_TUNER_AUDIO_INPUT;
+        MONITORED_PERMS[PermissionEnum.CAPTURE_VOICE_COMMUNICATION_OUTPUT] =
+                CAPTURE_VOICE_COMMUNICATION_OUTPUT;
+        MONITORED_PERMS[PermissionEnum.BLUETOOTH_CONNECT] = BLUETOOTH_CONNECT;
     }
 
     private final Object mLock = new Object();
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 4023e52..b436c8b 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -225,6 +225,7 @@
 import android.content.pm.IPackageManager;
 import android.content.pm.LauncherApps;
 import android.content.pm.ModuleInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.PackageManagerInternal;
@@ -4536,6 +4537,34 @@
         }
 
         @Override
+        public List<String> getPackagesBypassingDnd(int userId,
+                                                    boolean includeConversationChannels) {
+            checkCallerIsSystem();
+
+            final ArraySet<String> packageNames = new ArraySet<>();
+
+            for (int user : mUm.getProfileIds(userId, false)) {
+                List<PackageInfo> pkgs = mPackageManagerClient.getInstalledPackagesAsUser(0, user);
+                for (PackageInfo pi : pkgs) {
+                    String pkg = pi.packageName;
+                    // If any NotificationChannel for this package is bypassing, the
+                    // package is considered bypassing.
+                    for (NotificationChannel channel : getNotificationChannelsBypassingDnd(pkg,
+                            pi.applicationInfo.uid).getList()) {
+                        // Skips non-demoted conversation channels.
+                        if (!includeConversationChannels
+                                && !TextUtils.isEmpty(channel.getConversationId())
+                                && !channel.isDemoted()) {
+                            continue;
+                        }
+                        packageNames.add(pkg);
+                    }
+                }
+            }
+            return new ArrayList<String>(packageNames);
+        }
+
+        @Override
         public boolean areChannelsBypassingDnd() {
             if (android.app.Flags.modesApi()) {
                 return mZenModeHelper.getConsolidatedNotificationPolicy().allowPriorityChannels()
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 2f23955..4a59fc2 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -128,6 +128,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
 import com.android.internal.policy.ForceShowNavBarSettingsObserver;
 import com.android.internal.policy.GestureNavigationSettingsObserver;
 import com.android.internal.policy.ScreenDecorationsUtils;
@@ -673,6 +674,7 @@
                 mService.mHighRefreshRateDenylist);
 
         mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver(mHandler,
+                BackgroundThread.getHandler(),
                 mContext, () -> {
             synchronized (mLock) {
                 onConfigurationChanged();
diff --git a/services/core/java/com/android/server/wm/SafeActivityOptions.java b/services/core/java/com/android/server/wm/SafeActivityOptions.java
index f2dc55f..b452131 100644
--- a/services/core/java/com/android/server/wm/SafeActivityOptions.java
+++ b/services/core/java/com/android/server/wm/SafeActivityOptions.java
@@ -140,7 +140,9 @@
     }
 
     private ActivityOptions cloneLaunchingOptions(ActivityOptions options) {
-        return options == null ? null : ActivityOptions.makeBasic()
+        if (options == null) return null;
+
+        final ActivityOptions cloneOptions = ActivityOptions.makeBasic()
                 .setLaunchTaskDisplayArea(options.getLaunchTaskDisplayArea())
                 .setLaunchDisplayId(options.getLaunchDisplayId())
                 .setCallerDisplayId(options.getCallerDisplayId())
@@ -150,6 +152,8 @@
                 .setPendingIntentCreatorBackgroundActivityStartMode(
                         options.getPendingIntentCreatorBackgroundActivityStartMode())
                 .setRemoteTransition(options.getRemoteTransition());
+        cloneOptions.setLaunchWindowingMode(options.getLaunchWindowingMode());
+        return cloneOptions;
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index b814ccd..2b375e1 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -2027,7 +2027,9 @@
             // Otherwise, look at the package
             final ApplicationInfo appInfo = mPmInternal.getApplicationInfo(
                     packageName, 0 /* flags */, SYSTEM_UID, UserHandle.getUserId(callingUid));
-            if (appInfo == null || appInfo.uid != callingUid) {
+            if (appInfo == null
+                    || !mPmInternal.isSameApp(
+                            packageName, callingUid, UserHandle.getUserId(callingUid))) {
                 throw new SecurityException("Package " + packageName + " not in UID "
                         + callingUid);
             }
diff --git a/services/devicepolicy/Android.bp b/services/devicepolicy/Android.bp
index da965bb..32b571a 100644
--- a/services/devicepolicy/Android.bp
+++ b/services/devicepolicy/Android.bp
@@ -17,8 +17,10 @@
 java_library_static {
     name: "services.devicepolicy",
     defaults: ["platform_service_defaults"],
-    srcs: [":services.devicepolicy-sources"],
-
+    srcs: [
+        ":services.devicepolicy-sources",
+        ":statslog-devicepolicy-java-gen",
+    ],
     libs: [
         "services.core",
         "app-compat-annotations",
@@ -27,3 +29,11 @@
         "androidx.annotation_annotation",
     ],
 }
+
+genrule {
+    name: "statslog-devicepolicy-java-gen",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --java $(out) --module devicepolicy" +
+        " --javaPackage com.android.server.devicepolicy --javaClass DevicePolicyStatsLog",
+    out: ["com/android/server/devicepolicy/DevicePolicyStatsLog.java"],
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index be1a52d..43a2a93 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -109,6 +109,7 @@
 import static android.app.AppOpsManager.OPSTR_SYSTEM_EXEMPT_FROM_SUSPENSION;
 import static android.app.AppOpsManager.OP_RUN_ANY_IN_BACKGROUND;
 import static android.app.AppOpsManager.OP_RUN_IN_BACKGROUND;
+import static android.app.StatsManager.PULL_SUCCESS;
 import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_AFFILIATED;
 import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
 import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;
@@ -265,12 +266,26 @@
 import static android.security.keystore.AttestationUtils.USE_INDIVIDUAL_ATTESTATION;
 
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.PROVISIONING_ENTRY_POINT_ADB;
+import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
 import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_HIGH;
 import static com.android.server.am.ActivityManagerService.STOCK_PM_FLAGS;
 import static com.android.server.devicepolicy.DevicePolicyEngine.DEFAULT_POLICY_SIZE_LIMIT;
+import static com.android.server.devicepolicy.DevicePolicyStatsLog.DEVICE_POLICY_MANAGEMENT_MODE;
+import static com.android.server.devicepolicy.DevicePolicyStatsLog.DEVICE_POLICY_MANAGEMENT_MODE__MANAGEMENT_MODE__COPE;
+import static com.android.server.devicepolicy.DevicePolicyStatsLog.DEVICE_POLICY_MANAGEMENT_MODE__MANAGEMENT_MODE__DEVICE_OWNER;
+import static com.android.server.devicepolicy.DevicePolicyStatsLog.DEVICE_POLICY_MANAGEMENT_MODE__MANAGEMENT_MODE__DEVICE_OWNER_FINANCED;
+import static com.android.server.devicepolicy.DevicePolicyStatsLog.DEVICE_POLICY_MANAGEMENT_MODE__MANAGEMENT_MODE__MANAGEMENT_MODE_UNSPECIFIED;
+import static com.android.server.devicepolicy.DevicePolicyStatsLog.DEVICE_POLICY_MANAGEMENT_MODE__MANAGEMENT_MODE__PROFILE_OWNER;
+import static com.android.server.devicepolicy.DevicePolicyStatsLog.DEVICE_POLICY_STATE;
+import static com.android.server.devicepolicy.DevicePolicyStatsLog.DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_HIGH;
+import static com.android.server.devicepolicy.DevicePolicyStatsLog.DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_LEGACY;
+import static com.android.server.devicepolicy.DevicePolicyStatsLog.DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_LOW;
+import static com.android.server.devicepolicy.DevicePolicyStatsLog.DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_MEDIUM;
+import static com.android.server.devicepolicy.DevicePolicyStatsLog.DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_NONE;
+import static com.android.server.devicepolicy.DevicePolicyStatsLog.DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_UNSPECIFIED;
 import static com.android.server.devicepolicy.TransferOwnershipMetadataManager.ADMIN_TYPE_DEVICE_OWNER;
 import static com.android.server.devicepolicy.TransferOwnershipMetadataManager.ADMIN_TYPE_PROFILE_OWNER;
 import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
@@ -305,6 +320,7 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.app.StatsManager;
 import android.app.StatusBarManager;
 import android.app.admin.AccountTypePolicyKey;
 import android.app.admin.BooleanPolicyValue;
@@ -477,6 +493,7 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.StatsEvent;
 import android.util.Xml;
 import android.view.IWindowManager;
 import android.view.accessibility.AccessibilityManager;
@@ -804,11 +821,11 @@
     public static final long EXPLICIT_WIPE_BEHAVIOUR = 242193913L;
 
     /**
-     * Apps targetting U+ should now expect that attempts to grant sensor permissions without
+     * Apps targeting V+ should now expect that attempts to grant sensor permissions without
      * authorisation will result in a security exception.
      */
     @ChangeId
-    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public static final long THROW_SECURITY_EXCEPTION_FOR_SENSOR_PERMISSIONS = 277035314L;
 
     /**
@@ -3348,6 +3365,9 @@
                 synchronized (getLockObject()) {
                     mDevicePolicyEngine.reapplyAllPoliciesOnBootLocked();
                 }
+                if (Flags.managementModePolicyMetrics()) {
+                    registerStatsCallbacks();
+                }
                 break;
             case SystemService.PHASE_ACTIVITY_MANAGER_READY:
                 synchronized (getLockObject()) {
@@ -3487,6 +3507,121 @@
         return true;
     }
 
+    /** Register callbacks for statsd pulled atoms. */
+    private void registerStatsCallbacks() {
+        final StatsManager statsManager = mContext.getSystemService(StatsManager.class);
+        if (statsManager == null) {
+            Slog.wtf(LOG_TAG, "StatsManager system service not found.");
+            return;
+        }
+        statsManager.setPullAtomCallback(
+                DEVICE_POLICY_MANAGEMENT_MODE,
+                null, // use defaultPullAtomMetadata values
+                DIRECT_EXECUTOR,
+                this::onPullManagementModeAtom);
+        statsManager.setPullAtomCallback(
+                DEVICE_POLICY_STATE,
+                null, // use defaultPullAtomMetadata values
+                DIRECT_EXECUTOR,
+                this::onPullPolicyStateAtom);
+    }
+
+    /** Writes the pulled atoms. */
+    private int onPullManagementModeAtom(int atomTag, List<StatsEvent> statsEvents) {
+        synchronized (getLockObject()) {
+            statsEvents.add(DevicePolicyStatsLog.buildStatsEvent(
+                    DEVICE_POLICY_MANAGEMENT_MODE,
+                    getStatsManagementModeLocked().managementMode()));
+            return PULL_SUCCESS;
+        }
+    }
+
+    /** Writes the pulled atoms. */
+    private int onPullPolicyStateAtom(int atomTag, List<StatsEvent> statsEvents) {
+        synchronized (getLockObject()) {
+            StatsManagementMode statsManagementMode = getStatsManagementModeLocked();
+            if (statsManagementMode.admin() != null) {
+                statsEvents.add(DevicePolicyStatsLog.buildStatsEvent(DEVICE_POLICY_STATE,
+                        getRequiredPasswordComplexityStatsLocked(statsManagementMode.admin()),
+                        statsManagementMode.managementMode()
+                        ));
+            } else {
+                statsEvents.add(DevicePolicyStatsLog.buildStatsEvent(DEVICE_POLICY_STATE,
+                        DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_NONE,
+                        DEVICE_POLICY_MANAGEMENT_MODE__MANAGEMENT_MODE__MANAGEMENT_MODE_UNSPECIFIED
+                ));
+            }
+            return PULL_SUCCESS;
+        }
+    }
+
+    private StatsManagementMode getStatsManagementModeLocked() {
+        int managementMode =
+                DEVICE_POLICY_MANAGEMENT_MODE__MANAGEMENT_MODE__MANAGEMENT_MODE_UNSPECIFIED;
+        ActiveAdmin admin = getDeviceOwnerAdminLocked();
+        if (admin != null) {
+            managementMode = getDeviceOwnerTypeLocked(
+                    getDeviceOwnerComponent(false).getPackageName())
+                    != DEVICE_OWNER_TYPE_FINANCED
+                    ? DEVICE_POLICY_MANAGEMENT_MODE__MANAGEMENT_MODE__DEVICE_OWNER
+                    : DEVICE_POLICY_MANAGEMENT_MODE__MANAGEMENT_MODE__DEVICE_OWNER_FINANCED;
+        } else {
+            // Find the first user with managing_app.
+            for (Integer profileUserId : mOwners.getProfileOwnerKeys()) {
+                if (isManagedProfile(profileUserId)) {
+                    admin = getProfileOwnerAdminLocked(profileUserId);
+                    managementMode = mOwners.isProfileOwnerOfOrganizationOwnedDevice(
+                            profileUserId)
+                            ? DEVICE_POLICY_MANAGEMENT_MODE__MANAGEMENT_MODE__COPE
+                            : DEVICE_POLICY_MANAGEMENT_MODE__MANAGEMENT_MODE__PROFILE_OWNER;
+                    break;
+                }
+            }
+        }
+        return new StatsManagementMode(managementMode, admin);
+    }
+
+    private record StatsManagementMode(int managementMode, ActiveAdmin admin) {
+    }
+
+    @GuardedBy("getLockObject()")
+    private int getRequiredPasswordComplexityStatsLocked(ActiveAdmin admin) {
+        int userId = admin.getUserHandle().getIdentifier();
+        EnforcingAdmin enforcingAdmin = EnforcingAdmin.createEnterpriseEnforcingAdmin(
+                admin.info.getComponent(),
+                userId,
+                admin);
+
+        Integer passwordComplexity = mDevicePolicyEngine.getLocalPolicySetByAdmin(
+                PolicyDefinition.PASSWORD_COMPLEXITY,
+                enforcingAdmin,
+                userId);
+        if (passwordComplexity == null) {
+            return admin.mPasswordPolicy.quality != PASSWORD_QUALITY_UNSPECIFIED
+                    ? DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_LEGACY
+                    : DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_UNSPECIFIED;
+        }
+        switch (passwordComplexity) {
+            case PASSWORD_COMPLEXITY_NONE -> {
+                return DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_NONE;
+            }
+            case PASSWORD_COMPLEXITY_LOW -> {
+                return DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_LOW;
+            }
+            case PASSWORD_COMPLEXITY_MEDIUM -> {
+                return DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_MEDIUM;
+            }
+            case PASSWORD_COMPLEXITY_HIGH -> {
+                return DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_HIGH;
+            }
+            default -> {
+                Slogf.wtf(LOG_TAG, "Unhandled password complexity: " + passwordComplexity);
+                // The following line is unreachable as Slogf.wtf crashes the process.
+                // But we need this to avoid compilation error missing return statement.
+                return DEVICE_POLICY_STATE__PASSWORD_COMPLEXITY__COMPLEXITY_UNSPECIFIED;
+            }
+        }
+    }
 
     private void applyManagedSubscriptionsPolicyIfRequired() {
         int copeProfileUserId = getOrganizationOwnedProfileUserId();
@@ -16736,7 +16871,7 @@
                     caller.getUserId());
             if (SENSOR_PERMISSIONS.contains(permission)
                     && grantState == PERMISSION_GRANT_STATE_GRANTED
-                    && (!canAdminGrantSensorsPermissions() || isCallerDelegate(caller))) {
+                    && !canAdminGrantSensorsPermissions()) {
                 if (mInjector.isChangeEnabled(THROW_SECURITY_EXCEPTION_FOR_SENSOR_PERMISSIONS,
                         caller.getPackageName(), caller.getUserId())) {
                     throw new SecurityException(
@@ -16759,6 +16894,20 @@
                     || isFinancedDeviceOwner(caller)))
                     || (caller.hasPackage() && isCallerDelegate(caller,
                     DELEGATION_PERMISSION_GRANT)));
+            if (SENSOR_PERMISSIONS.contains(permission)
+                    && grantState == PERMISSION_GRANT_STATE_GRANTED
+                    && !canAdminGrantSensorsPermissions()) {
+                if (mInjector.isChangeEnabled(THROW_SECURITY_EXCEPTION_FOR_SENSOR_PERMISSIONS,
+                        caller.getPackageName(), caller.getUserId())) {
+                    throw new SecurityException(
+                            "Caller not permitted to grant sensor permissions.");
+                } else {
+                    Slogf.e(LOG_TAG, "Caller attempted to grant sensor permissions but denied");
+                    // This is to match the legacy behaviour.
+                    callback.sendResult(Bundle.EMPTY);
+                    return;
+                }
+            }
             synchronized (getLockObject()) {
                 long ident = mInjector.binderClearCallingIdentity();
                 try {
diff --git a/services/tests/servicestests/src/com/android/server/autofill/RequestIdTest.java b/services/tests/servicestests/src/com/android/server/autofill/RequestIdTest.java
new file mode 100644
index 0000000..6d56c41
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/autofill/RequestIdTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.autofill;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class RequestIdTest {
+
+    List<Integer> datasetPrimaryNoWrap = new ArrayList<>();
+    List<Integer> datasetPrimaryWrap = new ArrayList<>();
+    List<Integer> datasetSecondaryNoWrap = new ArrayList<>();
+    List<Integer> datasetSecondaryWrap = new ArrayList<>();
+    List<Integer> datasetMixedNoWrap = new ArrayList<>();
+    List<Integer> datasetMixedWrap = new ArrayList<>();
+
+    @Before
+    public void setup() throws Exception {
+      int datasetSize = 300;
+
+        { // Generate primary only ids that do not wrap
+            RequestId requestId = new RequestId(0);
+            for (int i = 0; i < datasetSize; i++) {
+                datasetPrimaryNoWrap.add(requestId.nextId(false));
+            }
+        }
+
+        { // Generate primary only ids that wrap
+            RequestId requestId = new RequestId(0xff00);
+            for (int i = 0; i < datasetSize; i++) {
+                datasetPrimaryWrap.add(requestId.nextId(false));
+            }
+        }
+
+        { // Generate SECONDARY only ids that do not wrap
+            RequestId requestId = new RequestId(0);
+            for (int i = 0; i < datasetSize; i++) {
+                datasetSecondaryNoWrap.add(requestId.nextId(true));
+            }
+        }
+
+        { // Generate SECONDARY only ids that wrap
+            RequestId requestId = new RequestId(0xff00);
+            for (int i = 0; i < datasetSize; i++) {
+                datasetSecondaryWrap.add(requestId.nextId(true));
+            }
+        }
+
+        { // Generate MIXED only ids that do not wrap
+            RequestId requestId = new RequestId(0);
+            for (int i = 0; i < datasetSize; i++) {
+                datasetMixedNoWrap.add(requestId.nextId(i % 2 != 0));
+            }
+        }
+
+        { // Generate MIXED only ids that wrap
+            RequestId requestId = new RequestId(0xff00);
+            for (int i = 0; i < datasetSize; i++) {
+                datasetMixedWrap.add(requestId.nextId(i % 2 != 0));
+            }
+        }
+    }
+
+    @Test
+    public void testRequestIdLists() {
+        for (int id : datasetPrimaryNoWrap) {
+            assertThat(RequestId.isSecondaryProvider(id)).isFalse();
+            assertThat(id >= 0).isTrue();
+            assertThat(id < 0xffff).isTrue();
+        }
+
+        for (int id : datasetPrimaryWrap) {
+            assertThat(RequestId.isSecondaryProvider(id)).isFalse();
+            assertThat(id >= 0).isTrue();
+            assertThat(id < 0xffff).isTrue();
+        }
+
+        for (int id : datasetSecondaryNoWrap) {
+            assertThat(RequestId.isSecondaryProvider(id)).isTrue();
+            assertThat(id >= 0).isTrue();
+            assertThat(id < 0xffff).isTrue();
+        }
+
+        for (int id : datasetSecondaryWrap) {
+            assertThat(RequestId.isSecondaryProvider(id)).isTrue();
+            assertThat(id >= 0).isTrue();
+            assertThat(id < 0xffff).isTrue();
+        }
+    }
+
+    @Test
+    public void testRequestIdGeneration() {
+        RequestId requestId = new RequestId(0);
+
+        // Large Primary
+        for (int i = 0; i < 100000; i++) {
+            int y = requestId.nextId(false);
+            assertThat(RequestId.isSecondaryProvider(y)).isFalse();
+            assertThat(y >= 0).isTrue();
+            assertThat(y < 0xffff).isTrue();
+        }
+
+        // Large Secondary
+        requestId = new RequestId(0);
+        for (int i = 0; i < 100000; i++) {
+            int y = requestId.nextId(true);
+            assertThat(RequestId.isSecondaryProvider(y)).isTrue();
+            assertThat(y >= 0).isTrue();
+            assertThat(y < 0xffff).isTrue();
+        }
+
+        // Large Mixed
+        requestId = new RequestId(0);
+        for (int i = 0; i < 50000; i++) {
+            int y = requestId.nextId(i % 2 != 0);
+            assertThat(RequestId.isSecondaryProvider(y)).isEqualTo(i % 2 == 0);
+            assertThat(y >= 0).isTrue();
+            assertThat(y < 0xffff).isTrue();
+        }
+    }
+
+    @Test
+    public void testGetLastRequestId() {
+        // In this test, request ids are generated FIFO, so the last entry is also the last
+        // request
+
+        { // Primary no wrap
+          int lastIdIndex = datasetPrimaryNoWrap.size() - 1;
+          int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetPrimaryNoWrap);
+          assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
+        }
+
+        { // Primary wrap
+            int lastIdIndex = datasetPrimaryWrap.size() - 1;
+            int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetPrimaryWrap);
+            assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
+        }
+
+        { // Secondary no wrap
+            int lastIdIndex = datasetSecondaryNoWrap.size() - 1;
+            int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetSecondaryNoWrap);
+            assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
+        }
+
+        { // Secondary wrap
+            int lastIdIndex = datasetSecondaryWrap.size() - 1;
+            int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetSecondaryWrap);
+            assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
+        }
+
+        { // Mixed no wrap
+            int lastIdIndex = datasetMixedNoWrap.size() - 1;
+            int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetMixedNoWrap);
+            assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
+        }
+
+        { // Mixed wrap
+            int lastIdIndex = datasetMixedWrap.size() - 1;
+            int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetMixedWrap);
+            assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
+        }
+
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index f07e5bc..398dc281 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -208,6 +208,7 @@
 import android.content.pm.IPackageManager;
 import android.content.pm.LauncherApps;
 import android.content.pm.ModuleInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ParceledListSlice;
@@ -12072,6 +12073,127 @@
     }
 
     @Test
+    public void testGetPackagesBypassingDnd_empty() throws RemoteException {
+        mService.setPreferencesHelper(mPreferencesHelper);
+        List<String> result = mBinderService.getPackagesBypassingDnd(mUserId, true);
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    public void testGetPackagesBypassingDnd_excludeConversationChannels() throws RemoteException {
+        mService.setPreferencesHelper(mPreferencesHelper);
+
+        // Set packages
+        PackageInfo pkg0 = new PackageInfo();
+        pkg0.packageName = "pkg0";
+        pkg0.applicationInfo = new ApplicationInfo();
+        pkg0.applicationInfo.uid = mUid;
+        PackageInfo pkg1 = new PackageInfo();
+        pkg1.packageName = "pkg1";
+        pkg1.applicationInfo = new ApplicationInfo();
+        pkg1.applicationInfo.uid = mUid;
+        PackageInfo pkg2 = new PackageInfo();
+        pkg2.packageName = "pkg2";
+        pkg2.applicationInfo = new ApplicationInfo();
+        pkg2.applicationInfo.uid = mUid;
+
+        when(mPackageManagerClient.getInstalledPackagesAsUser(0, mUserId))
+                .thenReturn(List.of(pkg0, pkg1, pkg2));
+
+        // Conversation channels
+        NotificationChannel nc0 = new NotificationChannel("id0", "id0",
+                NotificationManager.IMPORTANCE_HIGH);
+        nc0.setConversationId("parentChannel", "conversationId");
+
+        // Demoted conversation channel
+        NotificationChannel nc1 = new NotificationChannel("id1", "id1",
+                NotificationManager.IMPORTANCE_HIGH);
+        nc1.setConversationId("parentChannel", "conversationId");
+        nc1.setDemoted(true);
+
+        // Non-conversation channels
+        NotificationChannel nc2 = new NotificationChannel("id2", "id2",
+                NotificationManager.IMPORTANCE_HIGH);
+        NotificationChannel nc3 = new NotificationChannel("id3", "id3",
+                NotificationManager.IMPORTANCE_HIGH);
+
+        ParceledListSlice<NotificationChannel> pls0 =
+                new ParceledListSlice(ImmutableList.of(nc0));
+        ParceledListSlice<NotificationChannel> pls1 =
+                new ParceledListSlice(ImmutableList.of(nc1));
+        ParceledListSlice<NotificationChannel> pls2 =
+                new ParceledListSlice(ImmutableList.of(nc2, nc3));
+
+        when(mPreferencesHelper.getNotificationChannelsBypassingDnd("pkg0", mUid))
+                .thenReturn(pls0);
+        when(mPreferencesHelper.getNotificationChannelsBypassingDnd("pkg1", mUid))
+                .thenReturn(pls1);
+        when(mPreferencesHelper.getNotificationChannelsBypassingDnd("pkg2", mUid))
+                .thenReturn(pls2);
+
+        List<String> result = mBinderService.getPackagesBypassingDnd(mUserId, false);
+
+        assertThat(result).containsExactly("pkg1", "pkg2");
+    }
+
+    @Test
+    public void testGetPackagesBypassingDnd_includeConversationChannels() throws RemoteException {
+        mService.setPreferencesHelper(mPreferencesHelper);
+
+        // Set packages
+        PackageInfo pkg0 = new PackageInfo();
+        pkg0.packageName = "pkg0";
+        pkg0.applicationInfo = new ApplicationInfo();
+        pkg0.applicationInfo.uid = mUid;
+        PackageInfo pkg1 = new PackageInfo();
+        pkg1.packageName = "pkg1";
+        pkg1.applicationInfo = new ApplicationInfo();
+        pkg1.applicationInfo.uid = mUid;
+        PackageInfo pkg2 = new PackageInfo();
+        pkg2.packageName = "pkg2";
+        pkg2.applicationInfo = new ApplicationInfo();
+        pkg2.applicationInfo.uid = mUid;
+
+        when(mPackageManagerClient.getInstalledPackagesAsUser(0, mUserId))
+                .thenReturn(List.of(pkg0, pkg1, pkg2));
+
+        // Conversation channels
+        NotificationChannel nc0 = new NotificationChannel("id0", "id0",
+                NotificationManager.IMPORTANCE_HIGH);
+        nc0.setConversationId("parentChannel", "conversationId");
+
+        // Demoted conversation channel
+        NotificationChannel nc1 = new NotificationChannel("id1", "id1",
+                NotificationManager.IMPORTANCE_HIGH);
+        nc1.setConversationId("parentChannel", "conversationId");
+        nc1.setDemoted(true);
+
+        // Non-conversation channels
+        NotificationChannel nc2 = new NotificationChannel("id2", "id2",
+                NotificationManager.IMPORTANCE_HIGH);
+        NotificationChannel nc3 = new NotificationChannel("id3", "id3",
+                NotificationManager.IMPORTANCE_HIGH);
+
+        ParceledListSlice<NotificationChannel> pls0 =
+                new ParceledListSlice(ImmutableList.of(nc0));
+        ParceledListSlice<NotificationChannel> pls1 =
+                new ParceledListSlice(ImmutableList.of(nc1));
+        ParceledListSlice<NotificationChannel> pls2 =
+                new ParceledListSlice(ImmutableList.of(nc2, nc3));
+
+        when(mPreferencesHelper.getNotificationChannelsBypassingDnd("pkg0", mUid))
+                .thenReturn(pls0);
+        when(mPreferencesHelper.getNotificationChannelsBypassingDnd("pkg1", mUid))
+                .thenReturn(pls1);
+        when(mPreferencesHelper.getNotificationChannelsBypassingDnd("pkg2", mUid))
+                .thenReturn(pls2);
+
+        List<String> result = mBinderService.getPackagesBypassingDnd(mUserId, true);
+
+        assertThat(result).containsExactly("pkg0", "pkg1", "pkg2");
+    }
+
+    @Test
     public void testMatchesCallFilter_noPermissionShouldThrow() throws Exception {
         // set the testable NMS to not system uid/appid
         mService.isSystemUid = false;