Merge "Reuse StateChange logic in SysUIState" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 2dd16de..4d9fdf0 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -587,6 +587,7 @@
 java_aconfig_library {
     name: "android.view.inputmethod.flags-aconfig-java",
     aconfig_declarations: "android.view.inputmethod.flags-aconfig",
+    host_supported: true,
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
diff --git a/cmds/svc/src/com/android/commands/svc/UsbCommand.java b/cmds/svc/src/com/android/commands/svc/UsbCommand.java
index 26e20f6..6542d08 100644
--- a/cmds/svc/src/com/android/commands/svc/UsbCommand.java
+++ b/cmds/svc/src/com/android/commands/svc/UsbCommand.java
@@ -89,6 +89,11 @@
             IUsbManager usbMgr = IUsbManager.Stub.asInterface(ServiceManager.getService(
                     Context.USB_SERVICE));
 
+            if (usbMgr == null) {
+                System.err.println("Could not obtain USB service. Try again later.");
+                return;
+            }
+
             Executor executor = context.getMainExecutor();
             Consumer<Integer> consumer = new Consumer<Integer>(){
                 public void accept(Integer status){
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index bb023f2..12bfccf 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -4507,7 +4507,7 @@
     method @NonNull public android.window.WindowContainerTransaction requestFocusOnTaskFragment(@NonNull android.os.IBinder);
     method @NonNull public android.window.WindowContainerTransaction scheduleFinishEnterPip(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
     method @NonNull public android.window.WindowContainerTransaction setActivityWindowingMode(@NonNull android.window.WindowContainerToken, int);
-    method @NonNull public android.window.WindowContainerTransaction setAdjacentRoots(@NonNull android.window.WindowContainerToken, @NonNull android.window.WindowContainerToken);
+    method @Deprecated @NonNull public android.window.WindowContainerTransaction setAdjacentRoots(@NonNull android.window.WindowContainerToken, @NonNull android.window.WindowContainerToken);
     method @NonNull public android.window.WindowContainerTransaction setAdjacentTaskFragments(@NonNull android.os.IBinder, @NonNull android.os.IBinder, @Nullable android.window.WindowContainerTransaction.TaskFragmentAdjacentParams);
     method @NonNull public android.window.WindowContainerTransaction setAppBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
     method @NonNull public android.window.WindowContainerTransaction setBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java
index 5f57a41..d5b2f98 100644
--- a/core/java/android/animation/AnimationHandler.java
+++ b/core/java/android/animation/AnimationHandler.java
@@ -110,8 +110,7 @@
         }
     };
 
-    public static final ThreadLocal<AnimationHandler> sAnimatorHandler =
-            ThreadLocal.withInitial(AnimationHandler::new);
+    public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
     private static AnimationHandler sTestHandler = null;
     private boolean mListDirty = false;
 
@@ -119,6 +118,9 @@
         if (sTestHandler != null) {
             return sTestHandler;
         }
+        if (sAnimatorHandler.get() == null) {
+            sAnimatorHandler.set(new AnimationHandler());
+        }
         return sAnimatorHandler.get();
     }
 
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 73de1b6..c74bd1a 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -6981,6 +6981,8 @@
      * <p>The caller must hold the
      * {@link android.Manifest.permission#TRIGGER_LOST_MODE} permission.
      *
+     * <p>This API accesses the device's location and will only be used when a device is lost.
+     *
      * <p>Register a broadcast receiver to receive lost mode location updates. This receiver should
      * subscribe to the {@link #ACTION_LOST_MODE_LOCATION_UPDATE} action and receive the location
      * from an intent extra {@link #EXTRA_LOST_MODE_LOCATION}.
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 5c267c9..4afe75f 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -265,6 +265,16 @@
 }
 
 flag {
+  name: "expanding_public_view"
+  namespace: "systemui"
+  description: "enables user expanding the public view of a notification"
+  bug: "398853084"
+    metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "api_rich_ongoing"
   is_exported: true
   namespace: "systemui"
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 55d78f9b..cc288b1 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -744,15 +744,22 @@
      */
     public static final long BIND_MATCH_QUARANTINED_COMPONENTS = 0x2_0000_0000L;
 
+    /**
+     * Flag for {@link #bindService} that allows the bound app to be frozen if it is eligible.
+     *
+     * @hide
+     */
+    public static final long BIND_ALLOW_FREEZE = 0x4_0000_0000L;
 
     /**
      * These bind flags reduce the strength of the binding such that we shouldn't
      * consider it as pulling the process up to the level of the one that is bound to it.
      * @hide
      */
-    public static final int BIND_REDUCTION_FLAGS =
+    public static final long BIND_REDUCTION_FLAGS =
             Context.BIND_ALLOW_OOM_MANAGEMENT | Context.BIND_WAIVE_PRIORITY
-                    | Context.BIND_NOT_PERCEPTIBLE | Context.BIND_NOT_VISIBLE;
+                    | Context.BIND_NOT_PERCEPTIBLE | Context.BIND_NOT_VISIBLE
+                    | Context.BIND_ALLOW_FREEZE;
 
     /** @hide */
     @IntDef(flag = true, prefix = { "RECEIVER_VISIBLE" }, value = {
diff --git a/core/java/android/hardware/input/IKeyGestureHandler.aidl b/core/java/android/hardware/input/IKeyGestureHandler.aidl
index 509b948..4da991e 100644
--- a/core/java/android/hardware/input/IKeyGestureHandler.aidl
+++ b/core/java/android/hardware/input/IKeyGestureHandler.aidl
@@ -28,15 +28,4 @@
      * to that gesture.
      */
     boolean handleKeyGesture(in AidlKeyGestureEvent event, in IBinder focusedToken);
-
-    /**
-     * Called to know if a particular gesture type is supported by the handler.
-     *
-     * TODO(b/358569822): Remove this call to reduce the binder calls to single call for
-     *  handleKeyGesture. For this we need to remove dependency of multi-key gestures to identify if
-     *  a key gesture is supported on first relevant key down.
-     *  Also, for now we prioritize handlers in the system server process above external handlers to
-     *  reduce IPC binder calls.
-     */
-    boolean isKeyGestureSupported(int gestureType);
 }
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 49db54d..d6419af 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1758,13 +1758,6 @@
          */
         boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
                 @Nullable IBinder focusedToken);
-
-        /**
-         * Called to identify if a particular gesture is of interest to a handler.
-         *
-         * NOTE: If no active handler supports certain gestures, the gestures will not be captured.
-         */
-        boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType);
     }
 
     /** @hide */
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index a9a45ae..c4b4831 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -1193,23 +1193,6 @@
             }
             return false;
         }
-
-        @Override
-        public boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
-            synchronized (mKeyGestureEventHandlerLock) {
-                if (mKeyGestureEventHandlers == null) {
-                    return false;
-                }
-                final int numHandlers = mKeyGestureEventHandlers.size();
-                for (int i = 0; i < numHandlers; i++) {
-                    KeyGestureEventHandler handler = mKeyGestureEventHandlers.get(i);
-                    if (handler.isKeyGestureSupported(gestureType)) {
-                        return true;
-                    }
-                }
-            }
-            return false;
-        }
     }
 
     /**
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index 1a712d2..9dd1fed 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -108,7 +108,8 @@
     public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD = 55;
     public static final int KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD = 56;
     public static final int KEY_GESTURE_TYPE_GLOBAL_ACTIONS = 57;
-    public static final int KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58;
+    @Deprecated
+    public static final int DEPRECATED_KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58;
     public static final int KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT = 59;
     public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT = 60;
     public static final int KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS = 61;
@@ -200,7 +201,6 @@
             KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
             KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD,
             KEY_GESTURE_TYPE_GLOBAL_ACTIONS,
-            KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
             KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
             KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
             KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
@@ -777,8 +777,6 @@
                 return "KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD";
             case KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
                 return "KEY_GESTURE_TYPE_GLOBAL_ACTIONS";
-            case KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
-                return "KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD";
             case KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
                 return "KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT";
             case KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index ae0b56e..45a21be 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -44,6 +44,16 @@
 }
 
 flag {
+  name: "notify_single_item_change_on_icon_load"
+  namespace: "intentresolver"
+  description: "ChooserGridAdapter to notify specific items change when the target icon is loaded (instead of all-item change)."
+  bug: "298193161"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "fix_resolver_memory_leak"
   is_exported: true
   namespace: "intentresolver"
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 44c3f9a..0152c52 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -75,9 +75,12 @@
     // These should match the constants in framework/base/libs/hwui/hwui/DrawTextFunctor.h
     private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX = 0f;
     private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR = 0f;
-    private static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP = 5f;
     // since we're not using soft light yet, this needs to be much lower than the spec'd 0.8
     private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.7f;
+    @VisibleForTesting
+    static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP = 5f;
+    @VisibleForTesting
+    static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR = 0.5f;
 
     /** @hide */
     @IntDef(prefix = { "BREAK_STRATEGY_" }, value = {
@@ -1030,7 +1033,9 @@
 
         var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX,
                 mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR);
-        var cornerRadius = mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP;
+        var cornerRadius = Math.max(
+                mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP,
+                mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR);
 
         // We set the alpha on the color itself instead of Paint.setAlpha(), because that function
         // actually mutates the color in... *ehem* very strange ways. Also the color might get reset
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index 7c1e497..3a2ec91 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -967,8 +967,11 @@
             DisplayEventReceiver.VsyncEventData vsyncEventData) {
         final long startNanos;
         final long frameIntervalNanos = vsyncEventData.frameInterval;
-        boolean resynced = false;
+        // Original intended vsync time that is not adjusted by jitter
+        // or buffer stuffing recovery. Reported for jank tracking.
+        final long intendedFrameTimeNanos = frameTimeNanos;
         long offsetFrameTimeNanos = frameTimeNanos;
+        boolean resynced = false;
 
         // Evaluate if buffer stuffing recovery needs to start or end, and
         // what actions need to be taken for recovery.
@@ -1012,7 +1015,6 @@
                             + ((offsetFrameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
                 }
 
-                long intendedFrameTimeNanos = offsetFrameTimeNanos;
                 startNanos = System.nanoTime();
                 // Calculating jitter involves using the original frame time without
                 // adjustments from buffer stuffing
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 6f346bd..394ac8f 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -1986,7 +1986,11 @@
             // report its requested visibility at the end of the animation, otherwise we would
             // lose the leash, and it would disappear during the animation
             // TODO(b/326377046) revisit this part and see if we can make it more general
-            typesToReport = mRequestedVisibleTypes | (mAnimatingTypes & ime());
+            if (Flags.reportAnimatingInsetsTypes()) {
+                typesToReport = mRequestedVisibleTypes;
+            } else {
+                typesToReport = mRequestedVisibleTypes | (mAnimatingTypes & ime());
+            }
         } else {
             typesToReport = mRequestedVisibleTypes;
         }
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index eca798d..2908855 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -379,7 +379,7 @@
             @Nullable IRemoteInputConnection remoteInputConnection,
             @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
             int unverifiedTargetSdkVersion, @UserIdInt int userId,
-            @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) {
         final IInputMethodManager service = getService();
         if (service == null) {
             return InputBindResult.NULL;
@@ -388,7 +388,7 @@
             return service.startInputOrWindowGainedFocus(startInputReason, client, windowToken,
                     startInputFlags, softInputMode, windowFlags, editorInfo, remoteInputConnection,
                     remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId,
-                    imeDispatcher);
+                    imeDispatcher, imeRequestedVisible);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -408,7 +408,8 @@
             @Nullable IRemoteInputConnection remoteInputConnection,
             @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
             int unverifiedTargetSdkVersion, @UserIdInt int userId,
-            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean useAsyncShowHideMethod) {
+            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible,
+            boolean useAsyncShowHideMethod) {
         final IInputMethodManager service = getService();
         if (service == null) {
             return -1;
@@ -417,7 +418,8 @@
             service.startInputOrWindowGainedFocusAsync(startInputReason, client, windowToken,
                     startInputFlags, softInputMode, windowFlags, editorInfo, remoteInputConnection,
                     remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId,
-                    imeDispatcher, advanceAngGetStartInputSequenceNumber(), useAsyncShowHideMethod);
+                    imeDispatcher, imeRequestedVisible, advanceAngGetStartInputSequenceNumber(),
+                    useAsyncShowHideMethod);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 0b34600..a41ab36 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -871,6 +871,19 @@
         IInputMethodManagerGlobalInvoker.reportPerceptibleAsync(windowToken, perceptible);
     }
 
+    private static boolean hasViewImeRequestedVisible(View view) {
+        // before the refactor, the requestedVisibleTypes for the IME were not in sync with
+        // the state that was actually requested.
+        if (Flags.refactorInsetsController() && view != null) {
+            final var controller = view.getWindowInsetsController();
+            if (controller != null) {
+                return (view.getWindowInsetsController()
+                        .getRequestedVisibleTypes() & WindowInsets.Type.ime()) != 0;
+            }
+        }
+        return false;
+    }
+
     private final class DelegateImpl implements
             ImeFocusController.InputMethodManagerDelegate {
 
@@ -941,6 +954,9 @@
                     Log.v(TAG, "Reporting focus gain, without startInput");
                 }
 
+                final boolean imeRequestedVisible = hasViewImeRequestedVisible(
+                        mCurRootView.getView());
+
                 // ignore the result
                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMM.startInputOrWindowGainedFocus");
                 IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus(
@@ -950,7 +966,7 @@
                         null,
                         null, null,
                         mCurRootView.mContext.getApplicationInfo().targetSdkVersion,
-                        UserHandle.myUserId(), mImeDispatcher);
+                        UserHandle.myUserId(), mImeDispatcher, imeRequestedVisible);
                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
             }
         }
@@ -2441,9 +2457,8 @@
                     ImeTracker.forLogging().onProgress(statsToken,
                             ImeTracker.PHASE_CLIENT_NO_ONGOING_USER_ANIMATION);
                     if (resultReceiver != null) {
-                        final boolean imeReqVisible =
-                                (viewRootImpl.getInsetsController().getRequestedVisibleTypes()
-                                        & WindowInsets.Type.ime()) != 0;
+                        final boolean imeReqVisible = hasViewImeRequestedVisible(
+                                viewRootImpl.getView());
                         resultReceiver.send(
                                 imeReqVisible ? InputMethodManager.RESULT_UNCHANGED_SHOWN
                                         : InputMethodManager.RESULT_SHOWN, null);
@@ -2656,9 +2671,8 @@
                     ImeTracker.forLogging().onProgress(statsToken,
                             ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE);
 
-                    final boolean imeReqVisible =
-                            (viewRootImpl.getInsetsController().getRequestedVisibleTypes()
-                                    & WindowInsets.Type.ime()) != 0;
+                    final boolean imeReqVisible = hasViewImeRequestedVisible(
+                            viewRootImpl.getView());
                     if (resultReceiver != null) {
                         resultReceiver.send(
                                 !imeReqVisible ? InputMethodManager.RESULT_UNCHANGED_HIDDEN
@@ -3412,6 +3426,7 @@
         final Handler icHandler;
         InputBindResult res = null;
         final boolean hasServedView;
+        final boolean imeRequestedVisible;
         synchronized (mH) {
             // Now that we are locked again, validate that our state hasn't
             // changed.
@@ -3479,10 +3494,13 @@
             }
             mServedInputConnection = servedInputConnection;
 
+            imeRequestedVisible = hasViewImeRequestedVisible(servedView);
+
             if (DEBUG) {
                 Log.v(TAG, "START INPUT: view=" + InputMethodDebug.dumpViewInfo(view)
                         + " ic=" + ic + " editorInfo=" + editorInfo + " startInputFlags="
-                        + InputMethodDebug.startInputFlagsToString(startInputFlags));
+                        + InputMethodDebug.startInputFlagsToString(startInputFlags)
+                        + " imeRequestedVisible=" + imeRequestedVisible);
             }
 
             // When we switch between non-editable views, do not call into the IMMS.
@@ -3513,7 +3531,7 @@
                         servedInputConnection == null ? null
                                 : servedInputConnection.asIRemoteAccessibilityInputConnection(),
                         view.getContext().getApplicationInfo().targetSdkVersion, targetUserId,
-                        mImeDispatcher, mAsyncShowHideMethodEnabled);
+                        mImeDispatcher, imeRequestedVisible, mAsyncShowHideMethodEnabled);
             } else {
                 res = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus(
                         startInputReason, mClient, windowGainingFocus, startInputFlags,
@@ -3521,7 +3539,7 @@
                         servedInputConnection == null ? null
                                 : servedInputConnection.asIRemoteAccessibilityInputConnection(),
                         view.getContext().getApplicationInfo().targetSdkVersion, targetUserId,
-                        mImeDispatcher);
+                        mImeDispatcher, imeRequestedVisible);
             }
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
             if (Flags.useZeroJankProxy()) {
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index ea345a5..485e7b3 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -684,7 +684,10 @@
      * organizer.
      * @param root1 the first root.
      * @param root2 the second root.
+     * @deprecated replace with {@link #setAdjacentRootSet}
      */
+    @SuppressWarnings("UnflaggedApi") // @TestApi without associated feature.
+    @Deprecated
     @NonNull
     public WindowContainerTransaction setAdjacentRoots(
             @NonNull WindowContainerToken root1, @NonNull WindowContainerToken root2) {
@@ -704,7 +707,7 @@
      *
      * @param roots the Tasks that should be adjacent to each other.
      * @throws IllegalArgumentException if roots have size < 2.
-     * @hide // TODO(b/373709676) Rename to setAdjacentRoots and update CTS.
+     * @hide // TODO(b/373709676) Rename to setAdjacentRoots and update CTS in 25Q4.
      */
     @NonNull
     public WindowContainerTransaction setAdjacentRootSet(@NonNull WindowContainerToken... roots) {
@@ -1003,7 +1006,7 @@
     /**
      * Sets to TaskFragments adjacent to each other. Containers below two visible adjacent
      * TaskFragments will be made invisible. This is similar to
-     * {@link #setAdjacentRoots(WindowContainerToken, WindowContainerToken)}, but can be used with
+     * {@link #setAdjacentRootSet(WindowContainerToken...)}, but can be used with
      * fragmentTokens when that TaskFragments haven't been created (but will be created in the same
      * {@link WindowContainerTransaction}).
      * @param fragmentToken1    client assigned unique token to create TaskFragment with specified
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index cbeaeda..94e87c7 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -105,17 +105,6 @@
 
 flag {
     namespace: "windowing_sdk"
-    name: "allow_multiple_adjacent_task_fragments"
-    description: "Refactor to allow more than 2 adjacent TaskFragments"
-    bug: "373709676"
-    is_fixed_read_only: true
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
-    namespace: "windowing_sdk"
     name: "track_system_ui_context_before_wms"
     description: "Keep track of SystemUiContext before WMS is initialized"
     bug: "384428048"
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index c009fc3..9bc6671 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -23,6 +23,7 @@
 import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
 import static android.content.ContentProvider.getUriWithoutUserId;
 import static android.content.ContentProvider.getUserIdFromUri;
+import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad;
 import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
 import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
 
@@ -163,9 +164,11 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -3212,6 +3215,8 @@
 
         private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20;
 
+        private final Set<ViewHolderBase> mBoundViewHolders = new HashSet<>();
+
         ChooserGridAdapter(ChooserListAdapter wrappedAdapter) {
             super();
             mChooserListAdapter = wrappedAdapter;
@@ -3232,6 +3237,31 @@
                     notifyDataSetChanged();
                 }
             });
+            if (notifySingleItemChangeOnIconLoad()) {
+                wrappedAdapter.setOnIconLoadedListener(this::onTargetIconLoaded);
+            }
+        }
+
+        private void onTargetIconLoaded(DisplayResolveInfo info) {
+            for (ViewHolderBase holder : mBoundViewHolders) {
+                switch (holder.getViewType()) {
+                    case VIEW_TYPE_NORMAL:
+                        TargetInfo itemInfo =
+                                mChooserListAdapter.getItem(
+                                        ((ItemViewHolder) holder).mListPosition);
+                        if (info == itemInfo) {
+                            notifyItemChanged(holder.getAdapterPosition());
+                        }
+                        break;
+                    case VIEW_TYPE_CALLER_AND_RANK:
+                        ItemGroupViewHolder groupHolder = (ItemGroupViewHolder) holder;
+                        if (suggestedAppsGroupContainsTarget(groupHolder, info)) {
+                            notifyItemChanged(holder.getAdapterPosition());
+                        }
+                        break;
+                }
+
+            }
         }
 
         public void setFooterHeight(int height) {
@@ -3382,6 +3412,9 @@
 
         @Override
         public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+            if (notifySingleItemChangeOnIconLoad()) {
+                mBoundViewHolders.add((ViewHolderBase) holder);
+            }
             int viewType = ((ViewHolderBase) holder).getViewType();
             switch (viewType) {
                 case VIEW_TYPE_DIRECT_SHARE:
@@ -3396,6 +3429,22 @@
         }
 
         @Override
+        public void onViewRecycled(RecyclerView.ViewHolder holder) {
+            if (notifySingleItemChangeOnIconLoad()) {
+                mBoundViewHolders.remove((ViewHolderBase) holder);
+            }
+            super.onViewRecycled(holder);
+        }
+
+        @Override
+        public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) {
+            if (notifySingleItemChangeOnIconLoad()) {
+                mBoundViewHolders.remove((ViewHolderBase) holder);
+            }
+            return super.onFailedToRecycleView(holder);
+        }
+
+        @Override
         public int getItemViewType(int position) {
             int count;
 
@@ -3604,6 +3653,33 @@
             }
         }
 
+        /**
+         * Checks whether the suggested apps group, {@code holder}, contains the target,
+         * {@code info}.
+         */
+        private boolean suggestedAppsGroupContainsTarget(
+                ItemGroupViewHolder holder, DisplayResolveInfo info) {
+
+            int position = holder.getAdapterPosition();
+            int start = getListPosition(position);
+            int startType = getRowType(start);
+
+            int columnCount = holder.getColumnCount();
+            int end = start + columnCount - 1;
+            while (getRowType(end) != startType && end >= start) {
+                end--;
+            }
+
+            for (int i = 0; i < columnCount; i++) {
+                if (start + i <= end) {
+                    if (mChooserListAdapter.getItem(holder.getItemIndex(i)) == info) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
         int getListPosition(int position) {
             position -= getSystemRowCount() + getProfileRowCount();
 
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index d38689c..1b8c36d 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -16,9 +16,12 @@
 
 package com.android.internal.app;
 
+import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad;
+
 import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
 import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
 
+import android.annotation.Nullable;
 import android.app.prediction.AppPredictor;
 import android.content.ComponentName;
 import android.content.Context;
@@ -56,6 +59,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Consumer;
 
 public class ChooserListAdapter extends ResolverListAdapter {
     private static final String TAG = "ChooserListAdapter";
@@ -108,6 +112,9 @@
     // Represents the UserSpace in which the Initial Intents should be resolved.
     private final UserHandle mInitialIntentsUserSpace;
 
+    @Nullable
+    private Consumer<DisplayResolveInfo> mOnIconLoadedListener;
+
     // For pinned direct share labels, if the text spans multiple lines, the TextView will consume
     // the full width, even if the characters actually take up less than that. Measure the actual
     // line widths and constrain the View's width based upon that so that the pin doesn't end up
@@ -218,6 +225,10 @@
                 true);
     }
 
+    public void setOnIconLoadedListener(Consumer<DisplayResolveInfo> onIconLoadedListener) {
+        mOnIconLoadedListener = onIconLoadedListener;
+    }
+
     AppPredictor getAppPredictor() {
         return mAppPredictor;
     }
@@ -329,6 +340,15 @@
         }
     }
 
+    @Override
+    protected void onIconLoaded(DisplayResolveInfo info) {
+        if (notifySingleItemChangeOnIconLoad() && mOnIconLoadedListener != null) {
+            mOnIconLoadedListener.accept(info);
+        } else {
+            notifyDataSetChanged();
+        }
+    }
+
     private void loadDirectShareIcon(SelectableTargetInfo info) {
         LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info);
         if (task == null) {
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 54c0e61..4d9ce86 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -680,6 +680,10 @@
         }
     }
 
+    protected void onIconLoaded(DisplayResolveInfo info) {
+        notifyDataSetChanged();
+    }
+
     private void loadLabel(DisplayResolveInfo info) {
         LoadLabelTask task = mLabelLoaders.get(info);
         if (task == null) {
@@ -1004,7 +1008,7 @@
                 mResolverListCommunicator.updateProfileViewButton();
             } else if (!mDisplayResolveInfo.hasDisplayIcon()) {
                 mDisplayResolveInfo.setDisplayIcon(d);
-                notifyDataSetChanged();
+                onIconLoaded(mDisplayResolveInfo);
             }
         }
     }
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 9380d99..0791612 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -105,7 +105,7 @@
             in @nullable EditorInfo editorInfo, in @nullable IRemoteInputConnection inputConnection,
             in @nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
             int unverifiedTargetSdkVersion, int userId,
-            in ImeOnBackInvokedDispatcher imeDispatcher);
+            in ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible);
 
     // If windowToken is null, this just does startInput().  Otherwise this reports that a window
     // has gained focus, and if 'editorInfo' is non-null then also does startInput.
@@ -120,8 +120,8 @@
             in @nullable EditorInfo editorInfo, in @nullable IRemoteInputConnection inputConnection,
             in @nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
             int unverifiedTargetSdkVersion, int userId,
-            in ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq,
-            boolean useAsyncShowHideMethod);
+            in ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible,
+            int startInputSeq, boolean useAsyncShowHideMethod);
 
     void showInputMethodPickerFromClient(in IInputMethodClient client,
             int auxiliarySubtypeMode);
@@ -224,7 +224,7 @@
      *  async **/
     oneway void acceptStylusHandwritingDelegationAsync(in IInputMethodClient client, in int userId,
             in String delegatePackageName, in String delegatorPackageName, int flags,
-            in IBooleanListener callback);
+                in IBooleanListener callback);
 
     /** Returns {@code true} if currently selected IME supports Stylus handwriting. */
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 8c5b20d..7a38dce 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4184,6 +4184,11 @@
     <!-- Whether device supports double tap to wake -->
     <bool name="config_supportDoubleTapWake">false</bool>
 
+    <!-- Whether device supports double tap to sleep. This will allow the user to enable/disable
+         double tap gestures in non-action areas in the lock screen and launcher workspace to go to
+         sleep. -->
+    <bool name="config_supportDoubleTapSleep">false</bool>
+
     <!-- The RadioAccessFamilies supported by the device.
          Empty is viewed as "all".  Only used on devices which
          don't support RIL_REQUEST_GET_RADIO_CAPABILITY
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3de30f7..46d18e3 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3142,6 +3142,7 @@
   <java-symbol type="color" name="chooser_row_divider" />
   <java-symbol type="layout" name="chooser_row_direct_share" />
   <java-symbol type="bool" name="config_supportDoubleTapWake" />
+  <java-symbol type="bool" name="config_supportDoubleTapSleep" />
   <java-symbol type="drawable" name="ic_perm_device_info" />
   <java-symbol type="string" name="config_radio_access_family" />
   <java-symbol type="string" name="notification_inbox_ellipsis" />
diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java
index 9e78af5..11ec9f8 100644
--- a/core/tests/coretests/src/android/text/LayoutTest.java
+++ b/core/tests/coretests/src/android/text/LayoutTest.java
@@ -16,6 +16,9 @@
 
 package android.text;
 
+import static android.text.Layout.HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR;
+import static android.text.Layout.HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP;
+
 import static com.android.graphics.hwui.flags.Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -1073,6 +1076,68 @@
         }
     }
 
+    @Test
+    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+    public void highContrastTextEnabled_testRoundedRectSize_belowMinimum_usesMinimumValue() {
+        mTextPaint.setColor(Color.BLACK);
+        mTextPaint.setTextSize(8); // Value chosen so that N * RADIUS_FACTOR < RADIUS_MIN_DP
+        Layout layout = new StaticLayout("Test text", mTextPaint, mWidth,
+                mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false);
+
+        MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256);
+        c.setHighContrastTextEnabled(true);
+        layout.draw(
+                c,
+                /* highlightPaths= */ null,
+                /* highlightPaints= */ null,
+                /* selectionPath= */ null,
+                /* selectionPaint= */ null,
+                /* cursorOffsetVertical= */ 0
+        );
+
+        final float expectedRoundedRectSize =
+                mTextPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP;
+        List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
+        for (int i = 0; i < drawCommands.size(); i++) {
+            MockCanvas.DrawCommand drawCommand = drawCommands.get(i);
+            if (drawCommand.rect != null) {
+                expect.that(drawCommand.rX).isEqualTo(expectedRoundedRectSize);
+                expect.that(drawCommand.rY).isEqualTo(expectedRoundedRectSize);
+            }
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+    public void highContrastTextEnabled_testRoundedRectSize_aboveMinimum_usesScaledValue() {
+        mTextPaint.setColor(Color.BLACK);
+        mTextPaint.setTextSize(50); // Value chosen so that N * RADIUS_FACTOR > RADIUS_MIN_DP
+        Layout layout = new StaticLayout("Test text", mTextPaint, mWidth,
+                mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false);
+
+        MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256);
+        c.setHighContrastTextEnabled(true);
+        layout.draw(
+                c,
+                /* highlightPaths= */ null,
+                /* highlightPaints= */ null,
+                /* selectionPath= */ null,
+                /* selectionPaint= */ null,
+                /* cursorOffsetVertical= */ 0
+        );
+
+        final float expectedRoundedRectSize =
+                mTextPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR;
+        List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
+        for (int i = 0; i < drawCommands.size(); i++) {
+            MockCanvas.DrawCommand drawCommand = drawCommands.get(i);
+            if (drawCommand.rect != null) {
+                expect.that(drawCommand.rX).isEqualTo(expectedRoundedRectSize);
+                expect.that(drawCommand.rY).isEqualTo(expectedRoundedRectSize);
+            }
+        }
+    }
+
     private int removeAlpha(int color) {
         return Color.rgb(
                 Color.red(color),
@@ -1087,6 +1152,8 @@
             public final String text;
             public final float x;
             public final float y;
+            public final float rX;
+            public final float rY;
             public final Path path;
             public final RectF rect;
             public final Paint paint;
@@ -1098,6 +1165,8 @@
                 this.paint = new Paint(paint);
                 path = null;
                 rect = null;
+                this.rX = 0;
+                this.rY = 0;
             }
 
             DrawCommand(Path path, Paint paint) {
@@ -1107,15 +1176,19 @@
                 x = 0;
                 text = null;
                 rect = null;
+                this.rX = 0;
+                this.rY = 0;
             }
 
-            DrawCommand(RectF rect, Paint paint) {
+            DrawCommand(RectF rect, Paint paint, float rX, float rY) {
                 this.rect = new RectF(rect);
                 this.paint = new Paint(paint);
                 path = null;
                 y = 0;
                 x = 0;
                 text = null;
+                this.rX = rX;
+                this.rY = rY;
             }
 
             @Override
@@ -1189,12 +1262,12 @@
 
         @Override
         public void drawRect(RectF rect, Paint p) {
-            mDrawCommands.add(new DrawCommand(rect, p));
+            mDrawCommands.add(new DrawCommand(rect, p, 0, 0));
         }
 
         @Override
         public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) {
-            mDrawCommands.add(new DrawCommand(rect, paint));
+            mDrawCommands.add(new DrawCommand(rect, paint, rx, ry));
         }
 
         List<DrawCommand> getDrawCommands() {
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java
index db69cf2..02a2968 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java
@@ -72,7 +72,8 @@
 
     private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry
             .getInstrumentation().getTargetContext().getUser();
-    private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10);
+    private static final UserHandle WORK_USER_HANDLE =
+            UserHandle.of(PERSONAL_USER_HANDLE.getIdentifier() + 1);
 
     @Rule
     public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp
index 03076c0..5066691 100644
--- a/libs/WindowManager/Shell/multivalentTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentTests/Android.bp
@@ -51,6 +51,7 @@
         "androidx.test.ext.junit",
         "mockito-robolectric-prebuilt",
         "mockito-kotlin2",
+        "platform-parametric-runner-lib",
         "truth",
         "flag-junit-base",
         "flag-junit",
@@ -74,6 +75,7 @@
         "frameworks-base-testutils",
         "mockito-kotlin2",
         "mockito-target-extended-minus-junit4",
+        "platform-parametric-runner-lib",
         "truth",
         "platform-test-annotations",
         "platform-test-rules",
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt
new file mode 100644
index 0000000..bdfaef2
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles
+
+import android.content.ComponentName
+import android.content.Context
+import android.platform.test.flag.junit.FlagsParameterization
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.Flags
+import com.android.wm.shell.taskview.TaskView
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+/** Tests for [BubbleExpandedView] */
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class BubbleExpandedViewTest(flags: FlagsParameterization) {
+
+    @get:Rule
+    val setFlagsRule = SetFlagsRule(flags)
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private val componentName = ComponentName(context, "TestClass")
+
+    @Test
+    fun getTaskId_onTaskCreated_returnsCorrectTaskId() {
+        val bubbleTaskView = BubbleTaskView(mock<TaskView>(), directExecutor())
+        val expandedView = BubbleExpandedView(context).apply {
+            initialize(
+                mock<BubbleExpandedViewManager>(),
+                mock<BubbleStackView>(),
+                mock<BubblePositioner>(),
+                false /* isOverflow */,
+                bubbleTaskView,
+            )
+            setAnimating(true) // Skips setContentVisibility for testing.
+        }
+
+        bubbleTaskView.listener.onTaskCreated(123, componentName)
+
+        assertThat(expandedView.getTaskId()).isEqualTo(123)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams() = FlagsParameterization.allCombinationsOf(
+            Flags.FLAG_ENABLE_BUBBLE_TASK_VIEW_LISTENER,
+        )
+    }
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
index 9087da3..636ff66 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
@@ -266,8 +266,6 @@
             optionsCaptor.capture(),
             any())
 
-        assertThat((intentCaptor.lastValue.flags
-                and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
         assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
         assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
         assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
@@ -295,8 +293,6 @@
             optionsCaptor.capture(),
             any())
 
-        assertThat((intentCaptor.lastValue.flags
-                and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
         assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
         assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
         assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
@@ -324,8 +320,6 @@
             optionsCaptor.capture(),
             any())
 
-        assertThat((intentCaptor.lastValue.flags
-                and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
         assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
         assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
         assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
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 c2aa146..3ebe87d 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
@@ -38,11 +38,11 @@
             android:layout_height="wrap_content"
             android:layout_weight="1"
             android:orientation="vertical"
-            android:layout_marginStart="4dp"
-            android:layout_marginEnd="4dp">
+            android:gravity="center_horizontal"
+            android:layout_marginHorizontal="4dp">
 
             <Button
-                android:layout_width="94dp"
+                android:layout_width="108dp"
                 android:layout_height="60dp"
                 android:id="@+id/maximize_menu_immersive_toggle_button"
                 style="?android:attr/buttonBarButtonStyle"
@@ -75,8 +75,7 @@
             android:layout_weight="1"
             android:orientation="vertical"
             android:gravity="center_horizontal"
-            android:layout_marginStart="4dp"
-            android:layout_marginEnd="4dp">
+            android:layout_marginHorizontal="4dp">
 
             <Button
                 android:layout_width="108dp"
@@ -112,8 +111,7 @@
             android:layout_weight="1"
             android:orientation="vertical"
             android:gravity="center_horizontal"
-            android:layout_marginStart="4dp"
-            android:layout_marginEnd="4dp">
+            android:layout_marginHorizontal="4dp">
             <LinearLayout
                 android:id="@+id/maximize_menu_snap_menu_layout"
                 android:layout_width="wrap_content"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 2c2451c..8ac9230 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -238,7 +238,6 @@
                                 mContext.createContextAsUser(
                                         mBubble.getUser(), Context.CONTEXT_RESTRICTED);
                         Intent fillInIntent = new Intent();
-                        fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                         PendingIntent pi = PendingIntent.getActivity(
                                 context,
                                 /* requestCode= */ 0,
@@ -467,6 +466,11 @@
                         new BubbleTaskViewListener.Callback() {
                             @Override
                             public void onTaskCreated() {
+                                // The taskId is saved to use for removeTask,
+                                // preventing appearance in recent tasks.
+                                mTaskId = ((BubbleTaskViewListener) mCurrentTaskViewListener)
+                                    .getTaskId();
+
                                 setContentVisibility(true);
                             }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
index 63d7134..9c20e3a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
@@ -130,7 +130,6 @@
                             mContext.createContextAsUser(
                                     mBubble.getUser(), Context.CONTEXT_RESTRICTED);
                     Intent fillInIntent = new Intent();
-                    fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                     // First try get pending intent from the bubble
                     PendingIntent pi = mBubble.getPendingIntent();
                     if (pi == null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
index d5f2dbd..51a5b12 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
@@ -639,7 +639,7 @@
         @Override
         public void continueCollapse() {
             mBubble.cleanupTaskView();
-            if (mTaskLeash == null) return;
+            if (mTaskLeash == null || !mTaskLeash.isValid()) return;
             SurfaceControl.Transaction t = new SurfaceControl.Transaction();
             t.reparent(mTaskLeash, mRootLeash);
             t.apply();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 6c840f0..bdb21f2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -447,9 +447,9 @@
             bubble.cleanupViews(!inTransition);
             endAction.run();
         };
-        if (mBubbleData.getBubbles().isEmpty()) {
-            // we're removing the last bubble. collapse the expanded view and cleanup bubble views
-            // at the end.
+        if (mBubbleData.getBubbles().isEmpty() || inTransition) {
+            // If we are removing the last bubble or removing the current bubble via transition,
+            // collapse the expanded view and clean up bubbles at the end.
             collapse(cleanUp);
         } else {
             cleanUp.run();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index dd2050a..f737884 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -440,11 +440,18 @@
                             statsToken);
                 }
 
-                // In case of a hide, the statsToken should not been send yet (as the animation
-                // is still ongoing). It will be sent at the end of the animation
-                boolean hideAnimOngoing = !mImeRequestedVisible && mAnimation != null;
-                setVisibleDirectly(mImeRequestedVisible || mAnimation != null,
-                        hideAnimOngoing ? null : statsToken);
+                boolean hideAnimOngoing;
+                boolean reportVisible;
+                if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+                    hideAnimOngoing = false;
+                    reportVisible = mImeRequestedVisible;
+                } else {
+                    // In case of a hide, the statsToken should not been send yet (as the animation
+                    // is still ongoing). It will be sent at the end of the animation.
+                    hideAnimOngoing = !mImeRequestedVisible && mAnimation != null;
+                    reportVisible = mImeRequestedVisible || mAnimation != null;
+                }
+                setVisibleDirectly(reportVisible, hideAnimOngoing ? null : statsToken);
             }
         }
 
@@ -628,7 +635,7 @@
                                 + " showing:" + (mAnimationDirection == DIRECTION_SHOW));
                     }
                     if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
-                        setAnimating(true);
+                        setAnimating(true /* imeAnimationOngoing */);
                     }
                     int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY, defaultY),
                             imeTop(shownY, defaultY), mAnimationDirection == DIRECTION_SHOW,
@@ -678,7 +685,7 @@
                     if (!android.view.inputmethod.Flags.refactorInsetsController()) {
                         dispatchEndPositioning(mDisplayId, mCancelled, t);
                     } else if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
-                        setAnimating(false);
+                        setAnimating(false /* imeAnimationOngoing */);
                     }
                     if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
                         ImeTracker.forLogging().onProgress(mStatsToken,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 92c3020..bc2ed3f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -1472,7 +1472,9 @@
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             IWindowManager windowManager,
             ShellTaskOrganizer shellTaskOrganizer,
-            DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider
+            DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider,
+            InputManager inputManager,
+            @ShellMainThread Handler mainHandler
     ) {
         if (!DesktopModeStatus.canEnterDesktopMode(context)) {
             return Optional.empty();
@@ -1484,7 +1486,9 @@
                         rootTaskDisplayAreaOrganizer,
                         windowManager,
                         shellTaskOrganizer,
-                        desktopWallpaperActivityTokenProvider));
+                        desktopWallpaperActivityTokenProvider,
+                        inputManager,
+                        mainHandler));
     }
 
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
index e89aafe..904d862 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
@@ -22,6 +22,8 @@
 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
 import android.app.WindowConfiguration.windowingModeToString
 import android.content.Context
+import android.hardware.input.InputManager
+import android.os.Handler
 import android.provider.Settings
 import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
 import android.view.Display.DEFAULT_DISPLAY
@@ -29,11 +31,13 @@
 import android.view.WindowManager.TRANSIT_CHANGE
 import android.window.DesktopExperienceFlags
 import android.window.WindowContainerTransaction
+import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.protolog.ProtoLog
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.shared.annotations.ShellMainThread
 import com.android.wm.shell.transition.Transitions
 
 /** Controls the display windowing mode in desktop mode */
@@ -44,8 +48,26 @@
     private val windowManager: IWindowManager,
     private val shellTaskOrganizer: ShellTaskOrganizer,
     private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider,
+    private val inputManager: InputManager,
+    @ShellMainThread private val mainHandler: Handler,
 ) {
 
+    private val onTabletModeChangedListener =
+        object : InputManager.OnTabletModeChangedListener {
+            override fun onTabletModeChanged(whenNanos: Long, inTabletMode: Boolean) {
+                refreshDisplayWindowingMode()
+            }
+        }
+
+    init {
+        if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) {
+            inputManager.registerOnTabletModeChangedListener(
+                onTabletModeChangedListener,
+                mainHandler,
+            )
+        }
+    }
+
     fun refreshDisplayWindowingMode() {
         if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return
 
@@ -89,10 +111,20 @@
         transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
     }
 
-    private fun getTargetWindowingModeForDefaultDisplay(): Int {
+    @VisibleForTesting
+    fun getTargetWindowingModeForDefaultDisplay(): Int {
         if (isExtendedDisplayEnabled() && hasExternalDisplay()) {
             return WINDOWING_MODE_FREEFORM
         }
+        if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) {
+            if (isInClamshellMode()) {
+                return WINDOWING_MODE_FREEFORM
+            }
+            return WINDOWING_MODE_FULLSCREEN
+        }
+
+        // If form factor-based desktop first switch is disabled, use the default display windowing
+        // mode here to keep the freeform mode for some form factors (e.g., FEATURE_PC).
         return windowManager.getWindowingMode(DEFAULT_DISPLAY)
     }
 
@@ -108,6 +140,8 @@
     private fun hasExternalDisplay() =
         rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY }
 
+    private fun isInClamshellMode() = inputManager.isInTabletMode() == InputManager.SWITCH_STATE_OFF
+
     private fun logV(msg: String, vararg arguments: Any?) {
         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
index 5269318..1ea545f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
@@ -56,7 +56,6 @@
 
     override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?): Boolean {
         if (
-            !isKeyGestureSupported(event.keyGestureType) ||
                 !desktopTasksController.isPresent ||
                 !desktopModeWindowDecorViewModel.isPresent
         ) {
@@ -136,19 +135,6 @@
         }
     }
 
-    override fun isKeyGestureSupported(gestureType: Int): Boolean =
-        when (gestureType) {
-            KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY ->
-                enableMoveToNextDisplayShortcut()
-            KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW,
-            KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW,
-            KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW,
-            KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW ->
-                DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue &&
-                    manageKeyGestures()
-            else -> false
-        }
-
     //  TODO: b/364154795 - wait for the completion of moveToNextDisplay transition, otherwise it
     //  will pick a wrong task when a user quickly perform other actions with keyboard shortcuts
     //  after moveToNextDisplay, and move this to FocusTransitionObserver class.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index c370c0c..75c09829 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -93,7 +93,6 @@
 import android.app.ActivityOptions;
 import android.app.IActivityTaskManager;
 import android.app.PendingIntent;
-import android.app.PictureInPictureParams;
 import android.app.TaskInfo;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
@@ -2249,10 +2248,10 @@
 
         setRootForceTranslucent(true, wct);
         if (!enableFlexibleSplit()) {
-            //TODO(b/373709676) Need to figure out how adjacentRoots work for flex split
+            // TODO: consider support 3 splits
 
             // Make the stages adjacent to each other so they occlude what's behind them.
-            wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
+            wct.setAdjacentRootSet(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
             mSplitLayout.getInvisibleBounds(mTempRect1);
             wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
         }
@@ -2263,7 +2262,7 @@
             });
             mLaunchAdjacentController.setLaunchAdjacentRoot(mSideStage.mRootTaskInfo.token);
         } else {
-            // TODO(b/373709676) Need to figure out how adjacentRoots work for flex split
+            // TODO: consider support 3 splits
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 673c3f6..3f0d931 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -1096,8 +1096,8 @@
         return Resources.ID_NULL;
     }
 
-    private PointF calculateMaximizeMenuPosition(int menuWidth, int menuHeight) {
-        final PointF position = new PointF();
+    private Point calculateMaximizeMenuPosition(int menuWidth, int menuHeight) {
+        final Point position = new Point();
         final Resources resources = mContext.getResources();
         final DisplayLayout displayLayout =
                 mDisplayController.getDisplayLayout(mTaskInfo.displayId);
@@ -1112,11 +1112,11 @@
         final int[] maximizeButtonLocation = new int[2];
         maximizeWindowButton.getLocationInWindow(maximizeButtonLocation);
 
-        float menuLeft = (mPositionInParent.x + maximizeButtonLocation[0] - ((float) (menuWidth
-                - maximizeWindowButton.getWidth()) / 2));
-        float menuTop = (mPositionInParent.y + captionHeight);
-        final float menuRight = menuLeft + menuWidth;
-        final float menuBottom = menuTop + menuHeight;
+        int menuLeft = (mPositionInParent.x + maximizeButtonLocation[0] - (menuWidth
+                - maximizeWindowButton.getWidth()) / 2);
+        int menuTop = (mPositionInParent.y + captionHeight);
+        final int menuRight = menuLeft + menuWidth;
+        final int menuBottom = menuTop + menuHeight;
 
         // If the menu is out of screen bounds, shift it as needed
         if (menuLeft < 0) {
@@ -1128,7 +1128,7 @@
             menuTop = (displayHeight - menuHeight);
         }
 
-        return new PointF(menuLeft, menuTop);
+        return new Point(menuLeft, menuTop);
     }
 
     boolean isHandleMenuActive() {
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 ad3525a..5d1a7a0 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
@@ -26,7 +26,7 @@
 import android.content.res.Resources
 import android.graphics.Paint
 import android.graphics.PixelFormat
-import android.graphics.PointF
+import android.graphics.Point
 import android.graphics.Rect
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.GradientDrawable
@@ -90,7 +90,7 @@
         private val displayController: DisplayController,
         private val taskInfo: RunningTaskInfo,
         private val decorWindowContext: Context,
-        private val positionSupplier: (Int, Int) -> PointF,
+        private val positionSupplier: (Int, Int) -> Point,
         private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() }
 ) {
     private var maximizeMenu: AdditionalViewHostViewContainer? = null
@@ -100,14 +100,14 @@
     private val cornerRadius = loadDimensionPixelSize(
             R.dimen.desktop_mode_maximize_menu_corner_radius
     ).toFloat()
-    private lateinit var menuPosition: PointF
+    private lateinit var menuPosition: Point
     private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding)
 
     /** Position the menu relative to the caption's position. */
     fun positionMenu(t: Transaction) {
         menuPosition = positionSupplier(maximizeMenuView?.measureWidth() ?: 0,
                                         maximizeMenuView?.measureHeight() ?: 0)
-        t.setPosition(leash, menuPosition.x, menuPosition.y)
+        t.setPosition(leash, menuPosition.x.toFloat(), menuPosition.y.toFloat())
     }
 
     /** Creates and shows the maximize window. */
@@ -208,8 +208,8 @@
             val menuHeight = menuView.measureHeight()
             menuPosition = positionSupplier(menuWidth, menuHeight)
             val lp = WindowManager.LayoutParams(
-                    menuWidth.toInt(),
-                    menuHeight.toInt(),
+                    menuWidth,
+                    menuHeight,
                     WindowManager.LayoutParams.TYPE_APPLICATION,
                     WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                             or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
@@ -222,7 +222,7 @@
 
         // Bring menu to front when open
         t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU)
-                .setPosition(leash, menuPosition.x, menuPosition.y)
+                .setPosition(leash, menuPosition.x.toFloat(), menuPosition.y.toFloat())
                 .setCornerRadius(leash, cornerRadius)
                 .show(leash)
         maximizeMenu =
@@ -1047,7 +1047,7 @@
         displayController: DisplayController,
         taskInfo: RunningTaskInfo,
         decorWindowContext: Context,
-        positionSupplier: (Int, Int) -> PointF,
+        positionSupplier: (Int, Int) -> Point,
         transactionSupplier: Supplier<Transaction>
     ): MaximizeMenu
 }
@@ -1060,7 +1060,7 @@
         displayController: DisplayController,
         taskInfo: RunningTaskInfo,
         decorWindowContext: Context,
-        positionSupplier: (Int, Int) -> PointF,
+        positionSupplier: (Int, Int) -> Point,
         transactionSupplier: Supplier<Transaction>
     ): MaximizeMenu {
         return MaximizeMenu(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
index c3d15df..9c55f0e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
@@ -17,6 +17,7 @@
 package com.android.wm.shell.windowdecor.tiling
 
 import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
 import android.content.Context
 import android.content.res.Configuration
 import android.content.res.Resources
@@ -28,9 +29,11 @@
 import android.view.SurfaceControl.Transaction
 import android.view.WindowManager.TRANSIT_CHANGE
 import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_PIP
 import android.view.WindowManager.TRANSIT_TO_BACK
 import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
 import android.window.TransitionRequestInfo
 import android.window.WindowContainerTransaction
 import com.android.internal.annotations.VisibleForTesting
@@ -422,6 +425,8 @@
             change.taskInfo?.let {
                 if (it.isFullscreen || isMinimized(change.mode, info.type)) {
                     removeTaskIfTiled(it.taskId, /* taskVanished= */ false, it.isFullscreen)
+                } else if (isEnteringPip(change, info.type)) {
+                    removeTaskIfTiled(it.taskId, /* taskVanished= */ true, it.isFullscreen)
                 }
             }
         }
@@ -434,6 +439,27 @@
                 infoType == TRANSIT_OPEN))
     }
 
+    private fun isEnteringPip(change: Change, transitType: Int): Boolean {
+        if (change.taskInfo != null && change.taskInfo?.windowingMode == WINDOWING_MODE_PINNED) {
+            // - TRANSIT_PIP: type (from RootWindowContainer)
+            // - TRANSIT_OPEN (from apps that enter PiP instantly on opening, mostly from
+            // CTS/Flicker tests).
+            // - TRANSIT_TO_FRONT, though uncommon with triggering PiP, should semantically also
+            // be allowed to animate if the task in question is pinned already - see b/308054074.
+            // - TRANSIT_CHANGE: This can happen if the request to enter PIP happens when we are
+            // collecting for another transition, such as TRANSIT_CHANGE (display rotation).
+            if (
+                transitType == TRANSIT_PIP ||
+                    transitType == TRANSIT_OPEN ||
+                    transitType == TRANSIT_TO_FRONT ||
+                    transitType == TRANSIT_CHANGE
+            ) {
+                return true
+            }
+        }
+        return false
+    }
+
     class AppResizingHelper(
         val taskInfo: RunningTaskInfo,
         val desktopModeWindowDecoration: DesktopModeWindowDecoration,
@@ -550,12 +576,16 @@
         taskVanished: Boolean = false,
         shouldDelayUpdate: Boolean = false,
     ) {
+        val taskRepository = desktopUserRepositories.current
         if (taskId == leftTaskResizingHelper?.taskInfo?.taskId) {
             removeTask(leftTaskResizingHelper, taskVanished, shouldDelayUpdate)
             leftTaskResizingHelper = null
-            rightTaskResizingHelper
-                ?.desktopModeWindowDecoration
-                ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+            val taskId = rightTaskResizingHelper?.taskInfo?.taskId
+            if (taskId != null && taskRepository.isVisibleTask(taskId)) {
+                rightTaskResizingHelper
+                    ?.desktopModeWindowDecoration
+                    ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+            }
             tearDownTiling()
             return
         }
@@ -563,9 +593,12 @@
         if (taskId == rightTaskResizingHelper?.taskInfo?.taskId) {
             removeTask(rightTaskResizingHelper, taskVanished, shouldDelayUpdate)
             rightTaskResizingHelper = null
-            leftTaskResizingHelper
-                ?.desktopModeWindowDecoration
-                ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+            val taskId = leftTaskResizingHelper?.taskInfo?.taskId
+            if (taskId != null && taskRepository.isVisibleTask(taskId)) {
+                leftTaskResizingHelper
+                    ?.desktopModeWindowDecoration
+                    ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+            }
             tearDownTiling()
         }
     }
@@ -600,7 +633,6 @@
 
     fun onOverviewAnimationStateChange(isRunning: Boolean) {
         if (!isTilingManagerInitialised) return
-
         if (isRunning) {
             desktopTilingDividerWindowManager?.hideDividerBar()
         } else if (allTiledTasksVisible()) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
index cc37c44..450989d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
@@ -21,9 +21,12 @@
 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
 import android.content.ContentResolver
+import android.hardware.input.InputManager
 import android.os.Binder
+import android.os.Handler
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
 import android.provider.Settings
 import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
 import android.view.Display.DEFAULT_DISPLAY
@@ -44,6 +47,7 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.testing.junit.testparameterinjector.TestParameter
 import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -64,13 +68,18 @@
  */
 @SmallTest
 @RunWith(TestParameterInjector::class)
-class DesktopDisplayModeControllerTest : ShellTestCase() {
+class DesktopDisplayModeControllerTest(
+    @TestParameter(valuesProvider = FlagsParameterizationProvider::class)
+    flags: FlagsParameterization
+) : ShellTestCase() {
     private val transitions = mock<Transitions>()
     private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>()
     private val mockWindowManager = mock<IWindowManager>()
     private val shellTaskOrganizer = mock<ShellTaskOrganizer>()
     private val desktopWallpaperActivityTokenProvider =
         mock<DesktopWallpaperActivityTokenProvider>()
+    private val inputManager = mock<InputManager>()
+    private val mainHandler = mock<Handler>()
 
     private lateinit var controller: DesktopDisplayModeController
 
@@ -82,6 +91,10 @@
     private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
     private val wallpaperToken = MockToken().token()
 
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
     @Before
     fun setUp() {
         whenever(transitions.startTransition(anyInt(), any(), isNull())).thenReturn(Binder())
@@ -95,27 +108,20 @@
                 mockWindowManager,
                 shellTaskOrganizer,
                 desktopWallpaperActivityTokenProvider,
+                inputManager,
+                mainHandler,
             )
         runningTasks.add(freeformTask)
         runningTasks.add(fullscreenTask)
         whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(ArrayList(runningTasks))
         whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken)
+        setTabletModeStatus(SwitchState.UNKNOWN)
     }
 
-    private fun testDisplayWindowingModeSwitch(
-        defaultWindowingMode: Int,
-        extendedDisplayEnabled: Boolean,
-        expectToSwitch: Boolean,
-    ) {
-        defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode
-        whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode)
-        val settingsSession =
-            ExtendedDisplaySettingsSession(
-                context.contentResolver,
-                if (extendedDisplayEnabled) 1 else 0,
-            )
-
-        settingsSession.use {
+    private fun testDisplayWindowingModeSwitchOnDisplayConnected(expectToSwitch: Boolean) {
+        defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+        whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(WINDOWING_MODE_FULLSCREEN)
+        ExtendedDisplaySettingsSession(context.contentResolver, 1).use {
             connectExternalDisplay()
             if (expectToSwitch) {
                 // Assumes [connectExternalDisplay] properly triggered the switching transition.
@@ -133,7 +139,7 @@
                 assertThat(arg.firstValue.changes[wallpaperToken.asBinder()]?.windowingMode)
                     .isEqualTo(WINDOWING_MODE_FULLSCREEN)
                 assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode)
-                    .isEqualTo(defaultWindowingMode)
+                    .isEqualTo(WINDOWING_MODE_FULLSCREEN)
                 assertThat(arg.secondValue.changes[wallpaperToken.asBinder()]?.windowingMode)
                     .isEqualTo(WINDOWING_MODE_FULLSCREEN)
             } else {
@@ -144,25 +150,64 @@
 
     @Test
     @DisableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING)
-    fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled(
-        @TestParameter param: ModeSwitchTestCase
-    ) {
-        testDisplayWindowingModeSwitch(
-            param.defaultWindowingMode,
-            param.extendedDisplayEnabled,
-            // When the flag is disabled, never switch.
-            expectToSwitch = false,
-        )
+    fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled() {
+        // When the flag is disabled, never switch.
+        testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ false)
     }
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING)
-    fun displayWindowingModeSwitchOnDisplayConnected(@TestParameter param: ModeSwitchTestCase) {
-        testDisplayWindowingModeSwitch(
-            param.defaultWindowingMode,
-            param.extendedDisplayEnabled,
-            param.expectToSwitchByDefault,
-        )
+    fun displayWindowingModeSwitchOnDisplayConnected() {
+        testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ true)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING)
+    @DisableFlags(Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH)
+    fun testTargetWindowingMode_formfactorDisabled(
+        @TestParameter param: ExternalDisplayBasedTargetModeTestCase,
+        @TestParameter tabletModeStatus: SwitchState,
+    ) {
+        whenever(mockWindowManager.getWindowingMode(anyInt()))
+            .thenReturn(param.defaultWindowingMode)
+        if (param.hasExternalDisplay) {
+            connectExternalDisplay()
+        } else {
+            disconnectExternalDisplay()
+        }
+        setTabletModeStatus(tabletModeStatus)
+
+        ExtendedDisplaySettingsSession(
+                context.contentResolver,
+                if (param.extendedDisplayEnabled) 1 else 0,
+            )
+            .use {
+                assertThat(controller.getTargetWindowingModeForDefaultDisplay())
+                    .isEqualTo(param.expectedWindowingMode)
+            }
+    }
+
+    @Test
+    @EnableFlags(
+        Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING,
+        Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH,
+    )
+    fun testTargetWindowingMode(@TestParameter param: FormFactorBasedTargetModeTestCase) {
+        if (param.hasExternalDisplay) {
+            connectExternalDisplay()
+        } else {
+            disconnectExternalDisplay()
+        }
+        setTabletModeStatus(param.tabletModeStatus)
+
+        ExtendedDisplaySettingsSession(
+                context.contentResolver,
+                if (param.extendedDisplayEnabled) 1 else 0,
+            )
+            .use {
+                assertThat(controller.getTargetWindowingModeForDefaultDisplay())
+                    .isEqualTo(param.expectedWindowingMode)
+            }
     }
 
     @Test
@@ -217,6 +262,10 @@
         controller.refreshDisplayWindowingMode()
     }
 
+    private fun setTabletModeStatus(status: SwitchState) {
+        whenever(inputManager.isInTabletMode()).thenReturn(status.value)
+    }
+
     private class ExtendedDisplaySettingsSession(
         private val contentResolver: ContentResolver,
         private val overrideValue: Int,
@@ -233,33 +282,158 @@
         }
     }
 
+    private class FlagsParameterizationProvider : TestParameterValuesProvider() {
+        override fun provideValues(
+            context: TestParameterValuesProvider.Context
+        ): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf(
+                Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH
+            )
+        }
+    }
+
     companion object {
         const val EXTERNAL_DISPLAY_ID = 100
 
-        enum class ModeSwitchTestCase(
+        enum class SwitchState(val value: Int) {
+            UNKNOWN(InputManager.SWITCH_STATE_UNKNOWN),
+            ON(InputManager.SWITCH_STATE_ON),
+            OFF(InputManager.SWITCH_STATE_OFF),
+        }
+
+        enum class ExternalDisplayBasedTargetModeTestCase(
             val defaultWindowingMode: Int,
+            val hasExternalDisplay: Boolean,
             val extendedDisplayEnabled: Boolean,
-            val expectToSwitchByDefault: Boolean,
+            val expectedWindowingMode: Int,
         ) {
-            FULLSCREEN_DISPLAY(
-                defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
-                extendedDisplayEnabled = true,
-                expectToSwitchByDefault = true,
-            ),
-            FULLSCREEN_DISPLAY_MIRRORING(
-                defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
-                extendedDisplayEnabled = false,
-                expectToSwitchByDefault = false,
-            ),
-            FREEFORM_DISPLAY(
+            FREEFORM_EXTERNAL_EXTENDED(
                 defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+                hasExternalDisplay = true,
                 extendedDisplayEnabled = true,
-                expectToSwitchByDefault = false,
+                expectedWindowingMode = WINDOWING_MODE_FREEFORM,
             ),
-            FREEFORM_DISPLAY_MIRRORING(
+            FULLSCREEN_EXTERNAL_EXTENDED(
+                defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+                hasExternalDisplay = true,
+                extendedDisplayEnabled = true,
+                expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+            ),
+            FREEFORM_NO_EXTERNAL_EXTENDED(
                 defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+                hasExternalDisplay = false,
+                extendedDisplayEnabled = true,
+                expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+            ),
+            FULLSCREEN_NO_EXTERNAL_EXTENDED(
+                defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+                hasExternalDisplay = false,
+                extendedDisplayEnabled = true,
+                expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+            ),
+            FREEFORM_EXTERNAL_MIRROR(
+                defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+                hasExternalDisplay = true,
                 extendedDisplayEnabled = false,
-                expectToSwitchByDefault = false,
+                expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+            ),
+            FULLSCREEN_EXTERNAL_MIRROR(
+                defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+                hasExternalDisplay = true,
+                extendedDisplayEnabled = false,
+                expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+            ),
+            FREEFORM_NO_EXTERNAL_MIRROR(
+                defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+                hasExternalDisplay = false,
+                extendedDisplayEnabled = false,
+                expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+            ),
+            FULLSCREEN_NO_EXTERNAL_MIRROR(
+                defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+                hasExternalDisplay = false,
+                extendedDisplayEnabled = false,
+                expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+            ),
+        }
+
+        enum class FormFactorBasedTargetModeTestCase(
+            val hasExternalDisplay: Boolean,
+            val extendedDisplayEnabled: Boolean,
+            val tabletModeStatus: SwitchState,
+            val expectedWindowingMode: Int,
+        ) {
+            EXTERNAL_EXTENDED_TABLET(
+                hasExternalDisplay = true,
+                extendedDisplayEnabled = true,
+                tabletModeStatus = SwitchState.ON,
+                expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+            ),
+            NO_EXTERNAL_EXTENDED_TABLET(
+                hasExternalDisplay = false,
+                extendedDisplayEnabled = true,
+                tabletModeStatus = SwitchState.ON,
+                expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+            ),
+            EXTERNAL_MIRROR_TABLET(
+                hasExternalDisplay = true,
+                extendedDisplayEnabled = false,
+                tabletModeStatus = SwitchState.ON,
+                expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+            ),
+            NO_EXTERNAL_MIRROR_TABLET(
+                hasExternalDisplay = false,
+                extendedDisplayEnabled = false,
+                tabletModeStatus = SwitchState.ON,
+                expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+            ),
+            EXTERNAL_EXTENDED_CLAMSHELL(
+                hasExternalDisplay = true,
+                extendedDisplayEnabled = true,
+                tabletModeStatus = SwitchState.OFF,
+                expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+            ),
+            NO_EXTERNAL_EXTENDED_CLAMSHELL(
+                hasExternalDisplay = false,
+                extendedDisplayEnabled = true,
+                tabletModeStatus = SwitchState.OFF,
+                expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+            ),
+            EXTERNAL_MIRROR_CLAMSHELL(
+                hasExternalDisplay = true,
+                extendedDisplayEnabled = false,
+                tabletModeStatus = SwitchState.OFF,
+                expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+            ),
+            NO_EXTERNAL_MIRROR_CLAMSHELL(
+                hasExternalDisplay = false,
+                extendedDisplayEnabled = false,
+                tabletModeStatus = SwitchState.OFF,
+                expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+            ),
+            EXTERNAL_EXTENDED_UNKNOWN(
+                hasExternalDisplay = true,
+                extendedDisplayEnabled = true,
+                tabletModeStatus = SwitchState.UNKNOWN,
+                expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+            ),
+            NO_EXTERNAL_EXTENDED_UNKNOWN(
+                hasExternalDisplay = false,
+                extendedDisplayEnabled = true,
+                tabletModeStatus = SwitchState.UNKNOWN,
+                expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+            ),
+            EXTERNAL_MIRROR_UNKNOWN(
+                hasExternalDisplay = true,
+                extendedDisplayEnabled = false,
+                tabletModeStatus = SwitchState.UNKNOWN,
+                expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+            ),
+            NO_EXTERNAL_MIRROR_UNKNOWN(
+                hasExternalDisplay = false,
+                extendedDisplayEnabled = false,
+                tabletModeStatus = SwitchState.UNKNOWN,
+                expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
             ),
         }
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
index c40a04c..b511fc3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
@@ -22,6 +22,7 @@
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
 import android.content.ComponentName
 import android.graphics.Rect
 import android.view.Display.DEFAULT_DISPLAY
@@ -45,6 +46,17 @@
             .apply { bounds?.let { setBounds(it) } }
             .build()
 
+    fun createPinnedTask(displayId: Int = DEFAULT_DISPLAY, bounds: Rect? = null): RunningTaskInfo =
+        TestRunningTaskInfoBuilder()
+            .setDisplayId(displayId)
+            .setParentTaskId(displayId)
+            .setToken(MockToken().token())
+            .setActivityType(ACTIVITY_TYPE_STANDARD)
+            .setWindowingMode(WINDOWING_MODE_PINNED)
+            .setLastActiveTime(100)
+            .apply { bounds?.let { setBounds(it) } }
+            .build()
+
     fun createFullscreenTaskBuilder(displayId: Int = DEFAULT_DISPLAY): TestRunningTaskInfoBuilder =
         TestRunningTaskInfoBuilder()
             .setDisplayId(displayId)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/RemoteTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/RemoteTransitionHandlerTest.kt
new file mode 100644
index 0000000..048981d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/RemoteTransitionHandlerTest.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.WindowManager
+import android.window.RemoteTransition
+import android.window.TransitionFilter
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestSyncExecutor
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+/**
+ * Test class for [RemoteTransitionHandler].
+ *
+ * atest WMShellUnitTests:RemoteTransitionHandlerTest
+ */
+@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class RemoteTransitionHandlerTest : ShellTestCase() {
+
+    private val testExecutor: TestSyncExecutor = TestSyncExecutor()
+
+    private val testRemoteTransition = RemoteTransition(TestRemoteTransition())
+    private lateinit var handler: RemoteTransitionHandler
+
+    @Before
+    fun setUp() {
+        handler = RemoteTransitionHandler(testExecutor)
+    }
+
+    @Test
+    fun handleRequest_noRemoteTransition_returnsNull() {
+        val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, null)
+
+        assertNull(handler.handleRequest(mock(), request))
+    }
+
+    @Test
+    fun handleRequest_testRemoteTransition_returnsWindowContainerTransaction() {
+        val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, testRemoteTransition)
+
+        assertTrue(handler.handleRequest(mock(), request) is WindowContainerTransaction)
+    }
+
+    @Test
+    fun startAnimation_noRemoteTransition_returnsFalse() {
+        val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, null)
+        handler.handleRequest(mock(), request)
+
+        val isHandled = handler.startAnimation(
+            /* transition= */ mock(),
+            /* info= */ createTransitionInfo(),
+            /* startTransaction= */ mock(),
+            /* finishTransaction= */ mock(),
+            /* finishCallback= */ {},
+        )
+
+        assertFalse(isHandled)
+    }
+
+    @Test
+    fun startAnimation_remoteTransition_returnsTrue() {
+        val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, testRemoteTransition)
+        handler.addFiltered(TransitionFilter(), testRemoteTransition)
+        handler.handleRequest(mock(), request)
+
+        val isHandled = handler.startAnimation(
+            /* transition= */ testRemoteTransition.remoteTransition.asBinder(),
+            /* info= */ createTransitionInfo(),
+            /* startTransaction= */ mock(),
+            /* finishTransaction= */ mock(),
+            /* finishCallback= */ {},
+        )
+
+        assertTrue(isHandled)
+    }
+
+    private fun createTransitionInfo(
+        type: Int = WindowManager.TRANSIT_OPEN,
+        changeMode: Int = WindowManager.TRANSIT_CLOSE,
+    ): TransitionInfo =
+        TransitionInfo(type, /* flags= */ 0).apply {
+            addChange(
+                TransitionInfo.Change(mock(), mock()).apply {
+                    mode = changeMode
+                    parent = null
+                }
+            )
+        }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 1371b38..8783249 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -60,7 +60,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.TypedArray;
-import android.graphics.PointF;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.net.Uri;
@@ -1805,7 +1805,7 @@
                 @NonNull DisplayController displayController,
                 @NonNull ActivityManager.RunningTaskInfo taskInfo,
                 @NonNull Context decorWindowContext,
-                @NonNull Function2<? super Integer,? super Integer,? extends PointF>
+                @NonNull Function2<? super Integer,? super Integer,? extends Point>
                     positionSupplier,
                 @NonNull Supplier<SurfaceControl.Transaction> transactionSupplier) {
             return mMaximizeMenu;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
index bc8faed..e4424f3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
@@ -16,6 +16,7 @@
 package com.android.wm.shell.windowdecor.tiling
 
 import android.app.ActivityManager
+import android.app.ActivityManager.RunningTaskInfo
 import android.content.Context
 import android.content.res.Resources
 import android.graphics.Rect
@@ -24,8 +25,10 @@
 import android.view.MotionEvent
 import android.view.SurfaceControl
 import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_PIP
 import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
 import android.window.WindowContainerTransaction
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
@@ -40,6 +43,7 @@
 import com.android.wm.shell.desktopmode.DesktopRepository
 import com.android.wm.shell.desktopmode.DesktopTasksController
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.createPinnedTask
 import com.android.wm.shell.desktopmode.DesktopUserRepositories
 import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
 import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
@@ -552,6 +556,37 @@
     }
 
     @Test
+    fun taskTiled_shouldBeRemoved_whenEnteringPip() {
+        val task1 = createPipTask()
+        val stableBounds = STABLE_BOUNDS_MOCK
+        whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+        whenever(tiledTaskHelper.taskInfo).thenReturn(task1)
+        whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+        tilingDecoration.onAppTiled(
+            task1,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.LEFT,
+            BOUNDS,
+        )
+        tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+        val changeInfo = createPipChangeTransition(task1)
+        tilingDecoration.onTransitionReady(
+            transition = mock(),
+            info = changeInfo,
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        assertThat(tilingDecoration.leftTaskResizingHelper).isNull()
+        verify(tiledTaskHelper, times(1)).dispose()
+    }
+
+    @Test
     fun taskNotTiled_shouldNotBeRemoved_whenNotTiled() {
         val task1 = createVisibleTask()
         val task2 = createVisibleTask()
@@ -652,6 +687,23 @@
             whenever(userRepositories.current.isVisibleTask(eq(it.taskId))).thenReturn(true)
         }
 
+    private fun createPipTask() =
+        createPinnedTask().also {
+            whenever(userRepositories.current.isVisibleTask(eq(it.taskId))).thenReturn(true)
+        }
+
+    private fun createPipChangeTransition(task: RunningTaskInfo?, type: Int = TRANSIT_PIP) =
+        TransitionInfo(type, /* flags= */ 0).apply {
+            addChange(
+                Change(mock(), mock()).apply {
+                    mode = TRANSIT_PIP
+                    parent = null
+                    taskInfo = task
+                    flags = flags
+                }
+            )
+        }
+
     companion object {
         private val NON_STABLE_BOUNDS_MOCK = Rect(50, 55, 100, 100)
         private val STABLE_BOUNDS_MOCK = Rect(0, 0, 100, 100)
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index a18c5f5..8ecd6ba 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -6520,41 +6520,79 @@
 }
 
 bool ResTable::getResourceFlags(uint32_t resID, uint32_t* outFlags) const {
-    if (mError != NO_ERROR) {
-        return false;
-    }
+  if (mError != NO_ERROR) {
+    return false;
+  }
 
-    const ssize_t p = getResourcePackageIndex(resID);
-    const int t = Res_GETTYPE(resID);
-    const int e = Res_GETENTRY(resID);
+  const ssize_t p = getResourcePackageIndex(resID);
+  const int t = Res_GETTYPE(resID);
+  const int e = Res_GETENTRY(resID);
 
-    if (p < 0) {
-        if (Res_GETPACKAGE(resID)+1 == 0) {
-            ALOGW("No package identifier when getting flags for resource number 0x%08x", resID);
-        } else {
-            ALOGW("No known package when getting flags for resource number 0x%08x", resID);
-        }
-        return false;
+  if (p < 0) {
+    if (Res_GETPACKAGE(resID)+1 == 0) {
+      ALOGW("No package identifier when getting flags for resource number 0x%08x", resID);
+    } else {
+      ALOGW("No known package when getting flags for resource number 0x%08x", resID);
     }
-    if (t < 0) {
-        ALOGW("No type identifier when getting flags for resource number 0x%08x", resID);
-        return false;
-    }
+    return false;
+  }
+  if (t < 0) {
+    ALOGW("No type identifier when getting flags for resource number 0x%08x", resID);
+    return false;
+  }
 
-    const PackageGroup* const grp = mPackageGroups[p];
-    if (grp == NULL) {
-        ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID);
-        return false;
-    }
+  const PackageGroup* const grp = mPackageGroups[p];
+  if (grp == NULL) {
+    ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID);
+    return false;
+  }
 
-    Entry entry;
-    status_t err = getEntry(grp, t, e, NULL, &entry);
-    if (err != NO_ERROR) {
-        return false;
-    }
+  Entry entry;
+  status_t err = getEntry(grp, t, e, NULL, &entry);
+  if (err != NO_ERROR) {
+    return false;
+  }
 
-    *outFlags = entry.specFlags;
-    return true;
+  *outFlags = entry.specFlags;
+  return true;
+}
+
+bool ResTable::getResourceEntryFlags(uint32_t resID, uint32_t* outFlags) const {
+  if (mError != NO_ERROR) {
+    return false;
+  }
+
+  const ssize_t p = getResourcePackageIndex(resID);
+  const int t = Res_GETTYPE(resID);
+  const int e = Res_GETENTRY(resID);
+
+  if (p < 0) {
+    if (Res_GETPACKAGE(resID)+1 == 0) {
+      ALOGW("No package identifier when getting flags for resource number 0x%08x", resID);
+    } else {
+      ALOGW("No known package when getting flags for resource number 0x%08x", resID);
+    }
+    return false;
+  }
+  if (t < 0) {
+    ALOGW("No type identifier when getting flags for resource number 0x%08x", resID);
+    return false;
+  }
+
+  const PackageGroup* const grp = mPackageGroups[p];
+  if (grp == NULL) {
+    ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID);
+    return false;
+  }
+
+  Entry entry;
+  status_t err = getEntry(grp, t, e, NULL, &entry);
+  if (err != NO_ERROR) {
+    return false;
+  }
+
+  *outFlags = entry.entry->flags();
+  return true;
 }
 
 bool ResTable::isPackageDynamic(uint8_t packageID) const {
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 0d45149..63b28da 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -1593,6 +1593,8 @@
         // If set, this is a compact entry with data type and value directly
         // encoded in the this entry, see ResTable_entry::compact
         FLAG_COMPACT = 0x0008,
+        // If set, this entry relies on read write android feature flags
+        FLAG_USES_FEATURE_FLAGS = 0x0010,
     };
 
     struct Full {
@@ -1622,6 +1624,7 @@
     uint16_t flags()  const { return dtohs(full.flags); };
     bool is_compact() const { return flags() & FLAG_COMPACT; }
     bool is_complex() const { return flags() & FLAG_COMPLEX; }
+    bool uses_feature_flags() const { return flags() & FLAG_USES_FEATURE_FLAGS; }
 
     size_t size() const {
         return is_compact() ? sizeof(ResTable_entry) : dtohs(this->full.size);
@@ -2039,6 +2042,8 @@
 
     bool getResourceFlags(uint32_t resID, uint32_t* outFlags) const;
 
+    bool getResourceEntryFlags(uint32_t resID, uint32_t* outFlags) const;
+
     /**
      * Returns whether or not the package for the given resource has been dynamically assigned.
      * If the resource can't be found, returns 'false'.
diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java
index e4de3e4..fccdba8 100644
--- a/media/java/android/media/quality/MediaQualityContract.java
+++ b/media/java/android/media/quality/MediaQualityContract.java
@@ -82,7 +82,7 @@
         String PARAMETER_NAME = "_name";
         String PARAMETER_PACKAGE = "_package";
         String PARAMETER_INPUT_ID = "_input_id";
-
+        String VENDOR_PARAMETERS = "_vendor_parameters";
     }
 
     /**
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index d55bbb3..bf73962 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -187,7 +187,9 @@
         if (mLottieDynamicColor) {
             LottieColorUtils.applyDynamicColors(getContext(), illustrationView);
         }
-        LottieColorUtils.applyMaterialColor(getContext(), illustrationView);
+        if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+            LottieColorUtils.applyMaterialColor(getContext(), illustrationView);
+        }
 
         if (mOnBindListener != null) {
             mOnBindListener.onBind(illustrationView);
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
index 4421424..e59cc81 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
@@ -157,10 +157,6 @@
     /** Applies material colors. */
     public static void applyMaterialColor(@NonNull Context context,
             @NonNull LottieAnimationView lottieAnimationView) {
-        if (!SettingsThemeHelper.isExpressiveTheme(context)) {
-            return;
-        }
-
         for (String key : MATERIAL_COLOR_MAP.keySet()) {
             final int color = context.getColor(MATERIAL_COLOR_MAP.get(key));
             lottieAnimationView.addValueCallback(
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java
index c5e6f60..2b95fd1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java
@@ -93,18 +93,10 @@
 
     @Override
     public boolean onTouchEvent(@Nullable MotionEvent event) {
-        onBackInvoked();
+        cancel();
         return super.onTouchEvent(event);
     }
 
-    private void onBackInvoked() {
-        if (mSetupUserDialog != null) {
-            mSetupUserDialog.dismiss();
-        }
-        setResult(RESULT_CANCELED);
-        finish();
-    }
-
     @VisibleForTesting
     void setSuccessResult(String userName, Drawable userIcon, String path, Boolean isAdmin) {
         Intent intent = new Intent(this, CreateUserActivity.class);
@@ -112,14 +104,12 @@
         intent.putExtra(EXTRA_IS_ADMIN, isAdmin);
         intent.putExtra(EXTRA_USER_ICON_PATH, path);
 
-        mSetupUserDialog.dismiss();
         setResult(RESULT_OK, intent);
         finish();
     }
 
     @VisibleForTesting
     void cancel() {
-        mSetupUserDialog.dismiss();
         setResult(RESULT_CANCELED);
         finish();
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
index d9f1b63..bce229f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
@@ -282,7 +282,7 @@
                 mCustomDialogHelper.getDialog().dismiss();
                 break;
             case EXIT_DIALOG:
-                finish();
+                mUserCreationDialog.dismiss();
                 break;
             default:
                 if (mCurrentState < EXIT_DIALOG) {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java
index f58eb7c..220c03e 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java
@@ -65,23 +65,23 @@
     }
 
     @Test
-    public void onTouchEvent_dismissesDialogAndCancelsResult() {
+    public void onTouchEvent_finishesActivityAndCancelsResult() {
         mCreateUserActivity.onTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0,
                 0));
 
-        assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse();
+        assertThat(mCreateUserActivity.isFinishing()).isTrue();
         assertThat(shadowOf(mCreateUserActivity).getResultCode())
                 .isEqualTo(Activity.RESULT_CANCELED);
     }
 
     @Test
-    public void setSuccessResult_dismissesDialogAndSetsSuccessResult() {
+    public void setSuccessResult_finishesActivityAndSetsSuccessResult() {
         Drawable mockDrawable = mock(Drawable.class);
 
         mCreateUserActivity.setSuccessResult(TEST_USER_NAME, mockDrawable, TEST_USER_ICON_PATH,
                 TEST_IS_ADMIN);
 
-        assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse();
+        assertThat(mCreateUserActivity.isFinishing()).isTrue();
         assertThat(shadowOf(mCreateUserActivity).getResultCode()).isEqualTo(Activity.RESULT_OK);
 
         Intent resultIntent = shadowOf(mCreateUserActivity).getResultIntent();
@@ -92,10 +92,10 @@
     }
 
     @Test
-    public void cancel_dismissesDialogAndSetsCancelResult() {
+    public void cancel_finishesActivityAndSetsCancelResult() {
         mCreateUserActivity.cancel();
 
-        assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse();
+        assertThat(mCreateUserActivity.isFinishing()).isTrue();
         assertThat(shadowOf(mCreateUserActivity).getResultCode())
                 .isEqualTo(Activity.RESULT_CANCELED);
     }
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 72ae76a..f628a42 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -312,6 +312,9 @@
     <!-- Permission necessary to change car audio volume through CarAudioManager -->
     <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME" />
 
+    <!-- To detect when projecting to Android Auto -->
+    <uses-permission android:name="android.permission.READ_PROJECTION_STATE" />
+
     <!-- Permission to control Android Debug Bridge (ADB) -->
     <uses-permission android:name="android.permission.MANAGE_DEBUGGING" />
 
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 3c86f28..9dd49d6 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1401,6 +1401,16 @@
 }
 
 flag {
+    name: "media_controls_device_manager_background_execution"
+    namespace: "systemui"
+    description: "Sends some instances creation to background thread"
+    bug: "400200474"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
   name: "output_switcher_redesign"
   namespace: "systemui"
   description: "Enables visual update for Media Output Switcher"
@@ -1843,6 +1853,16 @@
 }
 
 flag {
+  name: "disable_shade_visible_with_blur"
+  namespace: "systemui"
+  description: "Removes the check for a blur radius when determining shade window visibility"
+  bug: "356804470"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
     name: "notification_row_transparency"
     namespace: "systemui"
     description: "Enables transparency on the Notification Shade."
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index e43b8a0..7ee6a6e 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -107,6 +107,16 @@
      */
     // TODO(b/301385865): Remove this flag.
     private val disableWmTimeout: Boolean = false,
+
+    /**
+     * Whether we should disable the reparent transaction that puts the opening/closing window above
+     * the view's window. This should be set to true in tests only, where we can't currently use a
+     * valid leash.
+     *
+     * TODO(b/397180418): Remove this flag when we don't have the RemoteAnimation wrapper anymore
+     *   and we can just inject a fake transaction.
+     */
+    private val skipReparentTransaction: Boolean = false,
 ) {
     @JvmOverloads
     constructor(
@@ -1140,6 +1150,7 @@
                     DelegatingAnimationCompletionListener(listener, this::dispose),
                     transitionAnimator,
                     disableWmTimeout,
+                    skipReparentTransaction,
                 )
         }
 
@@ -1173,6 +1184,16 @@
          */
         // TODO(b/301385865): Remove this flag.
         disableWmTimeout: Boolean = false,
+
+        /**
+         * Whether we should disable the reparent transaction that puts the opening/closing window
+         * above the view's window. This should be set to true in tests only, where we can't
+         * currently use a valid leash.
+         *
+         * TODO(b/397180418): Remove this flag when we don't have the RemoteAnimation wrapper
+         *   anymore and we can just inject a fake transaction.
+         */
+        private val skipReparentTransaction: Boolean = false,
     ) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> {
         private val transitionContainer = controller.transitionContainer
         private val context = transitionContainer.context
@@ -1515,7 +1536,7 @@
                             )
                         }
 
-                        if (moveTransitionAnimationLayer()) {
+                        if (moveTransitionAnimationLayer() && !skipReparentTransaction) {
                             // Ensure that the launching window is rendered above the view's window,
                             // so it is not obstructed.
                             // TODO(b/397180418): re-use the start transaction once the
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
index 9a74687..e07d7b3 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
@@ -16,17 +16,18 @@
 
 package com.android.systemui.animation
 
+import kotlin.text.buildString
+
 class FontVariationUtils {
     private var mWeight = -1
     private var mWidth = -1
     private var mOpticalSize = -1
     private var mRoundness = -1
-    private var isUpdated = false
+    private var mCurrentFVar = ""
 
     /*
      * generate fontVariationSettings string, used for key in typefaceCache in TextAnimator
      * the order of axes should align to the order of parameters
-     * if every axis remains unchanged, return ""
      */
     fun updateFontVariation(
         weight: Int = -1,
@@ -34,15 +35,17 @@
         opticalSize: Int = -1,
         roundness: Int = -1,
     ): String {
-        isUpdated = false
+        var isUpdated = false
         if (weight >= 0 && mWeight != weight) {
             isUpdated = true
             mWeight = weight
         }
+
         if (width >= 0 && mWidth != width) {
             isUpdated = true
             mWidth = width
         }
+
         if (opticalSize >= 0 && mOpticalSize != opticalSize) {
             isUpdated = true
             mOpticalSize = opticalSize
@@ -52,23 +55,32 @@
             isUpdated = true
             mRoundness = roundness
         }
-        var resultString = ""
-        if (mWeight >= 0) {
-            resultString += "'${GSFAxes.WEIGHT.tag}' $mWeight"
+
+        if (!isUpdated) {
+            return mCurrentFVar
         }
-        if (mWidth >= 0) {
-            resultString +=
-                (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.WIDTH.tag}' $mWidth"
-        }
-        if (mOpticalSize >= 0) {
-            resultString +=
-                (if (resultString.isBlank()) "" else ", ") +
-                    "'${GSFAxes.OPTICAL_SIZE.tag}' $mOpticalSize"
-        }
-        if (mRoundness >= 0) {
-            resultString +=
-                (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.ROUND.tag}' $mRoundness"
-        }
-        return if (isUpdated) resultString else ""
+
+        return buildString {
+                if (mWeight >= 0) {
+                    if (!isBlank()) append(", ")
+                    append("'${GSFAxes.WEIGHT.tag}' $mWeight")
+                }
+
+                if (mWidth >= 0) {
+                    if (!isBlank()) append(", ")
+                    append("'${GSFAxes.WIDTH.tag}' $mWidth")
+                }
+
+                if (mOpticalSize >= 0) {
+                    if (!isBlank()) append(", ")
+                    append("'${GSFAxes.OPTICAL_SIZE.tag}' $mOpticalSize")
+                }
+
+                if (mRoundness >= 0) {
+                    if (!isBlank()) append(", ")
+                    append("'${GSFAxes.ROUND.tag}' $mRoundness")
+                }
+            }
+            .also { mCurrentFVar = it }
     }
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index 5d9c441..a4a96d1 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -1006,13 +1006,32 @@
             Log.d(TAG, "Animation ended")
         }
 
-        // TODO(b/330672236): Post this to the main thread instead so that it does not
-        // flicker with Flexiglass enabled.
-        controller.onTransitionAnimationEnd(isExpandingFullyAbove)
-        transitionContainerOverlay.remove(windowBackgroundLayer)
+        val onEnd = {
+            controller.onTransitionAnimationEnd(isExpandingFullyAbove)
+            transitionContainerOverlay.remove(windowBackgroundLayer)
 
-        if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
-            openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
+            if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
+                openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
+            }
+        }
+        // TODO(b/330672236): Post this to the main thread for launches as well, so that they do not
+        //   flicker with Flexiglass enabled.
+        if (controller.isLaunching) {
+            onEnd()
+        } else {
+            // onAnimationEnd is called at the end of the animation, on a Choreographer animation
+            // tick. During dialog launches, the following calls will move the animated content from
+            // the dialog overlay back to its original position, and this change must be reflected
+            // in the next frame given that we then sync the next frame of both the content and
+            // dialog ViewRoots. During SysUI activity launches, we will instantly collapse the
+            // shade at the end of the transition. However, if those are rendered by Compose, whose
+            // compositions are also scheduled on a Choreographer frame, any state change made
+            // *right now* won't be reflected in the next frame given that a Choreographer frame
+            // can't schedule another and have it happen in the same frame. So we post the forwarded
+            // calls to [Controller.onLaunchAnimationEnd] in the main executor, leaving this
+            // Choreographer frame, ensuring that any state change applied by
+            // onTransitionAnimationEnd() will be reflected in the same frame.
+            mainExecutor.execute { onEnd() }
         }
     }
 
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RunBlockingDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RunBlockingDetector.kt
new file mode 100644
index 0000000..fce536a
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RunBlockingDetector.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *            http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UFile
+import org.jetbrains.uast.UImportStatement
+
+/** Detects whether [runBlocking] is being imported. */
+class RunBlockingDetector : Detector(), SourceCodeScanner {
+
+    override fun getApplicableUastTypes(): List<Class<out UElement>> {
+        return listOf(UFile::class.java)
+    }
+
+    override fun createUastHandler(context: JavaContext): UElementHandler {
+        return object : UElementHandler() {
+            override fun visitFile(node: UFile) {
+                for (importStatement in node.imports) {
+                    visitImportStatement(context, importStatement)
+                }
+            }
+        }
+    }
+
+    private fun visitImportStatement(context: JavaContext, importStatement: UImportStatement) {
+        val importName = importStatement.importReference?.asSourceString()
+        if (FORBIDDEN_IMPORTS.contains(importName)) {
+            context.report(
+                ISSUE,
+                importStatement as UElement,
+                context.getLocation(importStatement),
+                "Importing $importName is not allowed.",
+            )
+        }
+    }
+
+    companion object {
+        @JvmField
+        val ISSUE =
+            Issue.create(
+                id = "RunBlockingUsage",
+                briefDescription = "Discouraged runBlocking call",
+                explanation =
+                    """
+                    Using `runBlocking` is generally discouraged in Android
+                    development as it can lead to UI freezes and ANRs.
+                    Consider using `launch` or `async` with coroutine scope
+                    instead. If needed from java, consider introducing a method
+                    with a callback instead from kotlin.
+                    """,
+                category = Category.PERFORMANCE,
+                priority = 8,
+                severity = Severity.WARNING,
+                implementation =
+                    Implementation(RunBlockingDetector::class.java, Scope.JAVA_FILE_SCOPE),
+            )
+
+        val FORBIDDEN_IMPORTS = listOf("kotlinx.coroutines.runBlocking")
+    }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index adb3116..b455c00 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -50,6 +50,7 @@
                 ShadeDisplayAwareDialogDetector.ISSUE,
                 RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING,
                 RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR,
+                RunBlockingDetector.ISSUE,
             )
 
     override val api: Int
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RunBlockingDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RunBlockingDetectorTest.kt
new file mode 100644
index 0000000..4ae429d2
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RunBlockingDetectorTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *            http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+class RunBlockingDetectorTest : SystemUILintDetectorTest() {
+
+    override fun getDetector(): Detector = RunBlockingDetector()
+
+    override fun getIssues(): List<Issue> = listOf(RunBlockingDetector.ISSUE)
+
+    @Test
+    fun testViolation() {
+        lint()
+            .files(
+                kotlin(
+                    """
+                    package com.example
+
+                    import kotlinx.coroutines.runBlocking
+
+                    class MyClass {
+                        fun myMethod() {
+                            runBlocking {
+                                // Some code here
+                            }
+                        }
+                    }
+                    """
+                ),
+                RUN_BLOCKING_DEFINITION,
+            )
+            .issues(RunBlockingDetector.ISSUE)
+            .run()
+            .expect(
+                """
+src/com/example/MyClass.kt:4: Warning: Importing kotlinx.coroutines.runBlocking is not allowed. [RunBlockingUsage]
+                    import kotlinx.coroutines.runBlocking
+                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+"""
+                    .trimIndent()
+            )
+    }
+
+    // Verifies that the lint check does *not* flag calls to other methods.
+    @Test
+    fun testNotViolation() {
+        lint()
+            .detector(RunBlockingDetector())
+            .issues(RunBlockingDetector.ISSUE)
+            .files(
+                kotlin(
+                    """
+                    package com.example
+
+                    class MyClass {
+                        fun myMethod() {
+                            myOtherMethod {
+                            }
+                        }
+
+                        fun myOtherMethod(block: () -> Unit) {
+                            block()
+                        }
+                    }
+                    """
+                )
+            )
+            .run()
+            .expectClean()
+    }
+
+    private companion object {
+        val RUN_BLOCKING_DEFINITION =
+            kotlin(
+                """
+                    package kotlinx.coroutines
+
+                    fun runBlocking(block: suspend () -> Unit) {
+                        // Implementation details don't matter for this test.
+                    }
+                    """
+            )
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 4bf0ceb..6e29e69 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -38,8 +38,9 @@
 import com.android.systemui.customization.R
 import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.core.LogcatOnlyMessageBuffer
-import com.android.systemui.log.core.Logger
 import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.ClockLogger
+import com.android.systemui.plugins.clocks.ClockLogger.Companion.escapeTime
 import java.io.PrintWriter
 import java.util.Calendar
 import java.util.Locale
@@ -67,7 +68,7 @@
     var messageBuffer: MessageBuffer
         get() = logger.buffer
         set(value) {
-            logger = Logger(value, TAG)
+            logger = ClockLogger(this, value, TAG)
         }
 
     var hasCustomPositionUpdatedAnimation: Boolean = false
@@ -185,7 +186,9 @@
         time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis()
         contentDescription = DateFormat.format(descFormat, time)
         val formattedText = DateFormat.format(format, time)
-        logger.d({ "refreshTime: new formattedText=$str1" }) { str1 = formattedText?.toString() }
+        logger.d({ "refreshTime: new formattedText=${escapeTime(str1)}" }) {
+            str1 = formattedText?.toString()
+        }
 
         // Setting text actually triggers a layout pass in TextView (because the text view is set to
         // wrap_content width and TextView always relayouts for this). This avoids needless relayout
@@ -195,7 +198,7 @@
         }
 
         text = formattedText
-        logger.d({ "refreshTime: done setting new time text to: $str1" }) {
+        logger.d({ "refreshTime: done setting new time text to: ${escapeTime(str1)}" }) {
             str1 = formattedText?.toString()
         }
 
@@ -225,7 +228,7 @@
 
     @SuppressLint("DrawAllocation")
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
-        logger.d("onMeasure")
+        logger.onMeasure(widthMeasureSpec, heightMeasureSpec)
 
         if (!isSingleLineInternal && MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
             // Call straight into TextView.setTextSize to avoid setting lastUnconstrainedTextSize
@@ -263,14 +266,14 @@
             canvas.translate(parentWidth / 4f, 0f)
         }
 
-        logger.d({ "onDraw($str1)" }) { str1 = text.toString() }
+        logger.onDraw("$text")
         // intentionally doesn't call super.onDraw here or else the text will be rendered twice
         textAnimator?.draw(canvas)
         canvas.restore()
     }
 
     override fun invalidate() {
-        logger.d("invalidate")
+        logger.invalidate()
         super.invalidate()
     }
 
@@ -280,7 +283,7 @@
         lengthBefore: Int,
         lengthAfter: Int,
     ) {
-        logger.d({ "onTextChanged($str1)" }) { str1 = text.toString() }
+        logger.d({ "onTextChanged(${escapeTime(str1)})" }) { str1 = "$text" }
         super.onTextChanged(text, start, lengthBefore, lengthAfter)
     }
 
@@ -370,7 +373,7 @@
             return
         }
 
-        logger.d("animateCharge")
+        logger.animateCharge()
         val startAnimPhase2 = Runnable {
             setTextStyle(
                 weight = if (isDozing()) dozingWeight else lockScreenWeight,
@@ -394,7 +397,7 @@
     }
 
     fun animateDoze(isDozing: Boolean, animate: Boolean) {
-        logger.d("animateDoze")
+        logger.animateDoze(isDozing, animate)
         setTextStyle(
             weight = if (isDozing) dozingWeight else lockScreenWeight,
             color = if (isDozing) dozingColor else lockScreenColor,
@@ -484,7 +487,7 @@
                 isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
                 else -> DOUBLE_LINE_FORMAT_12_HOUR
             }
-        logger.d({ "refreshFormat($str1)" }) { str1 = format?.toString() }
+        logger.d({ "refreshFormat(${escapeTime(str1)})" }) { str1 = format?.toString() }
 
         descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
         refreshTime()
@@ -634,7 +637,7 @@
 
     companion object {
         private val TAG = AnimatableClockView::class.simpleName!!
-        private val DEFAULT_LOGGER = Logger(LogcatOnlyMessageBuffer(LogLevel.WARNING), TAG)
+        private val DEFAULT_LOGGER = ClockLogger(null, LogcatOnlyMessageBuffer(LogLevel.DEBUG), TAG)
 
         const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600
         private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt
index dd1599e..9857d7f 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.shared.clocks
 
 import android.graphics.Canvas
+import com.android.systemui.plugins.clocks.VPoint
+import com.android.systemui.plugins.clocks.VPointF
 
 object CanvasUtil {
     fun Canvas.translate(pt: VPointF) = this.translate(pt.x, pt.y)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
index f5ccc52..941cebf 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
@@ -20,7 +20,8 @@
 import android.animation.AnimatorListenerAdapter
 import android.animation.TimeInterpolator
 import android.animation.ValueAnimator
-import com.android.systemui.shared.clocks.VPointF.Companion.times
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.times
 
 class DigitTranslateAnimator(private val updateCallback: (VPointF) -> Unit) {
     var currentTranslation = VPointF.ZERO
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
index 336c66e..9ac9e60 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
@@ -16,13 +16,13 @@
 
 package com.android.systemui.shared.clocks
 
-import android.graphics.RectF
 import android.view.View
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.plugins.clocks.ClockAnimations
 import com.android.systemui.plugins.clocks.ClockEvents
 import com.android.systemui.plugins.clocks.ClockFaceConfig
 import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.VRectF
 
 interface SimpleClockLayerController {
     val view: View
@@ -32,5 +32,5 @@
     val config: ClockFaceConfig
 
     @VisibleForTesting var fakeTimeMills: Long?
-    var onViewBoundsChanged: ((RectF) -> Unit)?
+    var onViewBoundsChanged: ((VRectF) -> Unit)?
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt
index 1e90a23..0740b0e 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt
@@ -18,8 +18,9 @@
 
 import android.graphics.Rect
 import android.view.View
-import com.android.systemui.shared.clocks.VPoint.Companion.center
-import com.android.systemui.shared.clocks.VPointF.Companion.center
+import com.android.systemui.plugins.clocks.VPoint.Companion.center
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.center
 
 object ViewUtils {
     fun View.computeLayoutDiff(targetRegion: Rect, isLargeClock: Boolean): VPointF {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index 2dc3e2b..ba32ab0 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.shared.clocks.view
 
 import android.graphics.Canvas
-import android.graphics.RectF
 import android.icu.text.NumberFormat
 import android.util.MathUtils.constrainedMap
 import android.view.View
@@ -29,14 +28,15 @@
 import com.android.systemui.customization.R
 import com.android.systemui.plugins.clocks.ClockFontAxisSetting
 import com.android.systemui.plugins.clocks.ClockLogger
+import com.android.systemui.plugins.clocks.VPoint
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.max
+import com.android.systemui.plugins.clocks.VPointF.Companion.times
+import com.android.systemui.plugins.clocks.VRectF
 import com.android.systemui.shared.clocks.CanvasUtil.translate
 import com.android.systemui.shared.clocks.CanvasUtil.use
 import com.android.systemui.shared.clocks.ClockContext
 import com.android.systemui.shared.clocks.DigitTranslateAnimator
-import com.android.systemui.shared.clocks.VPoint
-import com.android.systemui.shared.clocks.VPointF
-import com.android.systemui.shared.clocks.VPointF.Companion.max
-import com.android.systemui.shared.clocks.VPointF.Companion.times
 import com.android.systemui.shared.clocks.ViewUtils.measuredSize
 import java.util.Locale
 import kotlin.collections.filterNotNull
@@ -101,7 +101,7 @@
         updateLocale(Locale.getDefault())
     }
 
-    var onViewBoundsChanged: ((RectF) -> Unit)? = null
+    var onViewBoundsChanged: ((VRectF) -> Unit)? = null
     private val digitOffsets = mutableMapOf<Int, Float>()
 
     protected fun calculateSize(
@@ -189,13 +189,7 @@
 
     fun updateLocation() {
         val layoutBounds = this.layoutBounds ?: return
-        val bounds =
-            RectF(
-                layoutBounds.centerX() - measuredWidth / 2f,
-                layoutBounds.centerY() - measuredHeight / 2f,
-                layoutBounds.centerX() + measuredWidth / 2f,
-                layoutBounds.centerY() + measuredHeight / 2f,
-            )
+        val bounds = VRectF.fromCenter(layoutBounds.center, this.measuredSize)
         setFrame(
             bounds.left.roundToInt(),
             bounds.top.roundToInt(),
@@ -215,16 +209,11 @@
         onAnimateDoze = null
     }
 
-    private val layoutBounds = RectF()
+    private var layoutBounds = VRectF.ZERO
 
     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
         logger.onLayout(changed, left, top, right, bottom)
-
-        layoutBounds.left = left.toFloat()
-        layoutBounds.top = top.toFloat()
-        layoutBounds.right = right.toFloat()
-        layoutBounds.bottom = bottom.toFloat()
-
+        layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
         updateChildFrames(isLayout = true)
     }
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index 0ec2d18..2af25fe 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -23,7 +23,6 @@
 import android.graphics.PorterDuff
 import android.graphics.PorterDuffXfermode
 import android.graphics.Rect
-import android.graphics.RectF
 import android.os.VibrationEffect
 import android.text.Layout
 import android.text.TextPaint
@@ -45,6 +44,10 @@
 import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.replace
 import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.toFVar
 import com.android.systemui.plugins.clocks.ClockLogger
+import com.android.systemui.plugins.clocks.VPoint
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.size
+import com.android.systemui.plugins.clocks.VRectF
 import com.android.systemui.shared.Flags.ambientAod
 import com.android.systemui.shared.clocks.CanvasUtil.translate
 import com.android.systemui.shared.clocks.CanvasUtil.use
@@ -53,9 +56,6 @@
 import com.android.systemui.shared.clocks.DimensionParser
 import com.android.systemui.shared.clocks.FLEX_CLOCK_ID
 import com.android.systemui.shared.clocks.FontTextStyle
-import com.android.systemui.shared.clocks.VPoint
-import com.android.systemui.shared.clocks.VPointF
-import com.android.systemui.shared.clocks.VPointF.Companion.size
 import com.android.systemui.shared.clocks.ViewUtils.measuredSize
 import com.android.systemui.shared.clocks.ViewUtils.size
 import com.android.systemui.shared.clocks.toClockAxisSetting
@@ -66,11 +66,11 @@
 
 private val TAG = SimpleDigitalClockTextView::class.simpleName!!
 
-private fun Paint.getTextBounds(text: CharSequence, result: RectF = RectF()): RectF {
-    val rect = Rect()
-    this.getTextBounds(text, 0, text.length, rect)
-    result.set(rect)
-    return result
+private val tempRect = Rect()
+
+private fun Paint.getTextBounds(text: CharSequence): VRectF {
+    this.getTextBounds(text, 0, text.length, tempRect)
+    return VRectF(tempRect)
 }
 
 enum class VerticalAlignment {
@@ -143,7 +143,7 @@
         fidgetFontVariation = buildFidgetVariation(lsFontAxes).toFVar()
     }
 
-    var onViewBoundsChanged: ((RectF) -> Unit)? = null
+    var onViewBoundsChanged: ((VRectF) -> Unit)? = null
     private val parser = DimensionParser(clockCtx.context)
     var maxSingleDigitHeight = -1f
     var maxSingleDigitWidth = -1f
@@ -159,13 +159,13 @@
     private val initThread = Thread.currentThread()
 
     // textBounds is the size of text in LS, which only measures current text in lockscreen style
-    var textBounds = RectF()
+    var textBounds = VRectF.ZERO
     // prevTextBounds and targetTextBounds are to deal with dozing animation between LS and AOD
     // especially for the textView which has different bounds during the animation
     // prevTextBounds holds the state we are transitioning from
-    private val prevTextBounds = RectF()
+    private var prevTextBounds = VRectF.ZERO
     // targetTextBounds holds the state we are interpolating to
-    private val targetTextBounds = RectF()
+    private var targetTextBounds = VRectF.ZERO
     protected val logger = ClockLogger(this, clockCtx.messageBuffer, this::class.simpleName!!)
         get() = field ?: ClockLogger.INIT_LOGGER
 
@@ -215,8 +215,8 @@
         lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
         typeface = lockScreenPaint.typeface
 
-        lockScreenPaint.getTextBounds(text, textBounds)
-        targetTextBounds.set(textBounds)
+        textBounds = lockScreenPaint.getTextBounds(text)
+        targetTextBounds = textBounds
 
         textAnimator.setTextStyle(TextAnimator.Style(fVar = lsFontVariation))
         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
@@ -287,7 +287,7 @@
         canvas.use {
             digitTranslateAnimator?.apply { canvas.translate(currentTranslation) }
             canvas.translate(getDrawTranslation(interpBounds))
-            if (isLayoutRtl()) canvas.translate(interpBounds.width() - textBounds.width(), 0f)
+            if (isLayoutRtl()) canvas.translate(interpBounds.width - textBounds.width, 0f)
             textAnimator.draw(canvas)
         }
     }
@@ -302,16 +302,12 @@
         super.setAlpha(alpha)
     }
 
-    private val layoutBounds = RectF()
+    private var layoutBounds = VRectF.ZERO
 
     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
         super.onLayout(changed, left, top, right, bottom)
         logger.onLayout(changed, left, top, right, bottom)
-
-        layoutBounds.left = left.toFloat()
-        layoutBounds.top = top.toFloat()
-        layoutBounds.right = right.toFloat()
-        layoutBounds.bottom = bottom.toFloat()
+        layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
     }
 
     override fun invalidate() {
@@ -327,7 +323,7 @@
 
     fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
         if (!this::textAnimator.isInitialized) return
-        logger.animateDoze()
+        logger.animateDoze(isDozing, isAnimated)
         textAnimator.setTextStyle(
             TextAnimator.Style(
                 fVar = if (isDozing) aodFontVariation else lsFontVariation,
@@ -341,6 +337,11 @@
             ),
         )
         updateTextBoundsForTextAnimator()
+
+        if (!isAnimated) {
+            requestLayout()
+            (parent as? FlexClockView)?.requestLayout()
+        }
     }
 
     fun animateCharge() {
@@ -408,10 +409,10 @@
     }
 
     fun refreshText() {
-        lockScreenPaint.getTextBounds(text, textBounds)
-        if (this::textAnimator.isInitialized) {
-            textAnimator.textInterpolator.targetPaint.getTextBounds(text, targetTextBounds)
-        }
+        textBounds = lockScreenPaint.getTextBounds(text)
+        targetTextBounds =
+            if (!this::textAnimator.isInitialized) textBounds
+            else textAnimator.textInterpolator.targetPaint.getTextBounds(text)
 
         if (layout == null) {
             requestLayout()
@@ -432,23 +433,23 @@
     }
 
     /** Returns the interpolated text bounding rect based on interpolation progress */
-    private fun getInterpolatedTextBounds(progress: Float = getInterpolatedProgress()): RectF {
+    private fun getInterpolatedTextBounds(progress: Float = getInterpolatedProgress()): VRectF {
         if (progress <= 0f) {
             return prevTextBounds
         } else if (!textAnimator.isRunning || progress >= 1f) {
             return targetTextBounds
         }
 
-        return RectF().apply {
-            left = lerp(prevTextBounds.left, targetTextBounds.left, progress)
-            right = lerp(prevTextBounds.right, targetTextBounds.right, progress)
-            top = lerp(prevTextBounds.top, targetTextBounds.top, progress)
-            bottom = lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress)
-        }
+        return VRectF(
+            left = lerp(prevTextBounds.left, targetTextBounds.left, progress),
+            right = lerp(prevTextBounds.right, targetTextBounds.right, progress),
+            top = lerp(prevTextBounds.top, targetTextBounds.top, progress),
+            bottom = lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress),
+        )
     }
 
     private fun computeMeasuredSize(
-        interpBounds: RectF,
+        interpBounds: VRectF,
         widthMeasureSpec: Int = measuredWidthAndState,
         heightMeasureSpec: Int = measuredHeightAndState,
     ): VPointF {
@@ -461,11 +462,11 @@
         return VPointF(
             when {
                 mode.x == EXACTLY -> MeasureSpec.getSize(widthMeasureSpec).toFloat()
-                else -> interpBounds.width() + 2 * lockScreenPaint.strokeWidth
+                else -> interpBounds.width + 2 * lockScreenPaint.strokeWidth
             },
             when {
                 mode.y == EXACTLY -> MeasureSpec.getSize(heightMeasureSpec).toFloat()
-                else -> interpBounds.height() + 2 * lockScreenPaint.strokeWidth
+                else -> interpBounds.height + 2 * lockScreenPaint.strokeWidth
             },
         )
     }
@@ -489,44 +490,23 @@
     }
 
     /** Set the location of the view to match the interpolated text bounds */
-    private fun setInterpolatedLocation(measureSize: VPointF): RectF {
-        val targetRect = RectF()
-        targetRect.apply {
-            when (xAlignment) {
-                XAlignment.LEFT -> {
-                    left = layoutBounds.left
-                    right = layoutBounds.left + measureSize.x
-                }
-                XAlignment.CENTER -> {
-                    left = layoutBounds.centerX() - measureSize.x / 2f
-                    right = layoutBounds.centerX() + measureSize.x / 2f
-                }
-                XAlignment.RIGHT -> {
-                    left = layoutBounds.right - measureSize.x
-                    right = layoutBounds.right
-                }
-            }
+    private fun setInterpolatedLocation(measureSize: VPointF): VRectF {
+        val pos =
+            VPointF(
+                when (xAlignment) {
+                    XAlignment.LEFT -> layoutBounds.left
+                    XAlignment.CENTER -> layoutBounds.center.x - measureSize.x / 2f
+                    XAlignment.RIGHT -> layoutBounds.right - measureSize.x
+                },
+                when (verticalAlignment) {
+                    VerticalAlignment.TOP -> layoutBounds.top
+                    VerticalAlignment.CENTER -> layoutBounds.center.y - measureSize.y / 2f
+                    VerticalAlignment.BOTTOM -> layoutBounds.bottom - measureSize.y
+                    VerticalAlignment.BASELINE -> layoutBounds.center.y - measureSize.y / 2f
+                },
+            )
 
-            when (verticalAlignment) {
-                VerticalAlignment.TOP -> {
-                    top = layoutBounds.top
-                    bottom = layoutBounds.top + measureSize.y
-                }
-                VerticalAlignment.CENTER -> {
-                    top = layoutBounds.centerY() - measureSize.y / 2f
-                    bottom = layoutBounds.centerY() + measureSize.y / 2f
-                }
-                VerticalAlignment.BOTTOM -> {
-                    top = layoutBounds.bottom - measureSize.y
-                    bottom = layoutBounds.bottom
-                }
-                VerticalAlignment.BASELINE -> {
-                    top = layoutBounds.centerY() - measureSize.y / 2f
-                    bottom = layoutBounds.centerY() + measureSize.y / 2f
-                }
-            }
-        }
-
+        val targetRect = VRectF.fromTopLeft(pos, measureSize)
         setFrame(
             targetRect.left.roundToInt(),
             targetRect.top.roundToInt(),
@@ -537,7 +517,7 @@
         return targetRect
     }
 
-    private fun getDrawTranslation(interpBounds: RectF): VPointF {
+    private fun getDrawTranslation(interpBounds: VRectF): VPointF {
         val sizeDiff = this.measuredSize - interpBounds.size
         val alignment =
             VPointF(
@@ -586,11 +566,11 @@
         if (fontSizePx > 0) {
             setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
             lockScreenPaint.textSize = textSize
-            lockScreenPaint.getTextBounds(text, textBounds)
-            targetTextBounds.set(textBounds)
+            textBounds = lockScreenPaint.getTextBounds(text)
+            targetTextBounds = textBounds
         }
         if (!constrainedByHeight) {
-            val lastUnconstrainedHeight = textBounds.height() + lockScreenPaint.strokeWidth * 2
+            val lastUnconstrainedHeight = textBounds.height + lockScreenPaint.strokeWidth * 2
             fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize
         }
 
@@ -608,8 +588,8 @@
 
         for (i in 0..9) {
             val rectForCalculate = lockScreenPaint.getTextBounds("$i")
-            maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height())
-            maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width())
+            maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height)
+            maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width)
         }
         maxSingleDigitWidth += 2 * lockScreenPaint.strokeWidth
         maxSingleDigitHeight += 2 * lockScreenPaint.strokeWidth
@@ -637,8 +617,8 @@
      * and targetPaint will store the state we transition to
      */
     private fun updateTextBoundsForTextAnimator() {
-        textAnimator.textInterpolator.basePaint.getTextBounds(text, prevTextBounds)
-        textAnimator.textInterpolator.targetPaint.getTextBounds(text, targetTextBounds)
+        prevTextBounds = textAnimator.textInterpolator.basePaint.getTextBounds(text)
+        targetTextBounds = textAnimator.textInterpolator.targetPaint.getTextBounds(text)
     }
 
     /**
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
index 8d3640d..53b364c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
@@ -41,21 +41,9 @@
     @Test
     fun testStyleValueUnchange_getBlankStr() {
         val fontVariationUtils = FontVariationUtils()
-        fontVariationUtils.updateFontVariation(
-            weight = 100,
-            width = 100,
-            opticalSize = 0,
-            roundness = 100,
-        )
-        val updatedFvar1 =
-            fontVariationUtils.updateFontVariation(
-                weight = 100,
-                width = 100,
-                opticalSize = 0,
-                roundness = 100,
-            )
-        Assert.assertEquals("", updatedFvar1)
-        val updatedFvar2 = fontVariationUtils.updateFontVariation()
-        Assert.assertEquals("", updatedFvar2)
+        Assert.assertEquals("", fontVariationUtils.updateFontVariation())
+        val fVar = fontVariationUtils.updateFontVariation(weight = 100)
+        Assert.assertEquals(fVar, fontVariationUtils.updateFontVariation())
+        Assert.assertEquals(fVar, fontVariationUtils.updateFontVariation(weight = 100))
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt
index 0f40089..56b06de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt
@@ -17,14 +17,12 @@
 
 package com.android.systemui.common.ui.view
 
+import android.testing.TestableLooper
+import android.view.MotionEvent
 import android.view.ViewConfiguration
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Down
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Move
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Up
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -33,18 +31,22 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
 import org.mockito.Mockito.never
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() {
 
     @Mock private lateinit var postDelayed: (Runnable, Long) -> DisposableHandle
     @Mock private lateinit var onLongPressDetected: (Int, Int) -> Unit
     @Mock private lateinit var onSingleTapDetected: (Int, Int) -> Unit
+    @Mock private lateinit var onDoubleTapDetected: () -> Unit
 
     private lateinit var underTest: TouchHandlingViewInteractionHandler
 
@@ -61,14 +63,17 @@
 
         underTest =
             TouchHandlingViewInteractionHandler(
+                context = context,
                 postDelayed = postDelayed,
                 isAttachedToWindow = { isAttachedToWindow },
                 onLongPressDetected = onLongPressDetected,
                 onSingleTapDetected = onSingleTapDetected,
+                onDoubleTapDetected = onDoubleTapDetected,
                 longPressDuration = { ViewConfiguration.getLongPressTimeout().toLong() },
                 allowedTouchSlop = ViewConfiguration.getTouchSlop(),
             )
         underTest.isLongPressHandlingEnabled = true
+        underTest.isDoubleTapHandlingEnabled = true
     }
 
     @Test
@@ -76,63 +81,250 @@
         val downX = 123
         val downY = 456
         dispatchTouchEvents(
-            Down(x = downX, y = downY),
-            Move(distanceMoved = ViewConfiguration.getTouchSlop() - 0.1f),
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+            MotionEvent.obtain(
+                0L,
+                0L,
+                MotionEvent.ACTION_MOVE,
+                123f + ViewConfiguration.getTouchSlop() - 0.1f,
+                456f,
+                0,
+            ),
         )
         delayedRunnable?.run()
 
         verify(onLongPressDetected).invoke(downX, downY)
-        verify(onSingleTapDetected, never()).invoke(any(), any())
+        verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
     }
 
     @Test
     fun longPressButFeatureNotEnabled() = runTest {
         underTest.isLongPressHandlingEnabled = false
-        dispatchTouchEvents(Down(x = 123, y = 456))
+        dispatchTouchEvents(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0))
 
         assertThat(delayedRunnable).isNull()
-        verify(onLongPressDetected, never()).invoke(any(), any())
-        verify(onSingleTapDetected, never()).invoke(any(), any())
+        verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+        verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
     }
 
     @Test
     fun longPressButViewNotAttached() = runTest {
         isAttachedToWindow = false
-        dispatchTouchEvents(Down(x = 123, y = 456))
+        dispatchTouchEvents(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0))
         delayedRunnable?.run()
 
-        verify(onLongPressDetected, never()).invoke(any(), any())
-        verify(onSingleTapDetected, never()).invoke(any(), any())
+        verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+        verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
     }
 
     @Test
     fun draggedTooFarToBeConsideredAlongPress() = runTest {
         dispatchTouchEvents(
-            Down(x = 123, y = 456),
-            Move(distanceMoved = ViewConfiguration.getTouchSlop() + 0.1f),
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123F, 456F, 0),
+            // Drag action within touch slop
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 123f, 456f, 0).apply {
+                addBatch(0L, 123f + ViewConfiguration.getTouchSlop() + 0.1f, 456f, 0f, 0f, 0)
+            },
         )
 
         assertThat(delayedRunnable).isNull()
-        verify(onLongPressDetected, never()).invoke(any(), any())
-        verify(onSingleTapDetected, never()).invoke(any(), any())
+        verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+        verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
     }
 
     @Test
     fun heldDownTooBrieflyToBeConsideredAlongPress() = runTest {
         dispatchTouchEvents(
-            Down(x = 123, y = 456),
-            Up(
-                distanceMoved = ViewConfiguration.getTouchSlop().toFloat(),
-                gestureDuration = ViewConfiguration.getLongPressTimeout() - 1L,
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+            MotionEvent.obtain(
+                0L,
+                ViewConfiguration.getLongPressTimeout() - 1L,
+                MotionEvent.ACTION_UP,
+                123f,
+                456F,
+                0,
             ),
         )
 
         assertThat(delayedRunnable).isNull()
-        verify(onLongPressDetected, never()).invoke(any(), any())
+        verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
         verify(onSingleTapDetected).invoke(123, 456)
     }
 
-    private fun dispatchTouchEvents(vararg models: MotionEventModel) {
-        models.forEach { model -> underTest.onTouchEvent(model) }
+    @Test
+    fun doubleTap() = runTest {
+        val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+        dispatchTouchEvents(
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+            MotionEvent.obtain(
+                secondTapTime,
+                secondTapTime,
+                MotionEvent.ACTION_DOWN,
+                123f,
+                456f,
+                0,
+            ),
+            MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_UP, 123f, 456f, 0),
+        )
+
+        verify(onDoubleTapDetected).invoke()
+        assertThat(delayedRunnable).isNull()
+        verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+        verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+    }
+
+    @Test
+    fun doubleTapButFeatureNotEnabled() = runTest {
+        underTest.isDoubleTapHandlingEnabled = false
+
+        val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+        dispatchTouchEvents(
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+            MotionEvent.obtain(
+                secondTapTime,
+                secondTapTime,
+                MotionEvent.ACTION_DOWN,
+                123f,
+                456f,
+                0,
+            ),
+            MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_UP, 123f, 456f, 0),
+        )
+
+        verify(onDoubleTapDetected, never()).invoke()
+        assertThat(delayedRunnable).isNull()
+        verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+        verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+    }
+
+    @Test
+    fun tapIntoLongPress() = runTest {
+        val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+        dispatchTouchEvents(
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+            MotionEvent.obtain(
+                secondTapTime,
+                secondTapTime,
+                MotionEvent.ACTION_DOWN,
+                123f,
+                456f,
+                0,
+            ),
+            MotionEvent.obtain(
+                secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+                secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+                MotionEvent.ACTION_MOVE,
+                123f + ViewConfiguration.getTouchSlop() - 0.1f,
+                456f,
+                0,
+            ),
+        )
+        delayedRunnable?.run()
+
+        verify(onDoubleTapDetected, never()).invoke()
+        verify(onSingleTapDetected).invoke(anyInt(), anyInt())
+        verify(onLongPressDetected).invoke(anyInt(), anyInt())
+    }
+
+    @Test
+    fun tapIntoDownHoldTooBrieflyToBeConsideredLongPress() = runTest {
+        val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+        dispatchTouchEvents(
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+            MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 123f, 456f, 0),
+            MotionEvent.obtain(
+                secondTapTime,
+                secondTapTime,
+                MotionEvent.ACTION_DOWN,
+                123f,
+                456f,
+                0,
+            ),
+            MotionEvent.obtain(
+                secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+                secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+                MotionEvent.ACTION_UP,
+                123f,
+                456f,
+                0,
+            ),
+        )
+        delayedRunnable?.run()
+
+        verify(onDoubleTapDetected, never()).invoke()
+        verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+        verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+    }
+
+    @Test
+    fun tapIntoDrag() = runTest {
+        val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+        dispatchTouchEvents(
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+            MotionEvent.obtain(
+                secondTapTime,
+                secondTapTime,
+                MotionEvent.ACTION_DOWN,
+                123f,
+                456f,
+                0,
+            ),
+            // Drag event within touch slop
+            MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_MOVE, 123f, 456f, 0)
+                .apply {
+                    addBatch(
+                        secondTapTime,
+                        123f + ViewConfiguration.getTouchSlop() + 0.1f,
+                        456f,
+                        0f,
+                        0f,
+                        0,
+                    )
+                },
+        )
+        delayedRunnable?.run()
+
+        verify(onDoubleTapDetected, never()).invoke()
+        verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+        verify(onSingleTapDetected).invoke(anyInt(), anyInt())
+    }
+
+    @Test
+    fun doubleTapOutOfAllowableSlop() = runTest {
+        val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+        val scaledDoubleTapSlop = ViewConfiguration.get(context).scaledDoubleTapSlop
+        dispatchTouchEvents(
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+            MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+            MotionEvent.obtain(
+                secondTapTime,
+                secondTapTime,
+                MotionEvent.ACTION_DOWN,
+                123f + scaledDoubleTapSlop + 0.1f,
+                456f + scaledDoubleTapSlop + 0.1f,
+                0,
+            ),
+            MotionEvent.obtain(
+                secondTapTime,
+                secondTapTime,
+                MotionEvent.ACTION_UP,
+                123f + scaledDoubleTapSlop + 0.1f,
+                456f + scaledDoubleTapSlop + 0.1f,
+                0,
+            ),
+        )
+
+        verify(onDoubleTapDetected, never()).invoke()
+        assertThat(delayedRunnable).isNull()
+        verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+        verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+    }
+
+    private fun dispatchTouchEvents(vararg events: MotionEvent) {
+        events.forEach { event -> underTest.onTouchEvent(event) }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
index ed73d89..6a25069 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
@@ -73,12 +73,12 @@
             assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
             assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
 
-            kosmos.setCommunalEnabled(true)
+            setCommunalEnabled(true)
 
             assertThat(fakeCommunalMediaRepository.isListening()).isTrue()
             assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue()
 
-            kosmos.setCommunalEnabled(false)
+            setCommunalEnabled(false)
 
             assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
             assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
@@ -93,13 +93,13 @@
             assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
             assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
 
-            kosmos.setCommunalEnabled(true)
+            setCommunalEnabled(true)
 
             // Media listening does not start when UMO is disabled.
             assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
             assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue()
 
-            kosmos.setCommunalEnabled(false)
+            setCommunalEnabled(false)
 
             assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
             assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryImplTest.kt
new file mode 100644
index 0000000..f9b29e9
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryImplTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2025 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.communal.data.repository
+
+import android.app.UiModeManager
+import android.app.UiModeManager.OnProjectionStateChangedListener
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.backgroundScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CarProjectionRepositoryImplTest : SysuiTestCase() {
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+    private val capturedListeners = mutableListOf<OnProjectionStateChangedListener>()
+
+    private val Kosmos.uiModeManager by
+        Kosmos.Fixture<UiModeManager> {
+            mock {
+                on {
+                    addOnProjectionStateChangedListener(
+                        eq(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE),
+                        any(),
+                        any(),
+                    )
+                } doAnswer
+                    {
+                        val listener = it.getArgument<OnProjectionStateChangedListener>(2)
+                        capturedListeners.add(listener)
+                        Unit
+                    }
+
+                on { removeOnProjectionStateChangedListener(any()) } doAnswer
+                    {
+                        val listener = it.getArgument<OnProjectionStateChangedListener>(0)
+                        capturedListeners.remove(listener)
+                        Unit
+                    }
+
+                on { activeProjectionTypes } doReturn UiModeManager.PROJECTION_TYPE_NONE
+            }
+        }
+
+    private val Kosmos.underTest by
+        Kosmos.Fixture {
+            CarProjectionRepositoryImpl(
+                uiModeManager = uiModeManager,
+                bgDispatcher = testDispatcher,
+            )
+        }
+
+    @Test
+    fun testProjectionActiveUpdatesAfterCallback() =
+        kosmos.runTest {
+            val projectionActive by collectLastValue(underTest.projectionActive)
+            assertThat(projectionActive).isFalse()
+
+            setActiveProjectionType(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE)
+            assertThat(projectionActive).isTrue()
+
+            setActiveProjectionType(UiModeManager.PROJECTION_TYPE_NONE)
+            assertThat(projectionActive).isFalse()
+        }
+
+    @Test
+    fun testProjectionInitialValueTrue() =
+        kosmos.runTest {
+            setActiveProjectionType(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE)
+
+            val projectionActive by collectLastValue(underTest.projectionActive)
+            assertThat(projectionActive).isTrue()
+        }
+
+    @Test
+    fun testUnsubscribeWhenCancelled() =
+        kosmos.runTest {
+            val job = underTest.projectionActive.launchIn(backgroundScope)
+            assertThat(capturedListeners).hasSize(1)
+
+            job.cancel()
+            assertThat(capturedListeners).isEmpty()
+        }
+
+    private fun Kosmos.setActiveProjectionType(@UiModeManager.ProjectionType projectionType: Int) {
+        uiModeManager.stub { on { activeProjectionTypes } doReturn projectionType }
+        capturedListeners.forEach { it.onProjectionStateChanged(projectionType, emptySet()) }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
index 5c98365..09d44a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
@@ -34,9 +34,7 @@
 import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.broadcastDispatcher
-import com.android.systemui.communal.data.model.DisabledReason
 import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl.Companion.GLANCEABLE_HUB_BACKGROUND_SETTING
-import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled
 import com.android.systemui.communal.shared.model.CommunalBackgroundType
 import com.android.systemui.communal.shared.model.WhenToDream
 import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
@@ -202,63 +200,6 @@
 
     @EnableFlags(FLAG_COMMUNAL_HUB)
     @Test
-    fun secondaryUserIsInvalid() =
-        kosmos.runTest {
-            val enabledState by collectLastValue(underTest.getEnabledState(SECONDARY_USER))
-
-            assertThat(enabledState?.enabled).isFalse()
-            assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_INVALID_USER)
-        }
-
-    @EnableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2)
-    @Test
-    fun classicFlagIsDisabled() =
-        kosmos.runTest {
-            setCommunalV2Enabled(false)
-            val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
-            assertThat(enabledState?.enabled).isFalse()
-            assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG)
-        }
-
-    @DisableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2)
-    @Test
-    fun communalHubFlagIsDisabled() =
-        kosmos.runTest {
-            val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
-            assertThat(enabledState?.enabled).isFalse()
-            assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG)
-        }
-
-    @EnableFlags(FLAG_COMMUNAL_HUB)
-    @Test
-    fun hubIsDisabledByUser() =
-        kosmos.runTest {
-            fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id)
-            val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
-            assertThat(enabledState?.enabled).isFalse()
-            assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_USER_SETTING)
-
-            fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 1, SECONDARY_USER.id)
-            assertThat(enabledState?.enabled).isFalse()
-
-            fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 1, PRIMARY_USER.id)
-            assertThat(enabledState?.enabled).isTrue()
-        }
-
-    @EnableFlags(FLAG_COMMUNAL_HUB)
-    @Test
-    fun hubIsDisabledByDevicePolicy() =
-        kosmos.runTest {
-            val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
-            assertThat(enabledState?.enabled).isTrue()
-
-            setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL)
-            assertThat(enabledState?.enabled).isFalse()
-            assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_DEVICE_POLICY)
-        }
-
-    @EnableFlags(FLAG_COMMUNAL_HUB)
-    @Test
     fun widgetsAllowedForWorkProfile_isFalse_whenDisallowedByDevicePolicy() =
         kosmos.runTest {
             val widgetsAllowedForWorkProfile by
@@ -269,36 +210,6 @@
             assertThat(widgetsAllowedForWorkProfile).isFalse()
         }
 
-    @EnableFlags(FLAG_COMMUNAL_HUB)
-    @Test
-    fun hubIsEnabled_whenDisallowedByDevicePolicyForWorkProfile() =
-        kosmos.runTest {
-            val enabledStateForPrimaryUser by
-                collectLastValue(underTest.getEnabledState(PRIMARY_USER))
-            assertThat(enabledStateForPrimaryUser?.enabled).isTrue()
-
-            setKeyguardFeaturesDisabled(WORK_PROFILE, KEYGUARD_DISABLE_WIDGETS_ALL)
-            assertThat(enabledStateForPrimaryUser?.enabled).isTrue()
-        }
-
-    @EnableFlags(FLAG_COMMUNAL_HUB)
-    @Test
-    fun hubIsDisabledByUserAndDevicePolicy() =
-        kosmos.runTest {
-            val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER))
-            assertThat(enabledState?.enabled).isTrue()
-
-            fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id)
-            setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL)
-
-            assertThat(enabledState?.enabled).isFalse()
-            assertThat(enabledState)
-                .containsExactly(
-                    DisabledReason.DISABLED_REASON_DEVICE_POLICY,
-                    DisabledReason.DISABLED_REASON_USER_SETTING,
-                )
-        }
-
     @Test
     @DisableFlags(FLAG_GLANCEABLE_HUB_BLURRED_BACKGROUND)
     fun backgroundType_defaultValue() =
@@ -327,26 +238,6 @@
         }
 
     @Test
-    fun screensaverDisabledByUser() =
-        kosmos.runTest {
-            val enabledState by collectLastValue(underTest.getScreensaverEnabledState(PRIMARY_USER))
-
-            fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ENABLED, 0, PRIMARY_USER.id)
-
-            assertThat(enabledState).isFalse()
-        }
-
-    @Test
-    fun screensaverEnabledByUser() =
-        kosmos.runTest {
-            val enabledState by collectLastValue(underTest.getScreensaverEnabledState(PRIMARY_USER))
-
-            fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ENABLED, 1, PRIMARY_USER.id)
-
-            assertThat(enabledState).isTrue()
-        }
-
-    @Test
     fun whenToDream_charging() =
         kosmos.runTest {
             val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorTest.kt
new file mode 100644
index 0000000..fc4cd43
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorTest.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.carProjectionRepository
+import com.android.systemui.communal.data.repository.fake
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CarProjectionInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+    private val Kosmos.underTest by Kosmos.Fixture { carProjectionInteractor }
+
+    @Test
+    fun testProjectionActive() =
+        kosmos.runTest {
+            val projectionActive by collectLastValue(underTest.projectionActive)
+            assertThat(projectionActive).isFalse()
+
+            carProjectionRepository.fake.setProjectionActive(true)
+            assertThat(projectionActive).isTrue()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt
new file mode 100644
index 0000000..f4a1c90
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.interactor
+
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.data.repository.batteryRepository
+import com.android.systemui.common.data.repository.fake
+import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN
+import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN
+import com.android.systemui.communal.data.model.SuppressionReason
+import com.android.systemui.communal.posturing.data.repository.fake
+import com.android.systemui.communal.posturing.data.repository.posturingRepository
+import com.android.systemui.communal.posturing.shared.model.PosturedState
+import com.android.systemui.dock.DockManager
+import com.android.systemui.dock.fakeDockManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.settings.fakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalAutoOpenInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+    private val Kosmos.underTest by Kosmos.Fixture { communalAutoOpenInteractor }
+
+    @Before
+    fun setUp() {
+        runBlocking { kosmos.fakeUserRepository.asMainUser() }
+        with(kosmos.fakeSettings) {
+            putBoolForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, false, MAIN_USER_ID)
+            putBoolForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, false, MAIN_USER_ID)
+            putBoolForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, false, MAIN_USER_ID)
+        }
+    }
+
+    @Test
+    fun testStartWhileCharging() =
+        kosmos.runTest {
+            val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen)
+            val suppressionReason by collectLastValue(underTest.suppressionReason)
+
+            fakeSettings.putBoolForUser(
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+                true,
+                MAIN_USER_ID,
+            )
+
+            batteryRepository.fake.setDevicePluggedIn(false)
+            assertThat(shouldAutoOpen).isFalse()
+            assertThat(suppressionReason)
+                .isEqualTo(
+                    SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN)
+                )
+
+            batteryRepository.fake.setDevicePluggedIn(true)
+            assertThat(shouldAutoOpen).isTrue()
+            assertThat(suppressionReason).isNull()
+        }
+
+    @Test
+    fun testStartWhileDocked() =
+        kosmos.runTest {
+            val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen)
+            val suppressionReason by collectLastValue(underTest.suppressionReason)
+
+            fakeSettings.putBoolForUser(
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK,
+                true,
+                MAIN_USER_ID,
+            )
+
+            batteryRepository.fake.setDevicePluggedIn(true)
+            fakeDockManager.setIsDocked(false)
+
+            assertThat(shouldAutoOpen).isFalse()
+            assertThat(suppressionReason)
+                .isEqualTo(
+                    SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN)
+                )
+
+            fakeDockManager.setIsDocked(true)
+            fakeDockManager.setDockEvent(DockManager.STATE_DOCKED)
+            assertThat(shouldAutoOpen).isTrue()
+            assertThat(suppressionReason).isNull()
+        }
+
+    @Test
+    fun testStartWhilePostured() =
+        kosmos.runTest {
+            val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen)
+            val suppressionReason by collectLastValue(underTest.suppressionReason)
+
+            fakeSettings.putBoolForUser(
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
+                true,
+                MAIN_USER_ID,
+            )
+
+            batteryRepository.fake.setDevicePluggedIn(true)
+            posturingRepository.fake.setPosturedState(PosturedState.NotPostured)
+
+            assertThat(shouldAutoOpen).isFalse()
+            assertThat(suppressionReason)
+                .isEqualTo(
+                    SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN)
+                )
+
+            posturingRepository.fake.setPosturedState(PosturedState.Postured(1f))
+            assertThat(shouldAutoOpen).isTrue()
+            assertThat(suppressionReason).isNull()
+        }
+
+    @Test
+    fun testStartNever() =
+        kosmos.runTest {
+            val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen)
+            val suppressionReason by collectLastValue(underTest.suppressionReason)
+
+            fakeSettings.putBoolForUser(
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+                false,
+                MAIN_USER_ID,
+            )
+            fakeSettings.putBoolForUser(
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK,
+                false,
+                MAIN_USER_ID,
+            )
+            fakeSettings.putBoolForUser(
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
+                false,
+                MAIN_USER_ID,
+            )
+
+            batteryRepository.fake.setDevicePluggedIn(true)
+            posturingRepository.fake.setPosturedState(PosturedState.Postured(1f))
+            fakeDockManager.setIsDocked(true)
+
+            assertThat(shouldAutoOpen).isFalse()
+            assertThat(suppressionReason)
+                .isEqualTo(
+                    SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN)
+                )
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
deleted file mode 100644
index beec184..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.communal.domain.interactor
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
-import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
-import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
-import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * This class is a variation of the [CommunalInteractorTest] for cases where communal is disabled.
- */
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class CommunalInteractorCommunalDisabledTest : SysuiTestCase() {
-    private val kosmos = testKosmos()
-    private val testScope = kosmos.testScope
-
-    private lateinit var communalRepository: FakeCommunalSceneRepository
-    private lateinit var widgetRepository: FakeCommunalWidgetRepository
-    private lateinit var keyguardRepository: FakeKeyguardRepository
-
-    private lateinit var underTest: CommunalInteractor
-
-    @Before
-    fun setUp() {
-        communalRepository = kosmos.fakeCommunalSceneRepository
-        widgetRepository = kosmos.fakeCommunalWidgetRepository
-        keyguardRepository = kosmos.fakeKeyguardRepository
-
-        mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB)
-
-        underTest = kosmos.communalInteractor
-    }
-
-    @Test
-    fun isCommunalEnabled_false() =
-        testScope.runTest { assertThat(underTest.isCommunalEnabled.value).isFalse() }
-
-    @Test
-    fun isCommunalAvailable_whenStorageUnlock_false() =
-        testScope.runTest {
-            val isCommunalAvailable by collectLastValue(underTest.isCommunalAvailable)
-
-            assertThat(isCommunalAvailable).isFalse()
-
-            keyguardRepository.setIsEncryptedOrLockdown(false)
-            runCurrent()
-
-            assertThat(isCommunalAvailable).isFalse()
-        }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 8424746..b65ecf46 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -21,7 +21,6 @@
 import android.app.admin.devicePolicyManager
 import android.content.Intent
 import android.content.pm.UserInfo
-import android.content.res.mainResources
 import android.os.UserHandle
 import android.os.UserManager
 import android.os.userManager
@@ -39,9 +38,8 @@
 import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.broadcastDispatcher
-import com.android.systemui.common.data.repository.batteryRepository
-import com.android.systemui.common.data.repository.fake
 import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
+import com.android.systemui.communal.data.model.SuppressionReason
 import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
 import com.android.systemui.communal.data.repository.fakeCommunalPrefsRepository
 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
@@ -50,14 +48,9 @@
 import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.domain.model.CommunalTransitionProgressModel
-import com.android.systemui.communal.posturing.data.repository.fake
-import com.android.systemui.communal.posturing.data.repository.posturingRepository
-import com.android.systemui.communal.posturing.shared.model.PosturedState
 import com.android.systemui.communal.shared.model.CommunalContentSize
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.shared.model.EditModeState
-import com.android.systemui.dock.DockManager
-import com.android.systemui.dock.fakeDockManager
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -75,19 +68,16 @@
 import com.android.systemui.settings.fakeUserTracker
 import com.android.systemui.statusbar.phone.fakeManagedProfileController
 import com.android.systemui.testKosmos
-import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.fakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.advanceTimeBy
-import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -98,10 +88,6 @@
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
 
-/**
- * This class of test cases assume that communal is enabled. For disabled cases, see
- * [CommunalInteractorCommunalDisabledTest].
- */
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
 class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
@@ -109,10 +95,7 @@
         UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN)
     private val secondaryUser = UserInfo(/* id= */ 1, /* name= */ "secondary user", /* flags= */ 0)
 
-    private val kosmos =
-        testKosmos()
-            .apply { mainResources = mContext.orCreateTestableResources.resources }
-            .useUnconfinedTestDispatcher()
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
 
     private val Kosmos.underTest by Kosmos.Fixture { communalInteractor }
 
@@ -128,104 +111,40 @@
 
         kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
         mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
-
-        mContext.orCreateTestableResources.addOverride(
-            com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault,
-            false,
-        )
-        mContext.orCreateTestableResources.addOverride(
-            com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault,
-            false,
-        )
-        mContext.orCreateTestableResources.addOverride(
-            com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault,
-            false,
-        )
-    }
-
-    @After
-    fun tearDown() {
-        mContext.orCreateTestableResources.removeOverride(
-            com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault
-        )
-        mContext.orCreateTestableResources.removeOverride(
-            com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault
-        )
-        mContext.orCreateTestableResources.removeOverride(
-            com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault
-        )
     }
 
     @Test
     fun communalEnabled_true() =
         kosmos.runTest {
-            fakeUserRepository.setSelectedUserInfo(mainUser)
+            communalSettingsInteractor.setSuppressionReasons(emptyList())
             assertThat(underTest.isCommunalEnabled.value).isTrue()
         }
 
     @Test
-    fun isCommunalAvailable_mainUserUnlockedAndMainUser_true() =
-        kosmos.runTest {
-            val isAvailable by collectLastValue(underTest.isCommunalAvailable)
-            assertThat(isAvailable).isFalse()
-
-            fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
-            fakeUserRepository.setSelectedUserInfo(mainUser)
-            fakeKeyguardRepository.setKeyguardShowing(true)
-
-            assertThat(isAvailable).isTrue()
-        }
-
-    @Test
-    fun isCommunalAvailable_mainUserLockedAndMainUser_false() =
-        kosmos.runTest {
-            val isAvailable by collectLastValue(underTest.isCommunalAvailable)
-            assertThat(isAvailable).isFalse()
-
-            fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false)
-            fakeUserRepository.setSelectedUserInfo(mainUser)
-            fakeKeyguardRepository.setKeyguardShowing(true)
-
-            assertThat(isAvailable).isFalse()
-        }
-
-    @Test
-    fun isCommunalAvailable_mainUserUnlockedAndSecondaryUser_false() =
-        kosmos.runTest {
-            val isAvailable by collectLastValue(underTest.isCommunalAvailable)
-            assertThat(isAvailable).isFalse()
-
-            fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
-            fakeUserRepository.setSelectedUserInfo(secondaryUser)
-            fakeKeyguardRepository.setKeyguardShowing(true)
-
-            assertThat(isAvailable).isFalse()
-        }
-
-    @Test
     fun isCommunalAvailable_whenKeyguardShowing_true() =
         kosmos.runTest {
+            communalSettingsInteractor.setSuppressionReasons(emptyList())
+            fakeKeyguardRepository.setKeyguardShowing(false)
+
             val isAvailable by collectLastValue(underTest.isCommunalAvailable)
             assertThat(isAvailable).isFalse()
 
-            fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
-            fakeUserRepository.setSelectedUserInfo(mainUser)
             fakeKeyguardRepository.setKeyguardShowing(true)
-
             assertThat(isAvailable).isTrue()
         }
 
     @Test
-    fun isCommunalAvailable_communalDisabled_false() =
+    fun isCommunalAvailable_suppressed() =
         kosmos.runTest {
-            mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2)
+            communalSettingsInteractor.setSuppressionReasons(emptyList())
+            fakeKeyguardRepository.setKeyguardShowing(true)
 
             val isAvailable by collectLastValue(underTest.isCommunalAvailable)
-            assertThat(isAvailable).isFalse()
+            assertThat(isAvailable).isTrue()
 
-            fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false)
-            fakeUserRepository.setSelectedUserInfo(mainUser)
-            fakeKeyguardRepository.setKeyguardShowing(true)
+            communalSettingsInteractor.setSuppressionReasons(
+                listOf(SuppressionReason.ReasonUnknown())
+            )
 
             assertThat(isAvailable).isFalse()
         }
@@ -1280,66 +1199,6 @@
                 .inOrder()
         }
 
-    @Test
-    fun showCommunalWhileCharging() =
-        kosmos.runTest {
-            fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
-            fakeUserRepository.setSelectedUserInfo(mainUser)
-            fakeKeyguardRepository.setKeyguardShowing(true)
-            fakeSettings.putIntForUser(
-                Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
-                1,
-                mainUser.id,
-            )
-
-            val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal)
-            batteryRepository.fake.setDevicePluggedIn(false)
-            assertThat(shouldShowCommunal).isFalse()
-
-            batteryRepository.fake.setDevicePluggedIn(true)
-            assertThat(shouldShowCommunal).isTrue()
-        }
-
-    @Test
-    fun showCommunalWhilePosturedAndCharging() =
-        kosmos.runTest {
-            fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
-            fakeUserRepository.setSelectedUserInfo(mainUser)
-            fakeKeyguardRepository.setKeyguardShowing(true)
-            fakeSettings.putIntForUser(
-                Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
-                1,
-                mainUser.id,
-            )
-
-            val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal)
-            batteryRepository.fake.setDevicePluggedIn(true)
-            posturingRepository.fake.setPosturedState(PosturedState.NotPostured)
-            assertThat(shouldShowCommunal).isFalse()
-
-            posturingRepository.fake.setPosturedState(PosturedState.Postured(1f))
-            assertThat(shouldShowCommunal).isTrue()
-        }
-
-    @Test
-    fun showCommunalWhileDocked() =
-        kosmos.runTest {
-            fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
-            fakeUserRepository.setSelectedUserInfo(mainUser)
-            fakeKeyguardRepository.setKeyguardShowing(true)
-            fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, 1, mainUser.id)
-
-            batteryRepository.fake.setDevicePluggedIn(true)
-            fakeDockManager.setIsDocked(false)
-
-            val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal)
-            assertThat(shouldShowCommunal).isFalse()
-
-            fakeDockManager.setIsDocked(true)
-            fakeDockManager.setDockEvent(DockManager.STATE_DOCKED)
-            assertThat(shouldShowCommunal).isTrue()
-        }
-
     private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) {
         whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id)))
             .thenReturn(disabledFlags)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 8dc7a33..b8dbc9f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -32,9 +32,9 @@
 import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
-import com.android.systemui.common.data.repository.batteryRepository
-import com.android.systemui.common.data.repository.fake
 import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
+import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN
+import com.android.systemui.communal.data.model.SuppressionReason
 import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
 import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
 import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository
@@ -50,6 +50,7 @@
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
 import com.android.systemui.communal.domain.interactor.communalTutorialInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalEnabled
 import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.log.CommunalMetricsLogger
@@ -101,7 +102,6 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.data.repository.fakeUserRepository
-import com.android.systemui.util.settings.fakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.advanceTimeBy
@@ -128,7 +128,9 @@
 @RunWith(ParameterizedAndroidJunit4::class)
 class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     @Mock private lateinit var mediaHost: MediaHost
+
     @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
+
     @Mock private lateinit var metricsLogger: CommunalMetricsLogger
 
     private val kosmos = testKosmos()
@@ -212,11 +214,8 @@
     @Test
     fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() =
         testScope.runTest {
-            // Keyguard showing, storage unlocked, main user, and tutorial not started.
             keyguardRepository.setKeyguardShowing(true)
-            keyguardRepository.setKeyguardOccluded(false)
-            userRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
-            setIsMainUser(true)
+            kosmos.setCommunalEnabled(true)
             tutorialRepository.setTutorialSettingState(
                 Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED
             )
@@ -951,21 +950,16 @@
     fun swipeToCommunal() =
         kosmos.runTest {
             setCommunalV2ConfigEnabled(true)
-            val mainUser = fakeUserRepository.asMainUser()
-            fakeKeyguardRepository.setKeyguardShowing(true)
-            fakeUserRepository.setUserUnlocked(mainUser.id, true)
-            fakeUserTracker.set(userInfos = listOf(mainUser), selectedUserIndex = 0)
-            fakeSettings.putIntForUser(
-                Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
-                1,
-                mainUser.id,
+            // Suppress manual opening
+            communalSettingsInteractor.setSuppressionReasons(
+                listOf(SuppressionReason.ReasonUnknown(FEATURE_MANUAL_OPEN))
             )
 
             val viewModel = createViewModel()
             val swipeToHubEnabled by collectLastValue(viewModel.swipeToHubEnabled)
             assertThat(swipeToHubEnabled).isFalse()
 
-            batteryRepository.fake.setDevicePluggedIn(true)
+            communalSettingsInteractor.setSuppressionReasons(emptyList())
             assertThat(swipeToHubEnabled).isTrue()
 
             keyguardTransitionRepository.sendTransitionStep(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index c15f797..df10d05 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.communal.widgets
 
 import android.content.pm.UserInfo
-import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
@@ -25,6 +24,7 @@
 import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalEnabled
 import com.android.systemui.communal.shared.model.FakeGlanceableHubMultiUserHelper
 import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper
 import com.android.systemui.coroutines.collectLastValue
@@ -37,11 +37,9 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.settings.fakeUserTracker
 import com.android.systemui.testKosmos
-import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID
 import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.user.domain.interactor.userLockedInteractor
 import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.fakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.test.runCurrent
@@ -282,22 +280,12 @@
             }
         }
 
-    private suspend fun setCommunalAvailable(
-        available: Boolean,
-        setKeyguardShowing: Boolean = true,
-    ) =
+    private fun setCommunalAvailable(available: Boolean, setKeyguardShowing: Boolean = true) =
         with(kosmos) {
-            fakeUserRepository.setUserUnlocked(MAIN_USER_ID, true)
-            fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
+            setCommunalEnabled(available)
             if (setKeyguardShowing) {
                 fakeKeyguardRepository.setKeyguardShowing(true)
             }
-            val settingsValue = if (available) 1 else 0
-            fakeSettings.putIntForUser(
-                Settings.Secure.GLANCEABLE_HUB_ENABLED,
-                settingsValue,
-                MAIN_USER_INFO.id,
-            )
         }
 
     private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
index 6c4325a..046d92d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
@@ -35,7 +35,6 @@
 import android.os.PowerManager
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
-import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags
@@ -43,8 +42,6 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
-import com.android.systemui.common.data.repository.batteryRepository
-import com.android.systemui.common.data.repository.fake
 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.communal.domain.interactor.setCommunalV2Available
@@ -72,7 +69,6 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 import com.android.systemui.testKosmos
-import com.android.systemui.util.settings.fakeSettings
 import com.google.common.truth.Truth
 import junit.framework.Assert.assertEquals
 import kotlinx.coroutines.runBlocking
@@ -433,9 +429,7 @@
     @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
     fun testTransitionToGlanceableHub_onWakeUpFromAod() =
         kosmos.runTest {
-            val user = setCommunalV2Available(true)
-            fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1, user.id)
-            batteryRepository.fake.setDevicePluggedIn(true)
+            setCommunalV2Available(true)
 
             val currentScene by collectLastValue(communalSceneInteractor.currentScene)
             fakeCommunalSceneRepository.changeScene(CommunalScenes.Blank)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index 9be786f..096c3da 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -193,6 +193,7 @@
 
     @Test
     @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
     fun testTransitionToLockscreen_onWake_canNotDream_glanceableHubAvailable() =
         kosmos.runTest {
             whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
index e203a27..1dddfc1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
@@ -18,11 +18,17 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.content.Intent
+import android.os.PowerManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.provider.Settings
 import android.view.accessibility.accessibilityManagerWrapper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.internal.logging.uiEventLogger
+import com.android.systemui.Flags.FLAG_DOUBLE_TAP_TO_SLEEP
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
@@ -39,6 +45,8 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
+import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.advanceTimeBy
@@ -46,14 +54,19 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
 import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
 class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
     private val kosmos =
         testKosmos().apply {
@@ -61,17 +74,23 @@
             this.uiEventLogger = mock<UiEventLoggerFake>()
         }
 
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
     private lateinit var underTest: KeyguardTouchHandlingInteractor
 
     private val logger = kosmos.uiEventLogger
     private val testScope = kosmos.testScope
     private val keyguardRepository = kosmos.fakeKeyguardRepository
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private val secureSettingsRepository = kosmos.userAwareSecureSettingsRepository
+
+    @Mock private lateinit var powerManager: PowerManager
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, true)
+        overrideResource(com.android.internal.R.bool.config_supportDoubleTapSleep, true)
         whenever(kosmos.accessibilityManagerWrapper.getRecommendedTimeoutMillis(anyInt(), anyInt()))
             .thenAnswer { it.arguments[0] }
 
@@ -80,13 +99,13 @@
 
     @After
     fun tearDown() {
-        mContext
-            .getOrCreateTestableResources()
-            .removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled)
+        val testableResource = mContext.getOrCreateTestableResources()
+        testableResource.removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled)
+        testableResource.removeOverride(com.android.internal.R.bool.config_supportDoubleTapSleep)
     }
 
     @Test
-    fun isEnabled() =
+    fun isLongPressEnabled() =
         testScope.runTest {
             val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
             KeyguardState.values().forEach { keyguardState ->
@@ -101,7 +120,7 @@
         }
 
     @Test
-    fun isEnabled_alwaysFalseWhenQuickSettingsAreVisible() =
+    fun isLongPressEnabled_alwaysFalseWhenQuickSettingsAreVisible() =
         testScope.runTest {
             val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
             KeyguardState.values().forEach { keyguardState ->
@@ -112,7 +131,7 @@
         }
 
     @Test
-    fun isEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() =
+    fun isLongPressEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() =
         testScope.runTest {
             overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, false)
             createUnderTest()
@@ -294,6 +313,119 @@
             assertThat(isMenuVisible).isFalse()
         }
 
+    @Test
+    @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+    fun isDoubleTapEnabled_flagEnabled_userSettingEnabled_onlyTrueInLockScreenState() {
+        testScope.runTest {
+            secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+
+            val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+            KeyguardState.entries.forEach { keyguardState ->
+                setUpState(keyguardState = keyguardState)
+
+                if (keyguardState == KeyguardState.LOCKSCREEN) {
+                    assertThat(isEnabled()).isTrue()
+                } else {
+                    assertThat(isEnabled()).isFalse()
+                }
+            }
+        }
+    }
+
+    @Test
+    @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+    fun isDoubleTapEnabled_flagEnabled_userSettingDisabled_alwaysFalse() {
+        testScope.runTest {
+            secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, false)
+
+            val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+            KeyguardState.entries.forEach { keyguardState ->
+                setUpState(keyguardState = keyguardState)
+
+                assertThat(isEnabled()).isFalse()
+            }
+        }
+    }
+
+    @Test
+    @DisableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+    fun isDoubleTapEnabled_flagDisabled_userSettingEnabled_alwaysFalse() {
+        testScope.runTest {
+            secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+
+            val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+            KeyguardState.entries.forEach { keyguardState ->
+                setUpState(keyguardState = keyguardState)
+
+                assertThat(isEnabled()).isFalse()
+            }
+        }
+    }
+
+
+
+    @Test
+    @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+    fun isDoubleTapEnabled_flagEnabledAndConfigDisabled_alwaysFalse() {
+        testScope.runTest {
+            secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+            overrideResource(com.android.internal.R.bool.config_supportDoubleTapSleep, false)
+            createUnderTest()
+
+            val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+            KeyguardState.entries.forEach { keyguardState ->
+                setUpState(keyguardState = keyguardState)
+
+                assertThat(isEnabled()).isFalse()
+            }
+        }
+    }
+
+    @Test
+    @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+    fun isDoubleTapEnabled_quickSettingsVisible_alwaysFalse() {
+        testScope.runTest {
+            secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+
+            val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+            KeyguardState.entries.forEach { keyguardState ->
+                setUpState(keyguardState = keyguardState, isQuickSettingsVisible = true)
+
+                assertThat(isEnabled()).isFalse()
+            }
+        }
+    }
+
+    @Test
+    @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+    fun onDoubleClick_doubleTapEnabled() {
+        testScope.runTest {
+            secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+            val isEnabled by collectLastValue(underTest.isDoubleTapHandlingEnabled)
+            runCurrent()
+
+            underTest.onDoubleClick()
+
+            assertThat(isEnabled).isTrue()
+            verify(powerManager).goToSleep(anyLong())
+        }
+    }
+
+    @Test
+    @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+    fun onDoubleClick_doubleTapDisabled() {
+        testScope.runTest {
+            secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, false)
+            val isEnabled by collectLastValue(underTest.isDoubleTapHandlingEnabled)
+            runCurrent()
+
+            underTest.onDoubleClick()
+
+            assertThat(isEnabled).isFalse()
+            verify(powerManager, never()).goToSleep(anyLong())
+        }
+    }
+
     private suspend fun createUnderTest(isRevampedWppFeatureEnabled: Boolean = true) {
         // This needs to be re-created for each test outside of kosmos since the flag values are
         // read during initialization to set up flows. Maybe there is a better way to handle that.
@@ -309,6 +441,9 @@
                 accessibilityManager = kosmos.accessibilityManagerWrapper,
                 pulsingGestureListener = kosmos.pulsingGestureListener,
                 faceAuthInteractor = kosmos.deviceEntryFaceAuthInteractor,
+                secureSettingsRepository = secureSettingsRepository,
+                powerManager = powerManager,
+                systemClock = kosmos.fakeSystemClock,
             )
         setUpState()
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 8df70ef..7d5e9a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -33,7 +33,7 @@
 import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.communal.domain.interactor.communalSceneTransitionInteractor
-import com.android.systemui.communal.domain.interactor.setCommunalAvailable
+import com.android.systemui.communal.domain.interactor.setCommunalV2Available
 import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
 import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled
 import com.android.systemui.communal.shared.model.CommunalScenes
@@ -1004,16 +1004,15 @@
     @BrokenWithSceneContainer(339465026)
     fun occludedToGlanceableHub_communalKtfRefactor() =
         testScope.runTest {
-            // GIVEN a device on lockscreen and communal is available
-            keyguardRepository.setKeyguardShowing(true)
-            kosmos.setCommunalAvailable(true)
-            runCurrent()
-
             // GIVEN a prior transition has run to OCCLUDED from GLANCEABLE_HUB
             runTransitionAndSetWakefulness(KeyguardState.GLANCEABLE_HUB, KeyguardState.OCCLUDED)
             keyguardRepository.setKeyguardOccluded(true)
             runCurrent()
 
+            // GIVEN a device on lockscreen and communal is available
+            kosmos.setCommunalV2Available(true)
+            runCurrent()
+
             // WHEN occlusion ends
             keyguardRepository.setKeyguardOccluded(false)
             runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt
index 68a591d..1d42424 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt
@@ -16,11 +16,9 @@
 
 package com.android.systemui.qs.panels.ui.viewmodel
 
-
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.FakeQSTile
 import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository
@@ -48,45 +46,43 @@
     }
 
     @Test
-    fun changeTileDetailsViewModel() = with(kosmos) {
-        testScope.runTest {
-            val specs = listOf(
-                spec,
-                specNoDetails,
-            )
-            tileSpecRepository.setTiles(0, specs)
-            runCurrent()
+    fun changeTileDetailsViewModel() =
+        with(kosmos) {
+            testScope.runTest {
+                val specs = listOf(spec, specNoDetails)
+                tileSpecRepository.setTiles(0, specs)
+                runCurrent()
 
-            val tiles = currentTilesInteractor.currentTiles.value
+                val tiles = currentTilesInteractor.currentTiles.value
 
-            assertThat(currentTilesInteractor.currentTilesSpecs.size).isEqualTo(2)
-            assertThat(tiles[1].spec).isEqualTo(specNoDetails)
-            (tiles[1].tile as FakeQSTile).hasDetailsViewModel = false
+                assertThat(currentTilesInteractor.currentTilesSpecs.size).isEqualTo(2)
+                assertThat(tiles[1].spec).isEqualTo(specNoDetails)
+                (tiles[1].tile as FakeQSTile).hasDetailsViewModel = false
 
-            assertThat(underTest.activeTileDetails).isNull()
+                assertThat(underTest.activeTileDetails).isNull()
 
-            // Click on the tile who has the `spec`.
-            assertThat(underTest.onTileClicked(spec)).isTrue()
-            assertThat(underTest.activeTileDetails).isNotNull()
-            assertThat(underTest.activeTileDetails?.getTitle()).isEqualTo("internet")
+                // Click on the tile who has the `spec`.
+                assertThat(underTest.onTileClicked(spec)).isTrue()
+                assertThat(underTest.activeTileDetails).isNotNull()
+                assertThat(underTest.activeTileDetails?.title).isEqualTo("internet")
 
-            // Click on a tile who dose not have a valid spec.
-            assertThat(underTest.onTileClicked(null)).isFalse()
-            assertThat(underTest.activeTileDetails).isNull()
+                // Click on a tile who dose not have a valid spec.
+                assertThat(underTest.onTileClicked(null)).isFalse()
+                assertThat(underTest.activeTileDetails).isNull()
 
-            // Click again on the tile who has the `spec`.
-            assertThat(underTest.onTileClicked(spec)).isTrue()
-            assertThat(underTest.activeTileDetails).isNotNull()
-            assertThat(underTest.activeTileDetails?.getTitle()).isEqualTo("internet")
+                // Click again on the tile who has the `spec`.
+                assertThat(underTest.onTileClicked(spec)).isTrue()
+                assertThat(underTest.activeTileDetails).isNotNull()
+                assertThat(underTest.activeTileDetails?.title).isEqualTo("internet")
 
-            // Click on a tile who dose not have a detailed view.
-            assertThat(underTest.onTileClicked(specNoDetails)).isFalse()
-            assertThat(underTest.activeTileDetails).isNull()
+                // Click on a tile who dose not have a detailed view.
+                assertThat(underTest.onTileClicked(specNoDetails)).isFalse()
+                assertThat(underTest.activeTileDetails).isNull()
 
-            underTest.closeDetailedView()
-            assertThat(underTest.activeTileDetails).isNull()
+                underTest.closeDetailedView()
+                assertThat(underTest.activeTileDetails).isNull()
 
-            assertThat(underTest.onTileClicked(null)).isFalse()
+                assertThat(underTest.onTileClicked(null)).isFalse()
+            }
         }
-    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index c308976..5bde7ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -691,11 +691,11 @@
                 var currentModel: TileDetailsViewModel? = null
                 val setCurrentModel = { model: TileDetailsViewModel? -> currentModel = model }
                 tiles!![0].tile.getDetailsViewModel(setCurrentModel)
-                assertThat(currentModel?.getTitle()).isEqualTo("a")
+                assertThat(currentModel?.title).isEqualTo("a")
 
                 currentModel = null
                 tiles!![1].tile.getDetailsViewModel(setCurrentModel)
-                assertThat(currentModel?.getTitle()).isEqualTo("b")
+                assertThat(currentModel?.title).isEqualTo("b")
 
                 currentModel = null
                 tiles!![2].tile.getDetailsViewModel(setCurrentModel)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
index c5752691d..65763a3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
@@ -45,6 +45,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.SbnBuilder;
+import com.android.systemui.statusbar.notification.collection.BundleEntry;
 import com.android.systemui.statusbar.notification.collection.ListEntry;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -273,6 +274,18 @@
     }
 
     @Test
+    public void testSilentSectioner_acceptsBundle() {
+        BundleEntry bundleEntry = new BundleEntry("testBundleKey");
+        assertTrue(mSilentSectioner.isInSection(bundleEntry));
+    }
+
+    @Test
+    public void testMinimizedSectioner_rejectsBundle() {
+        BundleEntry bundleEntry = new BundleEntry("testBundleKey");
+        assertFalse(mMinimizedSectioner.isInSection(bundleEntry));
+    }
+
+    @Test
     public void testMinSection() {
         when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
         setRankingAmbient(true);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 7847437..e6b2c25 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -364,6 +364,7 @@
                 .setUid(UID)
                 .setInitialPid(2000)
                 .setNotification(summary)
+                .setUser(USER_HANDLE)
                 .setParent(GroupEntry.ROOT_ENTRY)
                 .build();
         GroupEntryBuilder groupEntry = new GroupEntryBuilder()
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
index b52db83..7657a21 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
@@ -13,7 +13,6 @@
  */
 package com.android.systemui.plugins.clocks
 
-import android.graphics.RectF
 import com.android.systemui.plugins.annotations.ProtectedInterface
 import com.android.systemui.plugins.annotations.SimpleProperty
 import java.io.PrintWriter
@@ -50,5 +49,5 @@
 }
 
 interface ClockEventListener {
-    fun onBoundsChanged(bounds: RectF)
+    fun onBoundsChanged(bounds: VRectF)
 }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
index 20ee6c1..a658c15 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
@@ -45,6 +45,7 @@
      * render within the centered targetRect to avoid obstructing other elements. The specified
      * targetRegion is relative to the parent view.
      */
+    @Deprecated("No longer necessary, pending removal")
     fun onTargetRegionChanged(targetRegion: Rect?)
 
     /** Called to notify the clock about its display. */
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
index 02a3902..f9ff75d 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.plugins.clocks
 
-import android.graphics.Rect
 import android.view.View
 import android.view.View.MeasureSpec
 import com.android.systemui.log.core.LogLevel
@@ -56,12 +55,9 @@
     }
 
     fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
-        d({ "onLayout($bool1, ${Rect(int1, int2, long1.toInt(), long2.toInt())})" }) {
+        d({ "onLayout($bool1, ${VRect(long1.toULong())})" }) {
             bool1 = changed
-            int1 = left
-            int2 = top
-            long1 = right.toLong()
-            long2 = bottom.toLong()
+            long1 = VRect(left, top, right, bottom).data.toLong()
         }
     }
 
@@ -108,8 +104,11 @@
         }
     }
 
-    fun animateDoze() {
-        d("animateDoze()")
+    fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+        d({ "animateDoze(isDozing=$bool1, isAnimated=$bool2)" }) {
+            bool1 = isDozing
+            bool2 = isAnimated
+        }
     }
 
     fun animateCharge() {
@@ -117,10 +116,7 @@
     }
 
     fun animateFidget(x: Float, y: Float) {
-        d({ "animateFidget($str1, $str2)" }) {
-            str1 = x.toString()
-            str2 = y.toString()
-        }
+        d({ "animateFidget(${VPointF(long1.toULong())})" }) { long1 = VPointF(x, y).data.toLong() }
     }
 
     companion object {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt
similarity index 94%
rename from packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt
index 3dae530..1fb37ec 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.shared.clocks
+package com.android.systemui.plugins.clocks
 
 import android.graphics.Point
 import android.graphics.PointF
@@ -30,22 +30,20 @@
 
 private fun unpackX(data: ULong): Int = ((data and X_MASK) shr 32).toInt()
 
-private fun unpackY(data: ULong): Int = (data and Y_MASK).toInt()
+private fun unpackY(data: ULong): Int = ((data and Y_MASK) shr 0).toInt()
 
 private fun pack(x: Int, y: Int): ULong {
-    return ((x.toULong() shl 32) and X_MASK) or (y.toULong() and Y_MASK)
+    return ((x.toULong() shl 32) and X_MASK) or ((y.toULong() shl 0) and Y_MASK)
 }
 
 @JvmInline
-value class VPointF(private val data: ULong) {
+value class VPointF(val data: ULong) {
     val x: Float
         get() = Float.fromBits(unpackX(data))
 
     val y: Float
         get() = Float.fromBits(unpackY(data))
 
-    constructor() : this(0f, 0f)
-
     constructor(pt: PointF) : this(pt.x, pt.y)
 
     constructor(x: Int, y: Int) : this(x.toFloat(), y.toFloat())
@@ -139,15 +137,13 @@
 }
 
 @JvmInline
-value class VPoint(private val data: ULong) {
+value class VPoint(val data: ULong) {
     val x: Int
         get() = unpackX(data)
 
     val y: Int
         get() = unpackY(data)
 
-    constructor() : this(0, 0)
-
     constructor(x: Int, y: Int) : this(pack(x, y))
 
     fun toPoint() = Point(x, y)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt
new file mode 100644
index 0000000..3c1adf2
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2025 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.plugins.clocks
+
+import android.graphics.Rect
+import android.graphics.RectF
+import android.util.Half
+
+private val LEFT_MASK: ULong = 0xFFFF000000000000U
+private val TOP_MASK: ULong = 0x0000FFFF00000000U
+private val RIGHT_MASK: ULong = 0x00000000FFFF0000U
+private val BOTTOM_MASK: ULong = 0x000000000000FFFFU
+
+private fun unpackLeft(data: ULong): Short = ((data and LEFT_MASK) shr 48).toShort()
+
+private fun unpackTop(data: ULong): Short = ((data and TOP_MASK) shr 32).toShort()
+
+private fun unpackRight(data: ULong): Short = ((data and RIGHT_MASK) shr 16).toShort()
+
+private fun unpackBottom(data: ULong): Short = ((data and BOTTOM_MASK) shr 0).toShort()
+
+private fun pack(left: Short, top: Short, right: Short, bottom: Short): ULong {
+    return ((left.toULong() shl 48) and LEFT_MASK) or
+        ((top.toULong() shl 32) and TOP_MASK) or
+        ((right.toULong() shl 16) and RIGHT_MASK) or
+        ((bottom.toULong() shl 0) and BOTTOM_MASK)
+}
+
+@JvmInline
+value class VRectF(val data: ULong) {
+    val left: Float
+        get() = fromBits(unpackLeft(data))
+
+    val top: Float
+        get() = fromBits(unpackTop(data))
+
+    val right: Float
+        get() = fromBits(unpackRight(data))
+
+    val bottom: Float
+        get() = fromBits(unpackBottom(data))
+
+    val width: Float
+        get() = right - left
+
+    val height: Float
+        get() = bottom - top
+
+    constructor(rect: RectF) : this(rect.left, rect.top, rect.right, rect.bottom)
+
+    constructor(
+        rect: Rect
+    ) : this(
+        left = rect.left.toFloat(),
+        top = rect.top.toFloat(),
+        right = rect.right.toFloat(),
+        bottom = rect.bottom.toFloat(),
+    )
+
+    constructor(
+        left: Float,
+        top: Float,
+        right: Float,
+        bottom: Float,
+    ) : this(pack(toBits(left), toBits(top), toBits(right), toBits(bottom)))
+
+    val center: VPointF
+        get() = VPointF(left, top) + size / 2f
+
+    val size: VPointF
+        get() = VPointF(width, height)
+
+    override fun toString() = "($left, $top) -> ($right, $bottom)"
+
+    companion object {
+        private fun toBits(value: Float): Short = Half.halfToShortBits(Half.toHalf(value))
+
+        private fun fromBits(value: Short): Float = Half.toFloat(Half.intBitsToHalf(value.toInt()))
+
+        fun fromCenter(center: VPointF, size: VPointF): VRectF {
+            return VRectF(
+                center.x - size.x / 2,
+                center.y - size.y / 2,
+                center.x + size.x / 2,
+                center.y + size.y / 2,
+            )
+        }
+
+        fun fromTopLeft(pos: VPointF, size: VPointF): VRectF {
+            return VRectF(pos.x, pos.y, pos.x + size.x, pos.y + size.y)
+        }
+
+        val ZERO = VRectF(0f, 0f, 0f, 0f)
+    }
+}
+
+@JvmInline
+value class VRect(val data: ULong) {
+    val left: Int
+        get() = unpackLeft(data).toInt()
+
+    val top: Int
+        get() = unpackTop(data).toInt()
+
+    val right: Int
+        get() = unpackRight(data).toInt()
+
+    val bottom: Int
+        get() = unpackBottom(data).toInt()
+
+    val width: Int
+        get() = right - left
+
+    val height: Int
+        get() = bottom - top
+
+    constructor(
+        rect: Rect
+    ) : this(
+        left = rect.left.toShort(),
+        top = rect.top.toShort(),
+        right = rect.right.toShort(),
+        bottom = rect.bottom.toShort(),
+    )
+
+    constructor(
+        left: Int,
+        top: Int,
+        right: Int,
+        bottom: Int,
+    ) : this(
+        left = left.toShort(),
+        top = top.toShort(),
+        right = right.toShort(),
+        bottom = bottom.toShort(),
+    )
+
+    constructor(
+        left: Short,
+        top: Short,
+        right: Short,
+        bottom: Short,
+    ) : this(pack(left, top, right, bottom))
+
+    val center: VPoint
+        get() = VPoint(left, top) + size / 2
+
+    val size: VPoint
+        get() = VPoint(width, height)
+
+    override fun toString() = "($left, $top) -> ($right, $bottom)"
+
+    companion object {
+        val ZERO = VRect(0, 0, 0, 0)
+
+        fun fromCenter(center: VPoint, size: VPoint): VRect {
+            return VRect(
+                (center.x - size.x / 2).toShort(),
+                (center.y - size.y / 2).toShort(),
+                (center.x + size.x / 2).toShort(),
+                (center.y + size.y / 2).toShort(),
+            )
+        }
+
+        fun fromTopLeft(pos: VPoint, size: VPoint): VRect {
+            return VRect(
+                pos.x.toShort(),
+                pos.y.toShort(),
+                (pos.x + size.x).toShort(),
+                (pos.y + size.y).toShort(),
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt
index be0362f..ac7a8574 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt
@@ -16,15 +16,13 @@
 
 package com.android.systemui.plugins.qs
 
-/**
- * The base view model class for rendering the Tile's TileDetailsView.
- */
-abstract class TileDetailsViewModel {
+/** The view model interface for rendering the Tile's TileDetailsView. */
+interface TileDetailsViewModel {
     // The callback when the settings button is clicked. Currently this is the same as the on tile
     // long press callback
-    abstract fun clickOnSettingsButton()
+    fun clickOnSettingsButton()
 
-    abstract fun getTitle(): String
+    val title: String
 
-    abstract fun getSubTitle(): String
+    val subTitle: String
 }
diff --git a/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json
index b1d6a27..3f03fcf 100644
--- a/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json
+++ b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json
@@ -1 +1 @@
-{"v":"5.7.13","fr":60,"ip":0,"op":55,"w":80,"h":80,"nm":"unlocked_to_checkmark_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.143,32,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.761,0],[0,-2.7],[0,0]],"o":[[0,0],[0,-2.7],[-2.761,0],[0,0],[0,0]],"v":[[5,5],[5,-0.111],[0,-5],[-5,-0.111],[-5.01,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[37.999,44.999,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.42,0],[0,1.42],[1.42,0],[0,-1.42]],"o":[[1.42,0],[0,-1.42],[-1.42,0],[0,1.42]],"v":[[0,2.571],[2.571,0],[0,-2.571],[-2.571,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.5,40.75,0],"ix":2,"l":2},"a":{"a":0,"k":[12.5,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[112,112,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":77,"st":10,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".grey700","cl":"grey700","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
+{"v":"5.7.13","fr":60,"ip":0,"op":55,"w":80,"h":80,"nm":"unlocked_to_checkmark_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.143,32,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.761,0],[0,-2.7],[0,0]],"o":[[0,0],[0,-2.7],[-2.761,0],[0,0],[0,0]],"v":[[5,5],[5,-0.111],[0,-5],[-5,-0.111],[-5.01,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[37.999,44.999,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.42,0],[0,1.42],[1.42,0],[0,-1.42]],"o":[[1.42,0],[0,-1.42],[-1.42,0],[0,1.42]],"v":[[0,2.571],[2.571,0],[0,-2.571],[-2.571,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.5,40.75,0],"ix":2,"l":2},"a":{"a":0,"k":[12.5,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[112,112,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":77,"st":10,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 1549b69..763b107 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -21,7 +21,6 @@
 import android.content.Intent
 import android.content.IntentFilter
 import android.content.res.Resources
-import android.graphics.RectF
 import android.os.Trace
 import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
 import android.provider.Settings.Global.ZEN_MODE_OFF
@@ -60,6 +59,7 @@
 import com.android.systemui.plugins.clocks.ClockFaceController
 import com.android.systemui.plugins.clocks.ClockMessageBuffers
 import com.android.systemui.plugins.clocks.ClockTickRate
+import com.android.systemui.plugins.clocks.VRectF
 import com.android.systemui.plugins.clocks.WeatherData
 import com.android.systemui.plugins.clocks.ZenData
 import com.android.systemui.plugins.clocks.ZenData.ZenMode
@@ -250,7 +250,7 @@
     private var largeClockOnSecondaryDisplay = false
 
     val dozeAmount = MutableStateFlow(0f)
-    val onClockBoundsChanged = MutableStateFlow<RectF?>(null)
+    val onClockBoundsChanged = MutableStateFlow<VRectF>(VRectF.ZERO)
 
     private fun isDarkTheme(): Boolean {
         val isLightTheme = TypedValue()
@@ -315,7 +315,7 @@
 
     private val clockListener =
         object : ClockEventListener {
-            override fun onBoundsChanged(bounds: RectF) {
+            override fun onBoundsChanged(bounds: VRectF) {
                 onClockBoundsChanged.value = bounds
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
index 5863a93..7d8752e 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
@@ -21,18 +21,14 @@
 class BluetoothDetailsViewModel(
     private val onSettingsClick: () -> Unit,
     val detailsContentViewModel: BluetoothDetailsContentViewModel,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
     override fun clickOnSettingsButton() {
         onSettingsClick()
     }
 
-    override fun getTitle(): String {
-        // TODO: b/378513956 Update the placeholder text
-        return "Bluetooth"
-    }
+    // TODO: b/378513956 Update the placeholder text
+    override val title = "Bluetooth"
 
-    override fun getSubTitle(): String {
-        // TODO: b/378513956 Update the placeholder text
-        return "Tap to connect or disconnect a device"
-    }
+    // TODO: b/378513956 Update the placeholder text
+    override val subTitle = "Tap to connect or disconnect a device"
 }
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt
index 42f1b73..6c3535a 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt
@@ -27,17 +27,17 @@
 import android.view.accessibility.AccessibilityNodeInfo
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import com.android.systemui.Flags.doubleTapToSleep
 import com.android.systemui.log.TouchHandlingViewLogger
 import com.android.systemui.shade.TouchLogger
-import kotlin.math.pow
-import kotlin.math.sqrt
 import kotlinx.coroutines.DisposableHandle
 
 /**
- * View designed to handle long-presses.
+ * View designed to handle long-presses and double taps.
  *
- * The view will not handle any long pressed by default. To set it up, set up a listener and, when
- * ready to start consuming long-presses, set [setLongPressHandlingEnabled] to `true`.
+ * The view will not handle any gestures by default. To set it up, set up a listener and, when ready
+ * to start consuming gestures, set the gesture's enable function ([setLongPressHandlingEnabled],
+ * [setDoublePressHandlingEnabled]) to `true`.
  */
 class TouchHandlingView(
     context: Context,
@@ -62,6 +62,9 @@
 
         /** Notifies that the gesture was too short for a long press, it is actually a click. */
         fun onSingleTapDetected(view: View, x: Int, y: Int) = Unit
+
+        /** Notifies that a double tap has been detected by the given view. */
+        fun onDoubleTapDetected(view: View) = Unit
     }
 
     var listener: Listener? = null
@@ -70,6 +73,7 @@
 
     private val interactionHandler: TouchHandlingViewInteractionHandler by lazy {
         TouchHandlingViewInteractionHandler(
+            context = context,
             postDelayed = { block, timeoutMs ->
                 val dispatchToken = Any()
 
@@ -84,6 +88,9 @@
             onSingleTapDetected = { x, y ->
                 listener?.onSingleTapDetected(this@TouchHandlingView, x = x, y = y)
             },
+            onDoubleTapDetected = {
+                if (doubleTapToSleep()) listener?.onDoubleTapDetected(this@TouchHandlingView)
+            },
             longPressDuration = longPressDuration,
             allowedTouchSlop = allowedTouchSlop,
             logger = logger,
@@ -100,13 +107,17 @@
         interactionHandler.isLongPressHandlingEnabled = isEnabled
     }
 
+    fun setDoublePressHandlingEnabled(isEnabled: Boolean) {
+        interactionHandler.isDoubleTapHandlingEnabled = isEnabled
+    }
+
     override fun dispatchTouchEvent(event: MotionEvent): Boolean {
         return TouchLogger.logDispatchTouch("long_press", event, super.dispatchTouchEvent(event))
     }
 
     @SuppressLint("ClickableViewAccessibility")
-    override fun onTouchEvent(event: MotionEvent?): Boolean {
-        return interactionHandler.onTouchEvent(event?.toModel())
+    override fun onTouchEvent(event: MotionEvent): Boolean {
+        return interactionHandler.onTouchEvent(event)
     }
 
     private fun setupAccessibilityDelegate() {
@@ -154,33 +165,3 @@
             }
     }
 }
-
-private fun MotionEvent.toModel(): TouchHandlingViewInteractionHandler.MotionEventModel {
-    return when (actionMasked) {
-        MotionEvent.ACTION_DOWN ->
-            TouchHandlingViewInteractionHandler.MotionEventModel.Down(x = x.toInt(), y = y.toInt())
-        MotionEvent.ACTION_MOVE ->
-            TouchHandlingViewInteractionHandler.MotionEventModel.Move(
-                distanceMoved = distanceMoved()
-            )
-        MotionEvent.ACTION_UP ->
-            TouchHandlingViewInteractionHandler.MotionEventModel.Up(
-                distanceMoved = distanceMoved(),
-                gestureDuration = gestureDuration(),
-            )
-        MotionEvent.ACTION_CANCEL -> TouchHandlingViewInteractionHandler.MotionEventModel.Cancel
-        else -> TouchHandlingViewInteractionHandler.MotionEventModel.Other
-    }
-}
-
-private fun MotionEvent.distanceMoved(): Float {
-    return if (historySize > 0) {
-        sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2))
-    } else {
-        0f
-    }
-}
-
-private fun MotionEvent.gestureDuration(): Long {
-    return eventTime - downTime
-}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt
index 5863fc6..fe509d7 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt
@@ -17,12 +17,20 @@
 
 package com.android.systemui.common.ui.view
 
+import android.content.Context
 import android.graphics.Point
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.ViewConfiguration
 import com.android.systemui.log.TouchHandlingViewLogger
+import kotlin.math.pow
+import kotlin.math.sqrt
+import kotlin.properties.Delegates
 import kotlinx.coroutines.DisposableHandle
 
 /** Encapsulates logic to handle complex touch interactions with a [TouchHandlingView]. */
 class TouchHandlingViewInteractionHandler(
+    context: Context,
     /**
      * Callback to run the given [Runnable] with the given delay, returning a [DisposableHandle]
      * allowing the delayed runnable to be canceled before it is run.
@@ -34,6 +42,8 @@
     private val onLongPressDetected: (x: Int, y: Int) -> Unit,
     /** Callback reporting the a single tap gesture was detected at the given coordinates. */
     private val onSingleTapDetected: (x: Int, y: Int) -> Unit,
+    /** Callback reporting that a double tap gesture was detected. */
+    private val onDoubleTapDetected: () -> Unit,
     /** Time for the touch to be considered a long-press in ms */
     var longPressDuration: () -> Long,
     /**
@@ -58,48 +68,98 @@
     }
 
     var isLongPressHandlingEnabled: Boolean = false
+    var isDoubleTapHandlingEnabled: Boolean = false
     var scheduledLongPressHandle: DisposableHandle? = null
 
+    private var doubleTapAwaitingUp: Boolean = false
+    private var lastDoubleTapDownEventTime: Long? = null
+
     /** Record coordinate for last DOWN event for single tap */
     val lastEventDownCoordinate = Point(-1, -1)
 
-    fun onTouchEvent(event: MotionEventModel?): Boolean {
-        if (!isLongPressHandlingEnabled) {
-            return false
-        }
-        return when (event) {
-            is MotionEventModel.Down -> {
-                scheduleLongPress(event.x, event.y)
-                lastEventDownCoordinate.x = event.x
-                lastEventDownCoordinate.y = event.y
-                true
+    private val gestureDetector =
+        GestureDetector(
+            context,
+            object : GestureDetector.SimpleOnGestureListener() {
+                override fun onDoubleTap(event: MotionEvent): Boolean {
+                    if (isDoubleTapHandlingEnabled) {
+                        doubleTapAwaitingUp = true
+                        lastDoubleTapDownEventTime = event.eventTime
+                        return true
+                    }
+                    return false
+                }
+            },
+        )
+
+    fun onTouchEvent(event: MotionEvent): Boolean {
+        if (isDoubleTapHandlingEnabled) {
+            gestureDetector.onTouchEvent(event)
+            if (event.actionMasked == MotionEvent.ACTION_UP && doubleTapAwaitingUp) {
+                lastDoubleTapDownEventTime?.let { time ->
+                    if (
+                        event.eventTime - time < ViewConfiguration.getDoubleTapTimeout()
+                    ) {
+                        cancelScheduledLongPress()
+                        onDoubleTapDetected()
+                    }
+                }
+                doubleTapAwaitingUp = false
+            } else if (event.actionMasked == MotionEvent.ACTION_CANCEL && doubleTapAwaitingUp) {
+                doubleTapAwaitingUp = false
             }
-            is MotionEventModel.Move -> {
-                if (event.distanceMoved > allowedTouchSlop) {
-                    logger?.cancelingLongPressDueToTouchSlop(event.distanceMoved, allowedTouchSlop)
+        }
+
+        if (isLongPressHandlingEnabled) {
+            val motionEventModel = event.toModel()
+
+            return when (motionEventModel) {
+                is MotionEventModel.Down -> {
+                    scheduleLongPress(motionEventModel.x, motionEventModel.y)
+                    lastEventDownCoordinate.x = motionEventModel.x
+                    lastEventDownCoordinate.y = motionEventModel.y
+                    true
+                }
+
+                is MotionEventModel.Move -> {
+                    if (motionEventModel.distanceMoved > allowedTouchSlop) {
+                        logger?.cancelingLongPressDueToTouchSlop(
+                            motionEventModel.distanceMoved,
+                            allowedTouchSlop,
+                        )
+                        cancelScheduledLongPress()
+                    }
+                    false
+                }
+
+                is MotionEventModel.Up -> {
+                    logger?.onUpEvent(
+                        motionEventModel.distanceMoved,
+                        allowedTouchSlop,
+                        motionEventModel.gestureDuration,
+                    )
                     cancelScheduledLongPress()
+                    if (
+                        motionEventModel.distanceMoved <= allowedTouchSlop &&
+                            motionEventModel.gestureDuration < longPressDuration()
+                    ) {
+                        logger?.dispatchingSingleTap()
+                        dispatchSingleTap(lastEventDownCoordinate.x, lastEventDownCoordinate.y)
+                    }
+                    false
                 }
-                false
-            }
-            is MotionEventModel.Up -> {
-                logger?.onUpEvent(event.distanceMoved, allowedTouchSlop, event.gestureDuration)
-                cancelScheduledLongPress()
-                if (
-                    event.distanceMoved <= allowedTouchSlop &&
-                        event.gestureDuration < longPressDuration()
-                ) {
-                    logger?.dispatchingSingleTap()
-                    dispatchSingleTap(lastEventDownCoordinate.x, lastEventDownCoordinate.y)
+
+                is MotionEventModel.Cancel -> {
+                    logger?.motionEventCancelled()
+                    cancelScheduledLongPress()
+                    false
                 }
-                false
+
+                else -> false
             }
-            is MotionEventModel.Cancel -> {
-                logger?.motionEventCancelled()
-                cancelScheduledLongPress()
-                false
-            }
-            else -> false
         }
+
+        return false
     }
 
     private fun scheduleLongPress(x: Int, y: Int) {
@@ -134,4 +194,30 @@
 
         onSingleTapDetected(x, y)
     }
+
+    private fun MotionEvent.toModel(): MotionEventModel {
+        return when (actionMasked) {
+            MotionEvent.ACTION_DOWN -> MotionEventModel.Down(x = x.toInt(), y = y.toInt())
+            MotionEvent.ACTION_MOVE -> MotionEventModel.Move(distanceMoved = distanceMoved())
+            MotionEvent.ACTION_UP ->
+                MotionEventModel.Up(
+                    distanceMoved = distanceMoved(),
+                    gestureDuration = gestureDuration(),
+                )
+            MotionEvent.ACTION_CANCEL -> MotionEventModel.Cancel
+            else -> MotionEventModel.Other
+        }
+    }
+
+    private fun MotionEvent.distanceMoved(): Float {
+        return if (historySize > 0) {
+            sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2))
+        } else {
+            0f
+        }
+    }
+
+    private fun MotionEvent.gestureDuration(): Long {
+        return eventTime - downTime
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSuppressionStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSuppressionStartable.kt
new file mode 100644
index 0000000..6a611ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSuppressionStartable.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2025 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.communal
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.communal.data.model.SuppressionReason
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.dagger.CommunalTableLog
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+
+@SysUISingleton
+class CommunalSuppressionStartable
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    private val suppressionFlows: Set<@JvmSuppressWildcards Flow<SuppressionReason?>>,
+    private val communalSettingsInteractor: CommunalSettingsInteractor,
+    @CommunalTableLog private val tableLogBuffer: TableLogBuffer,
+) : CoreStartable {
+    override fun start() {
+        getSuppressionReasons()
+            .onEach { reasons -> communalSettingsInteractor.setSuppressionReasons(reasons) }
+            .logDiffsForTable(
+                tableLogBuffer = tableLogBuffer,
+                columnName = "suppressionReasons",
+                initialValue = emptyList(),
+            )
+            .flowOn(bgDispatcher)
+            .launchIn(applicationScope)
+    }
+
+    private fun getSuppressionReasons(): Flow<List<SuppressionReason>> {
+        if (!communalSettingsInteractor.isCommunalFlagEnabled()) {
+            return flowOf(listOf(SuppressionReason.ReasonFlagDisabled))
+        }
+        return combine(suppressionFlows) { reasons -> reasons.filterNotNull() }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
index bb3be53..a31c0bd 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule
 import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor
+import com.android.systemui.communal.domain.suppression.dagger.CommunalSuppressionModule
 import com.android.systemui.communal.shared.log.CommunalMetricsLogger
 import com.android.systemui.communal.shared.log.CommunalStatsLogProxyImpl
 import com.android.systemui.communal.shared.model.CommunalScenes
@@ -70,6 +71,7 @@
             CommunalSmartspaceRepositoryModule::class,
             CommunalStartableModule::class,
             GlanceableHubWidgetManagerModule::class,
+            CommunalSuppressionModule::class,
         ]
 )
 interface CommunalModule {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt
index 7358aa7..a4f75e8 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.communal.CommunalMetricsStartable
 import com.android.systemui.communal.CommunalOngoingContentStartable
 import com.android.systemui.communal.CommunalSceneStartable
+import com.android.systemui.communal.CommunalSuppressionStartable
 import com.android.systemui.communal.DevicePosturingListener
 import com.android.systemui.communal.log.CommunalLoggerStartable
 import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable
@@ -73,4 +74,9 @@
     @IntoMap
     @ClassKey(DevicePosturingListener::class)
     fun bindDevicePosturingistener(impl: DevicePosturingListener): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(CommunalSuppressionStartable::class)
+    fun bindCommunalSuppressionStartable(impl: CommunalSuppressionStartable): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt
deleted file mode 100644
index 83a5bdb..0000000
--- a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.communal.data.model
-
-import com.android.systemui.log.table.Diffable
-import com.android.systemui.log.table.TableRowLogger
-import java.util.EnumSet
-
-/** Reasons that communal is disabled, primarily for logging. */
-enum class DisabledReason(val loggingString: String) {
-    /** Communal should be disabled due to invalid current user */
-    DISABLED_REASON_INVALID_USER("invalidUser"),
-    /** Communal should be disabled due to the flag being off */
-    DISABLED_REASON_FLAG("flag"),
-    /** Communal should be disabled because the user has turned off the setting */
-    DISABLED_REASON_USER_SETTING("userSetting"),
-    /** Communal is disabled by the device policy app */
-    DISABLED_REASON_DEVICE_POLICY("devicePolicy"),
-}
-
-/**
- * Model representing the reasons communal hub should be disabled. Allows logging reasons separately
- * for debugging.
- */
-@JvmInline
-value class CommunalEnabledState(
-    private val disabledReasons: EnumSet<DisabledReason> =
-        EnumSet.noneOf(DisabledReason::class.java)
-) : Diffable<CommunalEnabledState>, Set<DisabledReason> by disabledReasons {
-
-    /** Creates [CommunalEnabledState] with a single reason for being disabled */
-    constructor(reason: DisabledReason) : this(EnumSet.of(reason))
-
-    /** Checks if there are any reasons communal should be disabled. If none, returns true. */
-    val enabled: Boolean
-        get() = isEmpty()
-
-    override fun logDiffs(prevVal: CommunalEnabledState, row: TableRowLogger) {
-        for (reason in DisabledReason.entries) {
-            val newVal = contains(reason)
-            if (newVal != prevVal.contains(reason)) {
-                row.logChange(
-                    columnName = reason.loggingString,
-                    value = newVal,
-                )
-            }
-        }
-    }
-
-    override fun logFull(row: TableRowLogger) {
-        for (reason in DisabledReason.entries) {
-            row.logChange(columnName = reason.loggingString, value = contains(reason))
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalFeature.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalFeature.kt
new file mode 100644
index 0000000..5fb1c4e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalFeature.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2025 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.communal.data.model
+
+import android.annotation.IntDef
+
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(
+    flag = true,
+    prefix = ["FEATURE_"],
+    value = [FEATURE_AUTO_OPEN, FEATURE_MANUAL_OPEN, FEATURE_ENABLED, FEATURE_ALL],
+)
+annotation class CommunalFeature
+
+/** If we should automatically open the hub */
+const val FEATURE_AUTO_OPEN: Int = 1
+
+/** If the user is allowed to manually open the hub */
+const val FEATURE_MANUAL_OPEN: Int = 1 shl 1
+
+/**
+ * If the hub should be considered enabled. If not, it may be cleaned up entirely to reduce memory
+ * footprint.
+ */
+const val FEATURE_ENABLED: Int = 1 shl 2
+
+const val FEATURE_ALL: Int = FEATURE_ENABLED or FEATURE_MANUAL_OPEN or FEATURE_AUTO_OPEN
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/SuppressionReason.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/SuppressionReason.kt
new file mode 100644
index 0000000..de05bed
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/SuppressionReason.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 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.communal.data.model
+
+sealed interface SuppressionReason {
+    @CommunalFeature val suppressedFeatures: Int
+
+    /** Whether this reason suppresses a particular feature. */
+    fun isSuppressed(@CommunalFeature feature: Int): Boolean {
+        return (suppressedFeatures and feature) != 0
+    }
+
+    /** Suppress hub automatically opening due to Android Auto projection */
+    data object ReasonCarProjection : SuppressionReason {
+        override val suppressedFeatures: Int = FEATURE_AUTO_OPEN
+    }
+
+    /** Suppress hub due to the "When to dream" conditions not being met */
+    data class ReasonWhenToAutoShow(override val suppressedFeatures: Int) : SuppressionReason
+
+    /** Suppress hub due to device policy */
+    data object ReasonDevicePolicy : SuppressionReason {
+        override val suppressedFeatures: Int = FEATURE_ALL
+    }
+
+    /** Suppress hub due to the user disabling the setting */
+    data object ReasonSettingDisabled : SuppressionReason {
+        override val suppressedFeatures: Int = FEATURE_ALL
+    }
+
+    /** Suppress hub due to the user being locked */
+    data object ReasonUserLocked : SuppressionReason {
+        override val suppressedFeatures: Int = FEATURE_ALL
+    }
+
+    /** Suppress hub due the a secondary user being active */
+    data object ReasonSecondaryUser : SuppressionReason {
+        override val suppressedFeatures: Int = FEATURE_ALL
+    }
+
+    /** Suppress hub due to the flag being disabled */
+    data object ReasonFlagDisabled : SuppressionReason {
+        override val suppressedFeatures: Int = FEATURE_ALL
+    }
+
+    /** Suppress hub due to an unknown reason, used as initial state and in tests */
+    data class ReasonUnknown(override val suppressedFeatures: Int = FEATURE_ALL) :
+        SuppressionReason
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CarProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CarProjectionRepository.kt
new file mode 100644
index 0000000..4fe641a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CarProjectionRepository.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2025 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.communal.data.repository
+
+import android.app.UiModeManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.kotlin.emitOnStart
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+interface CarProjectionRepository {
+    /** Whether car projection is active. */
+    val projectionActive: Flow<Boolean>
+
+    /**
+     * Checks the system for the current car projection state.
+     *
+     * @return True if projection is active, false otherwise.
+     */
+    suspend fun isProjectionActive(): Boolean
+}
+
+@SysUISingleton
+class CarProjectionRepositoryImpl
+@Inject
+constructor(
+    private val uiModeManager: UiModeManager,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+) : CarProjectionRepository {
+    override val projectionActive: Flow<Boolean> =
+        conflatedCallbackFlow {
+                val listener =
+                    UiModeManager.OnProjectionStateChangedListener { _, _ -> trySend(Unit) }
+                uiModeManager.addOnProjectionStateChangedListener(
+                    UiModeManager.PROJECTION_TYPE_AUTOMOTIVE,
+                    bgDispatcher.asExecutor(),
+                    listener,
+                )
+                awaitClose { uiModeManager.removeOnProjectionStateChangedListener(listener) }
+            }
+            .emitOnStart()
+            .map { isProjectionActive() }
+            .flowOn(bgDispatcher)
+
+    override suspend fun isProjectionActive(): Boolean =
+        withContext(bgDispatcher) {
+            (uiModeManager.activeProjectionTypes and UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) != 0
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt
index 7f137f3..0d590db 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt
@@ -22,4 +22,6 @@
 @Module
 interface CommunalRepositoryModule {
     @Binds fun communalRepository(impl: CommunalSceneRepositoryImpl): CommunalSceneRepository
+
+    @Binds fun carProjectionRepository(impl: CarProjectionRepositoryImpl): CarProjectionRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
index 4c291a0..6f688d1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
@@ -26,12 +26,9 @@
 import com.android.systemui.Flags.communalHub
 import com.android.systemui.Flags.glanceableHubV2
 import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.communal.data.model.CommunalEnabledState
-import com.android.systemui.communal.data.model.DisabledReason
-import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_DEVICE_POLICY
-import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_FLAG
-import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_INVALID_USER
-import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_USER_SETTING
+import com.android.systemui.communal.data.model.CommunalFeature
+import com.android.systemui.communal.data.model.FEATURE_ALL
+import com.android.systemui.communal.data.model.SuppressionReason
 import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryModule.Companion.DEFAULT_BACKGROUND_TYPE
 import com.android.systemui.communal.shared.model.CommunalBackgroundType
 import com.android.systemui.communal.shared.model.WhenToDream
@@ -43,22 +40,23 @@
 import com.android.systemui.util.kotlin.emitOnStart
 import com.android.systemui.util.settings.SecureSettings
 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
-import java.util.EnumSet
 import javax.inject.Inject
 import javax.inject.Named
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
 
 interface CommunalSettingsRepository {
-    /** A [CommunalEnabledState] for the specified user. */
-    fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState>
+    /** Whether a particular feature is enabled */
+    fun isEnabled(@CommunalFeature feature: Int): Flow<Boolean>
 
-    fun getScreensaverEnabledState(user: UserInfo): Flow<Boolean>
+    /**
+     * Suppresses the hub with the given reasons. If there are no reasons, the hub will not be
+     * suppressed.
+     */
+    fun setSuppressionReasons(reasons: List<SuppressionReason>)
 
     /**
      * Returns a [WhenToDream] for the specified user, indicating what state the device should be in
@@ -66,6 +64,9 @@
      */
     fun getWhenToDreamState(user: UserInfo): Flow<WhenToDream>
 
+    /** Returns whether glanceable hub is enabled by the current user. */
+    fun getSettingEnabledByUser(user: UserInfo): Flow<Boolean>
+
     /**
      * Returns true if any glanceable hub functionality should be enabled via configs and flags.
      *
@@ -123,6 +124,19 @@
         resources.getBoolean(com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault)
     }
 
+    private val _suppressionReasons =
+        MutableStateFlow<List<SuppressionReason>>(
+            // Suppress hub by default until we get an initial update.
+            listOf(SuppressionReason.ReasonUnknown(FEATURE_ALL))
+        )
+
+    override fun isEnabled(@CommunalFeature feature: Int): Flow<Boolean> =
+        _suppressionReasons.map { reasons -> reasons.none { it.isSuppressed(feature) } }
+
+    override fun setSuppressionReasons(reasons: List<SuppressionReason>) {
+        _suppressionReasons.value = reasons
+    }
+
     override fun getFlagEnabled(): Boolean {
         return if (getV2FlagEnabled()) {
             true
@@ -138,44 +152,6 @@
             glanceableHubV2()
     }
 
-    override fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState> {
-        if (!user.isMain) {
-            return flowOf(CommunalEnabledState(DISABLED_REASON_INVALID_USER))
-        }
-        if (!getFlagEnabled()) {
-            return flowOf(CommunalEnabledState(DISABLED_REASON_FLAG))
-        }
-        return combine(
-                getEnabledByUser(user).mapToReason(DISABLED_REASON_USER_SETTING),
-                getAllowedByDevicePolicy(user).mapToReason(DISABLED_REASON_DEVICE_POLICY),
-            ) { reasons ->
-                reasons.filterNotNull()
-            }
-            .map { reasons ->
-                if (reasons.isEmpty()) {
-                    EnumSet.noneOf(DisabledReason::class.java)
-                } else {
-                    EnumSet.copyOf(reasons)
-                }
-            }
-            .map { reasons -> CommunalEnabledState(reasons) }
-            .flowOn(bgDispatcher)
-    }
-
-    override fun getScreensaverEnabledState(user: UserInfo): Flow<Boolean> =
-        secureSettings
-            .observerFlow(userId = user.id, names = arrayOf(Settings.Secure.SCREENSAVER_ENABLED))
-            // Force an update
-            .onStart { emit(Unit) }
-            .map {
-                secureSettings.getIntForUser(
-                    Settings.Secure.SCREENSAVER_ENABLED,
-                    SCREENSAVER_ENABLED_SETTING_DEFAULT,
-                    user.id,
-                ) == 1
-            }
-            .flowOn(bgDispatcher)
-
     override fun getWhenToDreamState(user: UserInfo): Flow<WhenToDream> =
         secureSettings
             .observerFlow(
@@ -247,11 +223,11 @@
                     ?: defaultBackgroundType
             }
 
-    private fun getEnabledByUser(user: UserInfo): Flow<Boolean> =
+    override fun getSettingEnabledByUser(user: UserInfo): Flow<Boolean> =
         secureSettings
             .observerFlow(userId = user.id, names = arrayOf(Settings.Secure.GLANCEABLE_HUB_ENABLED))
             // Force an update
-            .onStart { emit(Unit) }
+            .emitOnStart()
             .map {
                 secureSettings.getIntForUser(
                     Settings.Secure.GLANCEABLE_HUB_ENABLED,
@@ -259,17 +235,13 @@
                     user.id,
                 ) == 1
             }
+            .flowOn(bgDispatcher)
 
     companion object {
         const val GLANCEABLE_HUB_BACKGROUND_SETTING = "glanceable_hub_background"
         private const val ENABLED_SETTING_DEFAULT = 1
-        private const val SCREENSAVER_ENABLED_SETTING_DEFAULT = 0
     }
 }
 
 private fun DevicePolicyManager.areKeyguardWidgetsAllowed(userId: Int): Boolean =
     (getKeyguardDisabledFeatures(null, userId) and KEYGUARD_DISABLE_WIDGETS_ALL) == 0
-
-private fun Flow<Boolean>.mapToReason(reason: DisabledReason) = map { enabled ->
-    if (enabled) null else reason
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractor.kt
new file mode 100644
index 0000000..17b61e1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractor.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.interactor
+
+import com.android.systemui.communal.data.repository.CarProjectionRepository
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+@SysUISingleton
+class CarProjectionInteractor @Inject constructor(repository: CarProjectionRepository) {
+    /** Whether car projection is active. */
+    val projectionActive = repository.projectionActive
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt
new file mode 100644
index 0000000..51df333
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.interactor
+
+import com.android.systemui.common.domain.interactor.BatteryInteractor
+import com.android.systemui.communal.dagger.CommunalModule.Companion.SWIPE_TO_HUB
+import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN
+import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN
+import com.android.systemui.communal.data.model.SuppressionReason
+import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor
+import com.android.systemui.communal.shared.model.WhenToDream
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dock.DockManager
+import com.android.systemui.dock.retrieveIsDocked
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
+import javax.inject.Inject
+import javax.inject.Named
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class CommunalAutoOpenInteractor
+@Inject
+constructor(
+    communalSettingsInteractor: CommunalSettingsInteractor,
+    @Background private val backgroundContext: CoroutineContext,
+    private val batteryInteractor: BatteryInteractor,
+    private val posturingInteractor: PosturingInteractor,
+    private val dockManager: DockManager,
+    @Named(SWIPE_TO_HUB) private val allowSwipeAlways: Boolean,
+) {
+    val shouldAutoOpen: Flow<Boolean> =
+        communalSettingsInteractor.whenToDream
+            .flatMapLatestConflated { whenToDream ->
+                when (whenToDream) {
+                    WhenToDream.WHILE_CHARGING -> batteryInteractor.isDevicePluggedIn
+                    WhenToDream.WHILE_DOCKED -> {
+                        allOf(batteryInteractor.isDevicePluggedIn, dockManager.retrieveIsDocked())
+                    }
+                    WhenToDream.WHILE_POSTURED -> {
+                        allOf(batteryInteractor.isDevicePluggedIn, posturingInteractor.postured)
+                    }
+                    WhenToDream.NEVER -> flowOf(false)
+                }
+            }
+            .flowOn(backgroundContext)
+
+    val suppressionReason: Flow<SuppressionReason?> =
+        shouldAutoOpen.map { conditionMet ->
+            if (conditionMet) {
+                null
+            } else {
+                var suppressedFeatures = FEATURE_AUTO_OPEN
+                if (!allowSwipeAlways) {
+                    suppressedFeatures = suppressedFeatures or FEATURE_MANUAL_OPEN
+                }
+                SuppressionReason.ReasonWhenToAutoShow(suppressedFeatures)
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 564628d..684c52a 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -30,13 +30,11 @@
 import com.android.systemui.Flags.communalResponsiveGrid
 import com.android.systemui.Flags.glanceableHubBlurredBackground
 import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.domain.interactor.BatteryInteractor
 import com.android.systemui.communal.data.repository.CommunalMediaRepository
 import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository
 import com.android.systemui.communal.data.repository.CommunalWidgetRepository
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.domain.model.CommunalContentModel.WidgetContent
-import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor
 import com.android.systemui.communal.shared.model.CommunalBackgroundType
 import com.android.systemui.communal.shared.model.CommunalContentSize
 import com.android.systemui.communal.shared.model.CommunalContentSize.FixedSize.FULL
@@ -45,14 +43,11 @@
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
 import com.android.systemui.communal.shared.model.EditModeState
-import com.android.systemui.communal.shared.model.WhenToDream
 import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
 import com.android.systemui.communal.widgets.WidgetConfigurator
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dock.DockManager
-import com.android.systemui.dock.retrieveIsDocked
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.Edge
@@ -69,11 +64,8 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.ManagedProfileController
-import com.android.systemui.user.domain.interactor.UserLockedInteractor
 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
-import com.android.systemui.util.kotlin.BooleanFlowOperators.not
 import com.android.systemui.util.kotlin.emitOnStart
-import com.android.systemui.util.kotlin.isDevicePluggedIn
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.minutes
 import kotlinx.coroutines.CoroutineDispatcher
@@ -125,10 +117,6 @@
     @CommunalLog logBuffer: LogBuffer,
     @CommunalTableLog tableLogBuffer: TableLogBuffer,
     private val managedProfileController: ManagedProfileController,
-    private val batteryInteractor: BatteryInteractor,
-    private val dockManager: DockManager,
-    private val posturingInteractor: PosturingInteractor,
-    private val userLockedInteractor: UserLockedInteractor,
 ) {
     private val logger = Logger(logBuffer, "CommunalInteractor")
 
@@ -162,11 +150,7 @@
 
     /** Whether communal features are enabled and available. */
     val isCommunalAvailable: Flow<Boolean> =
-        allOf(
-                communalSettingsInteractor.isCommunalEnabled,
-                userLockedInteractor.isUserUnlocked(userManager.mainUser),
-                keyguardInteractor.isKeyguardShowing,
-            )
+        allOf(communalSettingsInteractor.isCommunalEnabled, keyguardInteractor.isKeyguardShowing)
             .distinctUntilChanged()
             .onEach { available ->
                 logger.i({ "Communal is ${if (bool1) "" else "un"}available" }) {
@@ -184,37 +168,6 @@
                 replay = 1,
             )
 
-    /**
-     * Whether communal hub should be shown automatically, depending on the user's [WhenToDream]
-     * state.
-     */
-    val shouldShowCommunal: StateFlow<Boolean> =
-        allOf(
-                isCommunalAvailable,
-                communalSettingsInteractor.whenToDream
-                    .flatMapLatest { whenToDream ->
-                        when (whenToDream) {
-                            WhenToDream.NEVER -> flowOf(false)
-
-                            WhenToDream.WHILE_CHARGING -> batteryInteractor.isDevicePluggedIn
-
-                            WhenToDream.WHILE_DOCKED ->
-                                allOf(
-                                    batteryInteractor.isDevicePluggedIn,
-                                    dockManager.retrieveIsDocked(),
-                                )
-
-                            WhenToDream.WHILE_POSTURED ->
-                                allOf(
-                                    batteryInteractor.isDevicePluggedIn,
-                                    posturingInteractor.postured,
-                                )
-                        }
-                    }
-                    .flowOn(bgDispatcher),
-            )
-            .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false)
-
     private val _isDisclaimerDismissed = MutableStateFlow(false)
     val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow()
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
index a0b1261..ae89b39 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
@@ -18,17 +18,18 @@
 
 import android.content.pm.UserInfo
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.communal.data.model.CommunalEnabledState
+import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN
+import com.android.systemui.communal.data.model.FEATURE_ENABLED
+import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN
+import com.android.systemui.communal.data.model.SuppressionReason
 import com.android.systemui.communal.data.repository.CommunalSettingsRepository
 import com.android.systemui.communal.shared.model.CommunalBackgroundType
 import com.android.systemui.communal.shared.model.WhenToDream
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.log.dagger.CommunalTableLog
-import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import java.util.concurrent.Executor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -53,33 +54,43 @@
     private val repository: CommunalSettingsRepository,
     userInteractor: SelectedUserInteractor,
     private val userTracker: UserTracker,
-    @CommunalTableLog tableLogBuffer: TableLogBuffer,
 ) {
-    /** Whether or not communal is enabled for the currently selected user. */
+    /** Whether communal is enabled at all. */
     val isCommunalEnabled: StateFlow<Boolean> =
-        userInteractor.selectedUserInfo
-            .flatMapLatest { user -> repository.getEnabledState(user) }
-            .logDiffsForTable(
-                tableLogBuffer = tableLogBuffer,
-                columnPrefix = "disabledReason",
-                initialValue = CommunalEnabledState(),
-            )
-            .map { model -> model.enabled }
-            // Start this eagerly since the value is accessed synchronously in many places.
+        repository
+            .isEnabled(FEATURE_ENABLED)
             .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false)
 
-    /** Whether or not screensaver (dreams) is enabled for the currently selected user. */
-    val isScreensaverEnabled: Flow<Boolean> =
-        userInteractor.selectedUserInfo.flatMapLatest { user ->
-            repository.getScreensaverEnabledState(user)
-        }
+    /** Whether manually opening the hub is enabled */
+    val manualOpenEnabled: StateFlow<Boolean> =
+        repository
+            .isEnabled(FEATURE_MANUAL_OPEN)
+            .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false)
+
+    /** Whether auto-opening the hub is enabled */
+    val autoOpenEnabled: StateFlow<Boolean> =
+        repository
+            .isEnabled(FEATURE_AUTO_OPEN)
+            .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false)
 
     /** When to dream for the currently selected user. */
     val whenToDream: Flow<WhenToDream> =
-        userInteractor.selectedUserInfo.flatMapLatest { user ->
+        userInteractor.selectedUserInfo.flatMapLatestConflated { user ->
             repository.getWhenToDreamState(user)
         }
 
+    /** Whether communal hub is allowed by device policy for the current user */
+    val allowedForCurrentUserByDevicePolicy: Flow<Boolean> =
+        userInteractor.selectedUserInfo.flatMapLatestConflated { user ->
+            repository.getAllowedByDevicePolicy(user)
+        }
+
+    /** Whether the hub is enabled for the current user */
+    val settingEnabledForCurrentUser: Flow<Boolean> =
+        userInteractor.selectedUserInfo.flatMapLatestConflated { user ->
+            repository.getSettingEnabledByUser(user)
+        }
+
     /**
      * Returns true if any glanceable hub functionality should be enabled via configs and flags.
      *
@@ -109,6 +120,14 @@
      */
     fun isV2FlagEnabled(): Boolean = repository.getV2FlagEnabled()
 
+    /**
+     * Suppresses the hub with the given reasons. If there are no reasons, the hub will not be
+     * suppressed.
+     */
+    fun setSuppressionReasons(reasons: List<SuppressionReason>) {
+        repository.setSuppressionReasons(reasons)
+    }
+
     /** The type of background to use for the hub. Used to experiment with different backgrounds */
     val communalBackground: Flow<CommunalBackgroundType> =
         userInteractor.selectedUserInfo
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/FlowExt.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/FlowExt.kt
new file mode 100644
index 0000000..a10e90f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/FlowExt.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.suppression
+
+import com.android.systemui.communal.data.model.SuppressionReason
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+fun Flow<Boolean>.mapToReasonIfNotAllowed(reason: SuppressionReason): Flow<SuppressionReason?> =
+    this.map { allowed -> if (allowed) null else reason }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/dagger/CommunalSuppressionModule.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/dagger/CommunalSuppressionModule.kt
new file mode 100644
index 0000000..c62d77e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/dagger/CommunalSuppressionModule.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.suppression.dagger
+
+import com.android.systemui.Flags.glanceableHubV2
+import com.android.systemui.communal.data.model.SuppressionReason
+import com.android.systemui.communal.domain.interactor.CarProjectionInteractor
+import com.android.systemui.communal.domain.interactor.CommunalAutoOpenInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
+import com.android.systemui.communal.domain.suppression.mapToReasonIfNotAllowed
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.user.domain.interactor.UserLockedInteractor
+import com.android.systemui.util.kotlin.BooleanFlowOperators.not
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.IntoSet
+import dagger.multibindings.Multibinds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+@Module
+interface CommunalSuppressionModule {
+    /**
+     * A set of reasons why communal may be suppressed. Ensures that this can be injected even if
+     * it's empty.
+     */
+    @Multibinds fun suppressorSet(): Set<Flow<SuppressionReason?>>
+
+    companion object {
+        @Provides
+        @IntoSet
+        fun provideCarProjectionSuppressor(
+            interactor: CarProjectionInteractor
+        ): Flow<SuppressionReason?> {
+            if (!glanceableHubV2()) {
+                return flowOf(null)
+            }
+            return not(interactor.projectionActive)
+                .mapToReasonIfNotAllowed(SuppressionReason.ReasonCarProjection)
+        }
+
+        @Provides
+        @IntoSet
+        fun provideDevicePolicySuppressor(
+            interactor: CommunalSettingsInteractor
+        ): Flow<SuppressionReason?> {
+            return interactor.allowedForCurrentUserByDevicePolicy.mapToReasonIfNotAllowed(
+                SuppressionReason.ReasonDevicePolicy
+            )
+        }
+
+        @Provides
+        @IntoSet
+        fun provideSettingDisabledSuppressor(
+            interactor: CommunalSettingsInteractor
+        ): Flow<SuppressionReason?> {
+            return interactor.settingEnabledForCurrentUser.mapToReasonIfNotAllowed(
+                SuppressionReason.ReasonSettingDisabled
+            )
+        }
+
+        @Provides
+        @IntoSet
+        fun bindUserLockedSuppressor(interactor: UserLockedInteractor): Flow<SuppressionReason?> {
+            return interactor.currentUserUnlocked.mapToReasonIfNotAllowed(
+                SuppressionReason.ReasonUserLocked
+            )
+        }
+
+        @Provides
+        @IntoSet
+        fun provideAutoOpenSuppressor(
+            interactor: CommunalAutoOpenInteractor
+        ): Flow<SuppressionReason?> {
+            return interactor.suppressionReason
+        }
+
+        @Provides
+        @IntoSet
+        fun provideMainUserSuppressor(
+            interactor: SelectedUserInteractor
+        ): Flow<SuppressionReason?> {
+            return interactor.selectedUserInfo
+                .map { it.isMain }
+                .mapToReasonIfNotAllowed(SuppressionReason.ReasonSecondaryUser)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 62a98d7..857fa5c 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -369,12 +369,10 @@
 
     val swipeToHubEnabled: Flow<Boolean> by lazy {
         val inAllowedDeviceState =
-            if (swipeToHub) {
-                MutableStateFlow(true)
-            } else if (v2FlagEnabled()) {
-                communalInteractor.shouldShowCommunal
+            if (v2FlagEnabled()) {
+                communalSettingsInteractor.manualOpenEnabled
             } else {
-                MutableStateFlow(false)
+                MutableStateFlow(swipeToHub)
             }
 
         if (v2FlagEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
index a7c078f..36b75c6 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
@@ -61,7 +61,7 @@
     fun startTransitionFromDream() {
         val showGlanceableHub =
             if (communalSettingsInteractor.isV2FlagEnabled()) {
-                communalInteractor.shouldShowCommunal.value
+                communalSettingsInteractor.autoOpenEnabled.value
             } else {
                 communalInteractor.isCommunalEnabled.value &&
                     !keyguardUpdateMonitor.isEncryptedOrLockdown(userTracker.userId)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index ef06a85..54af8f5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -20,7 +20,6 @@
 import android.util.Log
 import com.android.app.animation.Interpolators
 import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
 import com.android.systemui.communal.shared.model.CommunalScenes
@@ -60,7 +59,6 @@
     private val wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor,
     private val communalSettingsInteractor: CommunalSettingsInteractor,
     private val communalSceneInteractor: CommunalSceneInteractor,
-    private val communalInteractor: CommunalInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.AOD,
@@ -110,7 +108,7 @@
                     val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value
                     val biometricUnlockMode = keyguardInteractor.biometricUnlockState.value.mode
                     val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value
-                    val shouldShowCommunal = communalInteractor.shouldShowCommunal.value
+                    val shouldShowCommunal = communalSettingsInteractor.autoOpenEnabled.value
 
                     if (!maybeHandleInsecurePowerGesture()) {
                         val shouldTransitionToLockscreen =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 6f5f662..1fc4108 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -153,7 +153,7 @@
                 .filterRelevantKeyguardStateAnd { isAwake -> isAwake }
                 .sample(
                     communalInteractor.isCommunalAvailable,
-                    communalInteractor.shouldShowCommunal,
+                    communalSettingsInteractor.autoOpenEnabled,
                 )
                 .collect { (_, isCommunalAvailable, shouldShowCommunal) ->
                     val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value
@@ -209,7 +209,7 @@
             powerInteractor.detailedWakefulness
                 .filterRelevantKeyguardStateAnd { it.isAwake() }
                 .sample(
-                    communalInteractor.shouldShowCommunal,
+                    communalSettingsInteractor.autoOpenEnabled,
                     communalInteractor.isCommunalAvailable,
                     keyguardInteractor.biometricUnlockState,
                     wakeToGoneInteractor.canWakeDirectlyToGone,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 0fb98ff..3b1b6fc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -115,7 +115,7 @@
                 powerInteractor.isAwake
                     .debounce(50L)
                     .filterRelevantKeyguardStateAnd { isAwake -> isAwake }
-                    .sample(communalInteractor.shouldShowCommunal)
+                    .sample(communalSettingsInteractor.autoOpenEnabled)
                     .collect { shouldShowCommunal ->
                         if (shouldShowCommunal) {
                             // This case handles tapping the power button to transition through
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index a01dc02..f8c7a86 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -20,7 +20,6 @@
 import android.util.MathUtils
 import com.android.app.animation.Interpolators
 import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
 import com.android.systemui.communal.shared.model.CommunalScenes
@@ -69,7 +68,6 @@
     private val shadeRepository: ShadeRepository,
     powerInteractor: PowerInteractor,
     private val communalSettingsInteractor: CommunalSettingsInteractor,
-    private val communalInteractor: CommunalInteractor,
     private val communalSceneInteractor: CommunalSceneInteractor,
     private val swipeToDismissInteractor: SwipeToDismissInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
@@ -355,7 +353,7 @@
 
     private fun listenForLockscreenToGlanceableHubV2() {
         scope.launch {
-            communalInteractor.shouldShowCommunal
+            communalSettingsInteractor.autoOpenEnabled
                 .filterRelevantKeyguardStateAnd { shouldShow -> shouldShow }
                 .collect {
                     communalSceneInteractor.changeScene(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
index 705eaa2..55534c4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
@@ -20,11 +20,14 @@
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
+import android.os.PowerManager
+import android.provider.Settings
 import android.view.accessibility.AccessibilityManager
 import androidx.annotation.VisibleForTesting
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.logging.UiEvent
 import com.android.internal.logging.UiEventLogger
+import com.android.systemui.Flags.doubleTapToSleep
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -36,10 +39,13 @@
 import com.android.systemui.shade.PulsingGestureListener
 import com.android.systemui.shade.ShadeDisplayAware
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
+import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository
+import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -66,10 +72,13 @@
     private val accessibilityManager: AccessibilityManagerWrapper,
     private val pulsingGestureListener: PulsingGestureListener,
     private val faceAuthInteractor: DeviceEntryFaceAuthInteractor,
+    private val secureSettingsRepository: UserAwareSecureSettingsRepository,
+    private val powerManager: PowerManager,
+    private val systemClock: SystemClock,
 ) {
     /** Whether the long-press handling feature should be enabled. */
     val isLongPressHandlingEnabled: StateFlow<Boolean> =
-        if (isFeatureEnabled()) {
+        if (isLongPressFeatureEnabled()) {
                 combine(
                     transitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN),
                     repository.isQuickSettingsVisible,
@@ -85,6 +94,30 @@
                 initialValue = false,
             )
 
+    /** Whether the double tap handling handling feature should be enabled. */
+    val isDoubleTapHandlingEnabled: StateFlow<Boolean> =
+        if (isDoubleTapFeatureEnabled()) {
+                combine(
+                    transitionInteractor.transitionValue(KeyguardState.LOCKSCREEN),
+                    repository.isQuickSettingsVisible,
+                    isDoubleTapSettingEnabled(),
+                ) {
+                    isFullyTransitionedToLockScreen,
+                    isQuickSettingsVisible,
+                    isDoubleTapSettingEnabled ->
+                    isFullyTransitionedToLockScreen == 1f &&
+                        !isQuickSettingsVisible &&
+                        isDoubleTapSettingEnabled
+                }
+            } else {
+                flowOf(false)
+            }
+            .stateIn(
+                scope = scope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = false,
+            )
+
     private val _isMenuVisible = MutableStateFlow(false)
     /** Model for whether the menu should be shown. */
     val isMenuVisible: StateFlow<Boolean> =
@@ -116,7 +149,7 @@
     private var delayedHideMenuJob: Job? = null
 
     init {
-        if (isFeatureEnabled()) {
+        if (isLongPressFeatureEnabled()) {
             broadcastDispatcher
                 .broadcastFlow(IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
                 .onEach { hideMenu() }
@@ -175,17 +208,30 @@
 
     /** Notifies that the lockscreen has been double clicked. */
     fun onDoubleClick() {
-        pulsingGestureListener.onDoubleTapEvent()
+        if (isDoubleTapHandlingEnabled.value) {
+            powerManager.goToSleep(systemClock.uptimeMillis())
+        } else {
+            pulsingGestureListener.onDoubleTapEvent()
+        }
+    }
+
+    private fun isDoubleTapSettingEnabled(): Flow<Boolean> {
+        return secureSettingsRepository.boolSetting(Settings.Secure.DOUBLE_TAP_TO_SLEEP)
     }
 
     private fun showSettings() {
         _shouldOpenSettings.value = true
     }
 
-    private fun isFeatureEnabled(): Boolean {
+    private fun isLongPressFeatureEnabled(): Boolean {
         return context.resources.getBoolean(R.bool.long_press_keyguard_customize_lockscreen_enabled)
     }
 
+    private fun isDoubleTapFeatureEnabled(): Boolean {
+        return doubleTapToSleep() &&
+            context.resources.getBoolean(com.android.internal.R.bool.config_supportDoubleTapSleep)
+    }
+
     /** Updates application state to ask to show the menu. */
     private fun showMenu() {
         _isMenuVisible.value = true
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 17e14c3..70a827d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -48,7 +48,7 @@
     /**
      * Updates UI for:
      * - device entry containing view (parent view for the below views)
-     *     - long-press handling view (transparent, no UI)
+     *     - touch handling view (transparent, no UI)
      *     - foreground icon view (lock/unlock/fingerprint)
      *     - background view (optional)
      */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index e81d535..5ef2d6f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.clocks.VRectF
 import com.android.systemui.res.R
 import com.android.systemui.shared.R as sharedR
 import kotlinx.coroutines.DisposableHandle
@@ -135,7 +136,7 @@
                                     }
                                 }
 
-                                if (clockBounds == null) return@collect
+                                if (clockBounds == VRectF.ZERO) return@collect
                                 if (isLargeClock) {
                                     val largeDateHeight =
                                         keyguardRootView
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt
index 195413a..485e1ce 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt
@@ -75,6 +75,13 @@
 
                     onSingleTap(x, y)
                 }
+
+                override fun onDoubleTapDetected(view: View) {
+                    if (falsingManager.isFalseDoubleTap()) {
+                        return
+                    }
+                    viewModel.onDoubleClick()
+                }
             }
 
         view.repeatWhenAttached {
@@ -90,9 +97,20 @@
                             }
                     }
                 }
+                launch("$TAG#viewModel.isDoubleTapHandlingEnabled") {
+                    viewModel.isDoubleTapHandlingEnabled.collect { isEnabled ->
+                        view.setDoublePressHandlingEnabled(isEnabled)
+                        view.contentDescription =
+                            if (isEnabled) {
+                                view.resources.getString(R.string.accessibility_desc_lock_screen)
+                            } else {
+                                null
+                            }
+                    }
+                }
             }
         }
     }
 
-    private const val TAG = "KeyguardLongPressViewBinder"
+    private const val TAG = "KeyguardTouchViewBinder"
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
index 1d2edc6..d4e7af4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
@@ -33,6 +33,9 @@
     /** Whether the long-press handling feature should be enabled. */
     val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled
 
+    /** Whether the double tap handling feature should be enabled. */
+    val isDoubleTapHandlingEnabled: Flow<Boolean> = interactor.isDoubleTapHandlingEnabled
+
     /** Notifies that the user has long-pressed on the lock screen.
      *
      * @param isA11yAction: Whether the action was performed as an a11y action
diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
index e7c2a45..b71d8c9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
@@ -17,6 +17,7 @@
 package com.android.systemui.media;
 
 import android.annotation.Nullable;
+import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -126,6 +127,8 @@
                 Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid="
                         + Binder.getCallingUid() + ")");
             }
+            enforceUriUserId(uri);
+
             Client client;
             synchronized (mClients) {
                 client = mClients.get(token);
@@ -207,6 +210,7 @@
 
         @Override
         public String getTitle(Uri uri) {
+            enforceUriUserId(uri);
             final UserHandle user = Binder.getCallingUserHandle();
             return Ringtone.getTitle(getContextForUser(user), uri,
                     false /*followSettingsUri*/, false /*allowRemote*/);
@@ -214,6 +218,7 @@
 
         @Override
         public ParcelFileDescriptor openRingtone(Uri uri) {
+            enforceUriUserId(uri);
             final UserHandle user = Binder.getCallingUserHandle();
             final ContentResolver resolver = getContextForUser(user).getContentResolver();
 
@@ -241,6 +246,28 @@
         }
     };
 
+    /**
+     * Must be called from the Binder calling thread.
+     * Ensures caller is from the same userId as the content they're trying to access.
+     * @param uri the URI to check
+     * @throws SecurityException when in a non-system call and userId in uri differs from the
+     *                           caller's userId
+     */
+    private void enforceUriUserId(Uri uri) throws SecurityException {
+        final int uriUserId = ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId());
+        // for a non-system call, verify the URI to play belongs to the same user as the caller
+        if (UserHandle.isApp(Binder.getCallingUid()) && (UserHandle.myUserId() != uriUserId)) {
+            final String errorMessage = "Illegal access to uri=" + uri
+                    + " content associated with user=" + uriUserId
+                    + ", current userID: " + UserHandle.myUserId();
+            if (android.media.audio.Flags.ringtoneUserUriCheck()) {
+                throw new SecurityException(errorMessage);
+            } else {
+                Log.e(TAG, errorMessage, new Exception());
+            }
+        }
+    }
+
     private Context getContextForUser(UserHandle user) {
         try {
             return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
index 49b53c2..dfb32e6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
@@ -37,6 +37,7 @@
 import com.android.settingslib.media.MediaDevice
 import com.android.settingslib.media.PhoneMediaDevice
 import com.android.settingslib.media.flags.Flags
+import com.android.systemui.Flags.mediaControlsDeviceManagerBackgroundExecution
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.controls.shared.MediaControlDrawables
@@ -94,36 +95,21 @@
         data: MediaData,
         immediately: Boolean,
         receivedSmartspaceCardLatency: Int,
-        isSsReactivated: Boolean
+        isSsReactivated: Boolean,
     ) {
-        if (oldKey != null && oldKey != key) {
-            val oldEntry = entries.remove(oldKey)
-            oldEntry?.stop()
-        }
-        var entry = entries[key]
-        if (entry == null || entry.token != data.token) {
-            entry?.stop()
-            if (data.device != null) {
-                // If we were already provided device info (e.g. from RCN), keep that and don't
-                // listen for updates, but process once to push updates to listeners
-                processDevice(key, oldKey, data.device)
-                return
-            }
-            val controller = data.token?.let { controllerFactory.create(it) }
-            val localMediaManager =
-                localMediaManagerFactory.create(data.packageName, controller?.sessionToken)
-            val muteAwaitConnectionManager =
-                muteAwaitConnectionManagerFactory.create(localMediaManager)
-            entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager)
-            entries[key] = entry
-            entry.start()
+        if (mediaControlsDeviceManagerBackgroundExecution()) {
+            bgExecutor.execute { onMediaLoaded(key, oldKey, data) }
+        } else {
+            onMediaLoaded(key, oldKey, data)
         }
     }
 
     override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
-        val token = entries.remove(key)
-        token?.stop()
-        token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } }
+        if (mediaControlsDeviceManagerBackgroundExecution()) {
+            bgExecutor.execute { onMediaRemoved(key, userInitiated) }
+        } else {
+            onMediaRemoved(key, userInitiated)
+        }
     }
 
     fun dump(pw: PrintWriter) {
@@ -141,6 +127,47 @@
         listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) }
     }
 
+    private fun onMediaLoaded(key: String, oldKey: String?, data: MediaData) {
+        if (oldKey != null && oldKey != key) {
+            val oldEntry = entries.remove(oldKey)
+            oldEntry?.stop()
+        }
+        var entry = entries[key]
+        if (entry == null || entry.token != data.token) {
+            entry?.stop()
+            if (data.device != null) {
+                // If we were already provided device info (e.g. from RCN), keep that and
+                // don't listen for updates, but process once to push updates to listeners
+                if (mediaControlsDeviceManagerBackgroundExecution()) {
+                    fgExecutor.execute { processDevice(key, oldKey, data.device) }
+                } else {
+                    processDevice(key, oldKey, data.device)
+                }
+                return
+            }
+            val controller = data.token?.let { controllerFactory.create(it) }
+            val localMediaManager =
+                localMediaManagerFactory.create(data.packageName, controller?.sessionToken)
+            val muteAwaitConnectionManager =
+                muteAwaitConnectionManagerFactory.create(localMediaManager)
+            entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager)
+            entries[key] = entry
+            entry.start()
+        }
+    }
+
+    private fun onMediaRemoved(key: String, userInitiated: Boolean) {
+        val token = entries.remove(key)
+        token?.stop()
+        if (mediaControlsDeviceManagerBackgroundExecution()) {
+            fgExecutor.execute {
+                token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } }
+            }
+        } else {
+            token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } }
+        }
+    }
+
     interface Listener {
         /** Called when the route has changed for a given notification. */
         fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?)
@@ -260,7 +287,7 @@
         override fun onAboutToConnectDeviceAdded(
             deviceAddress: String,
             deviceName: String,
-            deviceIcon: Drawable?
+            deviceIcon: Drawable?,
         ) {
             aboutToConnectDeviceOverride =
                 AboutToConnectDevice(
@@ -270,8 +297,8 @@
                             /* enabled */ enabled = true,
                             /* icon */ deviceIcon,
                             /* name */ deviceName,
-                            /* showBroadcastButton */ showBroadcastButton = false
-                        )
+                            /* showBroadcastButton */ showBroadcastButton = false,
+                        ),
                 )
             updateCurrent()
         }
@@ -292,7 +319,7 @@
 
         override fun onBroadcastMetadataChanged(
             broadcastId: Int,
-            metadata: BluetoothLeBroadcastMetadata
+            metadata: BluetoothLeBroadcastMetadata,
         ) {
             logger.logBroadcastMetadataChanged(broadcastId, metadata.toString())
             updateCurrent()
@@ -352,14 +379,14 @@
                             //           route.
                             connectedDevice?.copy(
                                 name = it.name ?: connectedDevice.name,
-                                icon = icon
+                                icon = icon,
                             )
                         }
                             ?: MediaDeviceData(
                                 enabled = false,
                                 icon = MediaControlDrawables.getHomeDevices(context),
                                 name = context.getString(R.string.media_seamless_other_device),
-                                showBroadcastButton = false
+                                showBroadcastButton = false,
                             )
                     logger.logRemoteDevice(routingSession?.name, connectedDevice)
                 } else {
@@ -398,7 +425,7 @@
                         device?.iconWithoutBackground,
                         name,
                         id = device?.id,
-                        showBroadcastButton = false
+                        showBroadcastButton = false,
                     )
             }
         }
@@ -415,7 +442,7 @@
                 icon = iconWithoutBackground,
                 name = name,
                 id = id,
-                showBroadcastButton = false
+                showBroadcastButton = false,
             )
 
         private fun getLeAudioBroadcastDeviceData(): MediaDeviceData {
@@ -425,7 +452,7 @@
                     icon = MediaControlDrawables.getLeAudioSharing(context),
                     name = context.getString(R.string.audio_sharing_description),
                     intent = null,
-                    showBroadcastButton = false
+                    showBroadcastButton = false,
                 )
             } else {
                 MediaDeviceData(
@@ -433,7 +460,7 @@
                     icon = MediaControlDrawables.getAntenna(context),
                     name = broadcastDescription,
                     intent = null,
-                    showBroadcastButton = true
+                    showBroadcastButton = true,
                 )
             }
         }
@@ -449,7 +476,7 @@
                 device,
                 controller,
                 routingSession?.name,
-                selectedRoutes?.firstOrNull()?.name
+                selectedRoutes?.firstOrNull()?.name,
             )
 
             if (controller == null) {
@@ -514,7 +541,7 @@
                 MediaDataUtils.getAppLabel(
                     context,
                     localMediaManager.packageName,
-                    context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)
+                    context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name),
                 )
             val isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
             if (isCurrentBroadcastedApp) {
@@ -538,5 +565,5 @@
  */
 private data class AboutToConnectDevice(
     val fullMediaDevice: MediaDevice? = null,
-    val backupMediaDeviceData: MediaDeviceData? = null
+    val backupMediaDeviceData: MediaDeviceData? = null,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
index 795e811..6ab4a52 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
@@ -68,7 +68,6 @@
     private final Executor mMainExecutor;
     private final Executor mBackgroundExecutor;
     View mHolderView;
-    private boolean mIsInitVolumeFirstTime;
 
     public MediaOutputAdapterLegacy(
             MediaSwitchingController controller,
@@ -78,7 +77,6 @@
         super(controller);
         mMainExecutor = mainExecutor;
         mBackgroundExecutor = backgroundExecutor;
-        mIsInitVolumeFirstTime = true;
     }
 
     @Override
@@ -261,10 +259,9 @@
                     updateSeekbarProgressBackground();
                 }
             }
-            boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
             mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
             if (showSeekBar) {
-                initSeekbar(device, isCurrentSeekbarInvisible);
+                initSeekbar(device);
                 updateContainerContentA11yImportance(false /* isImportant */);
                 mSeekBar.setContentDescription(contentDescription);
             } else {
@@ -274,9 +271,8 @@
 
         void updateGroupSeekBar(String contentDescription) {
             updateSeekbarProgressBackground();
-            boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
             mSeekBar.setVisibility(View.VISIBLE);
-            initGroupSeekbar(isCurrentSeekbarInvisible);
+            initGroupSeekbar();
             updateContainerContentA11yImportance(false /* isImportant */);
             mSeekBar.setContentDescription(contentDescription);
         }
@@ -364,31 +360,21 @@
                             mActiveRadius, 0, 0});
         }
 
-        private void initializeSeekbarVolume(
-                @Nullable MediaDevice device, int currentVolume,
-                boolean isCurrentSeekbarInvisible) {
+        private void initializeSeekbarVolume(@Nullable MediaDevice device, int currentVolume) {
             if (!isDragging()) {
                 if (mSeekBar.getVolume() != currentVolume && (mLatestUpdateVolume == -1
                         || currentVolume == mLatestUpdateVolume)) {
                     // Update only if volume of device and value of volume bar doesn't match.
                     // Check if response volume match with the latest request, to ignore obsolete
                     // response
-                    if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) {
+                    if (!mVolumeAnimator.isStarted()) {
                         if (currentVolume == 0) {
                             updateMutedVolumeIcon(device);
                         } else {
                             updateUnmutedVolumeIcon(device);
                         }
-                    } else {
-                        if (!mVolumeAnimator.isStarted()) {
-                            if (currentVolume == 0) {
-                                updateMutedVolumeIcon(device);
-                            } else {
-                                updateUnmutedVolumeIcon(device);
-                            }
-                            mSeekBar.setVolume(currentVolume);
-                            mLatestUpdateVolume = -1;
-                        }
+                        mSeekBar.setVolume(currentVolume);
+                        mLatestUpdateVolume = -1;
                     }
                 } else if (currentVolume == 0) {
                     mSeekBar.resetVolume();
@@ -398,12 +384,9 @@
                     mLatestUpdateVolume = -1;
                 }
             }
-            if (mIsInitVolumeFirstTime) {
-                mIsInitVolumeFirstTime = false;
-            }
         }
 
-        void initSeekbar(@NonNull MediaDevice device, boolean isCurrentSeekbarInvisible) {
+        void initSeekbar(@NonNull MediaDevice device) {
             SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() {
                 @Override
                 public int getVolume() {
@@ -432,7 +415,7 @@
             }
             mSeekBar.setMaxVolume(device.getMaxVolume());
             final int currentVolume = device.getCurrentVolume();
-            initializeSeekbarVolume(device, currentVolume, isCurrentSeekbarInvisible);
+            initializeSeekbarVolume(device, currentVolume);
 
             mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener(
                     device, volumeControl) {
@@ -445,7 +428,7 @@
         }
 
         // Initializes the seekbar for a group of devices.
-        void initGroupSeekbar(boolean isCurrentSeekbarInvisible) {
+        void initGroupSeekbar() {
             SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() {
                 @Override
                 public int getVolume() {
@@ -472,7 +455,7 @@
             mSeekBar.setMaxVolume(mController.getSessionVolumeMax());
 
             final int currentVolume = mController.getSessionVolume();
-            initializeSeekbarVolume(null, currentVolume, isCurrentSeekbarInvisible);
+            initializeSeekbarVolume(null, currentVolume);
             mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener(
                     null, volumeControl) {
                 @Override
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 9e6fa48..f796931 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -145,7 +145,7 @@
     @VisibleForTesting
     final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
     final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>();
-    private final List<MediaItem> mOutputMediaItemList = new CopyOnWriteArrayList<>();
+    private final OutputMediaItemListProxy mOutputMediaItemListProxy;
     private final List<MediaItem> mInputMediaItemList = new CopyOnWriteArrayList<>();
     private final AudioManager mAudioManager;
     private final PowerExemptionManager mPowerExemptionManager;
@@ -226,6 +226,7 @@
                 InfoMediaManager.createInstance(mContext, packageName, userHandle, lbm, token);
         mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
         mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
+        mOutputMediaItemListProxy = new OutputMediaItemListProxy();
         mDialogTransitionAnimator = dialogTransitionAnimator;
         mNearbyMediaDevicesManager = nearbyMediaDevicesManager;
         mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext);
@@ -245,7 +246,7 @@
     protected void start(@NonNull Callback cb) {
         synchronized (mMediaDevicesLock) {
             mCachedMediaDevices.clear();
-            mOutputMediaItemList.clear();
+            mOutputMediaItemListProxy.clear();
         }
         mNearbyDeviceInfoMap.clear();
         if (mNearbyMediaDevicesManager != null) {
@@ -291,7 +292,7 @@
         mLocalMediaManager.stopScan();
         synchronized (mMediaDevicesLock) {
             mCachedMediaDevices.clear();
-            mOutputMediaItemList.clear();
+            mOutputMediaItemListProxy.clear();
         }
         if (mNearbyMediaDevicesManager != null) {
             mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this);
@@ -333,7 +334,7 @@
 
     @Override
     public void onDeviceListUpdate(List<MediaDevice> devices) {
-        boolean isListEmpty = mOutputMediaItemList.isEmpty();
+        boolean isListEmpty = mOutputMediaItemListProxy.isEmpty();
         if (isListEmpty || !mIsRefreshing) {
             buildMediaItems(devices);
             mCallback.onDeviceListChanged();
@@ -347,11 +348,12 @@
     }
 
     @Override
-    public void onSelectedDeviceStateChanged(MediaDevice device,
-            @LocalMediaManager.MediaDeviceState int state) {
+    public void onSelectedDeviceStateChanged(
+            MediaDevice device, @LocalMediaManager.MediaDeviceState int state) {
         mCallback.onRouteChanged();
         mMetricLogger.logOutputItemSuccess(
-                device.toString(), new ArrayList<>(mOutputMediaItemList));
+                device.toString(),
+                new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList()));
     }
 
     @Override
@@ -362,7 +364,8 @@
     @Override
     public void onRequestFailed(int reason) {
         mCallback.onRouteChanged();
-        mMetricLogger.logOutputItemFailure(new ArrayList<>(mOutputMediaItemList), reason);
+        mMetricLogger.logOutputItemFailure(
+                new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList()), reason);
     }
 
     /**
@@ -381,7 +384,7 @@
         }
         try {
             synchronized (mMediaDevicesLock) {
-                mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
+                mOutputMediaItemListProxy.removeMutingExpectedDevices();
             }
             mAudioManager.cancelMuteAwaitConnection(mAudioManager.getMutingExpectedDevice());
         } catch (Exception e) {
@@ -574,14 +577,14 @@
 
     private void buildMediaItems(List<MediaDevice> devices) {
         synchronized (mMediaDevicesLock) {
-            List<MediaItem> updatedMediaItems = buildMediaItems(mOutputMediaItemList, devices);
-            mOutputMediaItemList.clear();
-            mOutputMediaItemList.addAll(updatedMediaItems);
+            List<MediaItem> updatedMediaItems =
+                    buildMediaItems(mOutputMediaItemListProxy.getOutputMediaItemList(), devices);
+            mOutputMediaItemListProxy.clearAndAddAll(updatedMediaItems);
         }
     }
 
-    protected List<MediaItem> buildMediaItems(List<MediaItem> oldMediaItems,
-            List<MediaDevice> devices) {
+    protected List<MediaItem> buildMediaItems(
+            List<MediaItem> oldMediaItems, List<MediaDevice> devices) {
         synchronized (mMediaDevicesLock) {
             if (!mLocalMediaManager.isPreferenceRouteListingExist()) {
                 attachRangeInfo(devices);
@@ -689,7 +692,8 @@
      * list.
      */
     @GuardedBy("mMediaDevicesLock")
-    private List<MediaItem> categorizeMediaItemsLocked(MediaDevice connectedMediaDevice,
+    private List<MediaItem> categorizeMediaItemsLocked(
+            MediaDevice connectedMediaDevice,
             List<MediaDevice> devices,
             boolean needToHandleMutingExpectedDevice) {
         List<MediaItem> finalMediaItems = new ArrayList<>();
@@ -748,6 +752,14 @@
     }
 
     private void attachConnectNewDeviceItemIfNeeded(List<MediaItem> mediaItems) {
+        MediaItem connectNewDeviceItem = getConnectNewDeviceItem();
+        if (connectNewDeviceItem != null) {
+            mediaItems.add(connectNewDeviceItem);
+        }
+    }
+
+    @Nullable
+    private MediaItem getConnectNewDeviceItem() {
         boolean isSelectedDeviceNotAGroup = getSelectedMediaDevice().size() == 1;
         if (enableInputRouting()) {
             // When input routing is enabled, there are expected to be at least 2 total selected
@@ -756,9 +768,9 @@
         }
 
         // Attach "Connect a device" item only when current output is not remote and not a group
-        if (!isCurrentConnectedDeviceRemote() && isSelectedDeviceNotAGroup) {
-            mediaItems.add(MediaItem.createPairNewDeviceMediaItem());
-        }
+        return (!isCurrentConnectedDeviceRemote() && isSelectedDeviceNotAGroup)
+                ? MediaItem.createPairNewDeviceMediaItem()
+                : null;
     }
 
     private void attachRangeInfo(List<MediaDevice> devices) {
@@ -847,13 +859,13 @@
         mediaItems.add(
                 MediaItem.createGroupDividerMediaItem(
                         mContext.getString(R.string.media_output_group_title)));
-        mediaItems.addAll(mOutputMediaItemList);
+        mediaItems.addAll(mOutputMediaItemListProxy.getOutputMediaItemList());
     }
 
     public List<MediaItem> getMediaItemList() {
         // If input routing is not enabled, only return output media items.
         if (!enableInputRouting()) {
-            return mOutputMediaItemList;
+            return mOutputMediaItemListProxy.getOutputMediaItemList();
         }
 
         // If input routing is enabled, return both output and input media items.
@@ -959,7 +971,7 @@
 
     public boolean isAnyDeviceTransferring() {
         synchronized (mMediaDevicesLock) {
-            for (MediaItem mediaItem : mOutputMediaItemList) {
+            for (MediaItem mediaItem : mOutputMediaItemListProxy.getOutputMediaItemList()) {
                 if (mediaItem.getMediaDevice().isPresent()
                         && mediaItem.getMediaDevice().get().getState()
                         == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
@@ -999,8 +1011,11 @@
         startActivity(launchIntent, controller);
     }
 
-    void launchLeBroadcastNotifyDialog(View mediaOutputDialog, BroadcastSender broadcastSender,
-            BroadcastNotifyDialog action, final DialogInterface.OnClickListener listener) {
+    void launchLeBroadcastNotifyDialog(
+            View mediaOutputDialog,
+            BroadcastSender broadcastSender,
+            BroadcastNotifyDialog action,
+            final DialogInterface.OnClickListener listener) {
         final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
         switch (action) {
             case ACTION_FIRST_LAUNCH:
@@ -1230,8 +1245,8 @@
         return !sourceList.isEmpty();
     }
 
-    boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant(BluetoothDevice sink,
-            BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) {
+    boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant(
+            BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) {
         LocalBluetoothLeBroadcastAssistant assistant =
                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
         if (assistant == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
new file mode 100644
index 0000000..1c9c0b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/** A proxy of holding the list of Output Switcher's output media items. */
+public class OutputMediaItemListProxy {
+    private final List<MediaItem> mOutputMediaItemList;
+
+    public OutputMediaItemListProxy() {
+        mOutputMediaItemList = new CopyOnWriteArrayList<>();
+    }
+
+    /** Returns the list of output media items. */
+    public List<MediaItem> getOutputMediaItemList() {
+        return mOutputMediaItemList;
+    }
+
+    /** Updates the list of output media items with the given list. */
+    public void clearAndAddAll(List<MediaItem> updatedMediaItems) {
+        mOutputMediaItemList.clear();
+        mOutputMediaItemList.addAll(updatedMediaItems);
+    }
+
+    /** Removes the media items with muting expected devices. */
+    public void removeMutingExpectedDevices() {
+        mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
+    }
+
+    /** Clears the output media item list. */
+    public void clear() {
+        mOutputMediaItemList.clear();
+    }
+
+    /** Returns whether the output media item list is empty. */
+    public boolean isEmpty() {
+        return mOutputMediaItemList.isEmpty();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index d8fc52b..8dc27bf4 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -162,10 +162,6 @@
             ): Boolean {
                 return this@NoteTaskInitializer.handleKeyGestureEvent(event)
             }
-
-            override fun isKeyGestureSupported(gestureType: Int): Boolean {
-                return this@NoteTaskInitializer.isKeyGestureSupported(gestureType)
-            }
         }
 
     /**
@@ -225,10 +221,6 @@
         return true
     }
 
-    private fun isKeyGestureSupported(gestureType: Int): Boolean {
-        return gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES
-    }
-
     companion object {
         val MULTI_PRESS_TIMEOUT = ViewConfiguration.getMultiPressTimeout().toLong()
         val LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
index 701f44e..d40ecc9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
@@ -61,6 +61,9 @@
 
     DisposableEffect(Unit) { onDispose { detailsViewModel.closeDetailedView() } }
 
+    val title = tileDetailedViewModel.title
+    val subTitle = tileDetailedViewModel.subTitle
+
     Column(
         modifier =
             modifier
@@ -90,7 +93,7 @@
                     )
                 }
                 Text(
-                    text = tileDetailedViewModel.getTitle(),
+                    text = title,
                     modifier = Modifier.align(Alignment.CenterVertically),
                     textAlign = TextAlign.Center,
                     style = MaterialTheme.typography.titleLarge,
@@ -110,7 +113,7 @@
                 }
             }
             Text(
-                text = tileDetailedViewModel.getSubTitle(),
+                text = subTitle,
                 modifier = Modifier.fillMaxWidth(),
                 textAlign = TextAlign.Center,
                 style = MaterialTheme.typography.titleSmall,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt
index 99f52c2..3ae90d2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt
@@ -16,14 +16,20 @@
 
 package com.android.systemui.qs.panels.ui.compose.toolbar
 
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
 import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.development.ui.compose.BuildNumber
 import com.android.systemui.qs.footer.ui.compose.IconButton
 import com.android.systemui.qs.panels.ui.viewmodel.toolbar.ToolbarViewModel
+import com.android.systemui.qs.ui.compose.borderOnFocus
 
 @Composable
 fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) {
@@ -44,7 +50,18 @@
             Modifier.sysuiResTag("settings_button_container"),
         )
 
-        Spacer(modifier = Modifier.weight(1f))
+        Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {
+            BuildNumber(
+                viewModelFactory = viewModel.buildNumberViewModelFactory,
+                textColor = MaterialTheme.colorScheme.onSurface,
+                modifier =
+                    Modifier.borderOnFocus(
+                            color = MaterialTheme.colorScheme.secondary,
+                            cornerSize = CornerSize(1.dp),
+                        )
+                        .wrapContentSize(),
+            )
+        }
 
         IconButton(
             { viewModel.powerButtonViewModel },
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt
index e54bfa2..10d7871 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.animation.Expandable
 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
 import com.android.systemui.classifier.domain.interactor.runIfNotFalseTap
+import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel
 import com.android.systemui.globalactions.GlobalActionsDialogLite
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.Hydrator
@@ -46,6 +47,7 @@
 @AssistedInject
 constructor(
     editModeButtonViewModelFactory: EditModeButtonViewModel.Factory,
+    val buildNumberViewModelFactory: BuildNumberViewModel.Factory,
     private val footerActionsInteractor: FooterActionsInteractor,
     private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>,
     private val falsingInteractor: FalsingInteractor,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt
index 7d396c5..8ffba1e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt
@@ -19,26 +19,14 @@
 import android.view.LayoutInflater
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.viewinterop.AndroidView
 import com.android.systemui.res.R
 
 @Composable
 fun InternetDetailsContent(viewModel: InternetDetailsViewModel) {
     val coroutineScope = rememberCoroutineScope()
-    val context = LocalContext.current
-
-    val internetDetailsContentManager = remember {
-        viewModel.contentManagerFactory.create(
-            canConfigMobileData = viewModel.getCanConfigMobileData(),
-            canConfigWifi = viewModel.getCanConfigWifi(),
-            coroutineScope = coroutineScope,
-            context = context,
-        )
-    }
 
     AndroidView(
         modifier = Modifier.fillMaxSize(),
@@ -46,11 +34,11 @@
             // Inflate with the existing dialog xml layout and bind it with the manager
             val view =
                 LayoutInflater.from(context).inflate(R.layout.internet_connectivity_dialog, null)
-            internetDetailsContentManager.bind(view)
+            viewModel.internetDetailsContentManager.bind(view, coroutineScope)
 
             view
             // TODO: b/377388104 - Polish the internet details view UI
         },
-        onRelease = { internetDetailsContentManager.unBind() },
+        onRelease = { viewModel.internetDetailsContentManager.unBind() },
     )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
index 659488b..d8e1755 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
@@ -43,6 +43,9 @@
 import android.widget.TextView
 import androidx.annotation.MainThread
 import androidx.annotation.WorkerThread
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LifecycleRegistry
@@ -79,8 +82,6 @@
     private val internetDetailsContentController: InternetDetailsContentController,
     @Assisted(CAN_CONFIG_MOBILE_DATA) private val canConfigMobileData: Boolean,
     @Assisted(CAN_CONFIG_WIFI) private val canConfigWifi: Boolean,
-    @Assisted private val coroutineScope: CoroutineScope,
-    @Assisted private var context: Context,
     private val uiEventLogger: UiEventLogger,
     @Main private val handler: Handler,
     @Background private val backgroundExecutor: Executor,
@@ -121,26 +122,29 @@
     private lateinit var shareWifiButton: Button
     private lateinit var airplaneModeButton: Button
     private var alertDialog: AlertDialog? = null
-
-    private val canChangeWifiState =
-        WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context)
+    private var canChangeWifiState = false
     private var wifiNetworkHeight = 0
     private var backgroundOn: Drawable? = null
     private var backgroundOff: Drawable? = null
     private var clickJob: Job? = null
     private var defaultDataSubId = internetDetailsContentController.defaultDataSubscriptionId
-    @VisibleForTesting
-    internal var adapter = InternetAdapter(internetDetailsContentController, coroutineScope)
+    @VisibleForTesting internal lateinit var adapter: InternetAdapter
     @VisibleForTesting internal var wifiEntriesCount: Int = 0
     @VisibleForTesting internal var hasMoreWifiEntries: Boolean = false
+    private lateinit var context: Context
+    private lateinit var coroutineScope: CoroutineScope
+
+    var title by mutableStateOf("")
+        private set
+
+    var subTitle by mutableStateOf("")
+        private set
 
     @AssistedFactory
     interface Factory {
         fun create(
             @Assisted(CAN_CONFIG_MOBILE_DATA) canConfigMobileData: Boolean,
             @Assisted(CAN_CONFIG_WIFI) canConfigWifi: Boolean,
-            coroutineScope: CoroutineScope,
-            context: Context,
         ): InternetDetailsContentManager
     }
 
@@ -152,12 +156,16 @@
      *
      * @param contentView The view to which the content manager should be bound.
      */
-    fun bind(contentView: View) {
+    fun bind(contentView: View, coroutineScope: CoroutineScope) {
         if (DEBUG) {
             Log.d(TAG, "Bind InternetDetailsContentManager")
         }
 
         this.contentView = contentView
+        context = contentView.context
+        this.coroutineScope = coroutineScope
+        adapter = InternetAdapter(internetDetailsContentController, coroutineScope)
+        canChangeWifiState = WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context)
 
         initializeLifecycle()
         initializeViews()
@@ -323,11 +331,11 @@
         }
     }
 
-    fun getTitleText(): String {
+    private fun getTitleText(): String {
         return internetDetailsContentController.getDialogTitleText().toString()
     }
 
-    fun getSubtitleText(): String {
+    private fun getSubtitleText(): String {
         return internetDetailsContentController.getSubtitleText(isProgressBarVisible).toString()
     }
 
@@ -336,6 +344,13 @@
             Log.d(TAG, "updateDetailsUI ")
         }
 
+        if (!::context.isInitialized) {
+            return
+        }
+
+        title = getTitleText()
+        subTitle = getSubtitleText()
+
         airplaneModeButton.visibility =
             if (internetContent.isAirplaneModeEnabled) View.VISIBLE else View.GONE
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
index 6709fd2..fb63bea4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
@@ -28,9 +28,22 @@
 @AssistedInject
 constructor(
     private val accessPointController: AccessPointController,
-    val contentManagerFactory: InternetDetailsContentManager.Factory,
+    private val contentManagerFactory: InternetDetailsContentManager.Factory,
     private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
+    val internetDetailsContentManager by lazy {
+        contentManagerFactory.create(
+            canConfigMobileData = accessPointController.canConfigMobileData(),
+            canConfigWifi = accessPointController.canConfigWifi(),
+        )
+    }
+
+    override val title: String
+        get() = internetDetailsContentManager.title
+
+    override val subTitle: String
+        get() = internetDetailsContentManager.subTitle
+
     override fun clickOnSettingsButton() {
         qsTileIntentUserActionHandler.handle(
             /* expandable= */ null,
@@ -38,30 +51,6 @@
         )
     }
 
-    override fun getTitle(): String {
-        // TODO: b/377388104 make title and sub title mutable states of string
-        // by internetDetailsContentManager.getTitleText()
-        // TODO: test title change between airplane mode and not airplane mode
-        // TODO: b/377388104 Update the placeholder text
-        return "Internet"
-    }
-
-    override fun getSubTitle(): String {
-        // TODO: b/377388104 make title and sub title mutable states of string
-        // by internetDetailsContentManager.getSubtitleText()
-        // TODO: test subtitle change between airplane mode and not airplane mode
-        // TODO: b/377388104 Update the placeholder text
-        return "Tab a network to connect"
-    }
-
-    fun getCanConfigMobileData(): Boolean {
-        return accessPointController.canConfigMobileData()
-    }
-
-    fun getCanConfigWifi(): Boolean {
-        return accessPointController.canConfigWifi()
-    }
-
     @AssistedFactory
     fun interface Factory {
         fun create(): InternetDetailsViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
index 9a39c3c..4f7e03b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
@@ -23,18 +23,14 @@
 class ModesDetailsViewModel(
     private val onSettingsClick: () -> Unit,
     val viewModel: ModesDialogViewModel,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
     override fun clickOnSettingsButton() {
         onSettingsClick()
     }
 
-    override fun getTitle(): String {
-        // TODO(b/388321032): Replace this string with a string in a translatable xml file.
-        return "Modes"
-    }
+    // TODO(b/388321032): Replace this string with a string in a translatable xml file.
+    override val title = "Modes"
 
-    override fun getSubTitle(): String {
-        // TODO(b/388321032): Replace this string with a string in a translatable xml file.
-        return "Silences interruptions from people and apps in different circumstances"
-    }
+    // TODO(b/388321032): Replace this string with a string in a translatable xml file.
+    override val subTitle = "Silences interruptions from people and apps in different circumstances"
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
index c84ddb6..59f209e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
@@ -23,19 +23,15 @@
 class ScreenRecordDetailsViewModel(
     val recordingController: RecordingController,
     val onStartRecordingClicked: Runnable,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
 
     override fun clickOnSettingsButton() {
         // No settings button in this tile.
     }
 
-    override fun getTitle(): String {
-        // TODO(b/388321032): Replace this string with a string in a translatable xml file,
-        return "Screen recording"
-    }
+    // TODO(b/388321032): Replace this string with a string in a translatable xml file,
+    override val title = "Screen recording"
 
-    override fun getSubTitle(): String {
-        // No sub-title in this tile.
-        return ""
-    }
+    // No sub-title in this tile.
+    override val subTitle = ""
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 3be2f1b..362b5db 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.shade
 
 import android.content.Context
+import android.content.res.Configuration
 import android.graphics.Rect
 import android.os.PowerManager
 import android.os.SystemClock
@@ -25,11 +26,13 @@
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
+import android.view.WindowInsets
 import android.widget.FrameLayout
 import androidx.activity.OnBackPressedDispatcher
 import androidx.activity.OnBackPressedDispatcherOwner
 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
 import androidx.compose.ui.platform.ComposeView
+import androidx.core.view.updateMargins
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
 import androidx.lifecycle.LifecycleObserver
@@ -101,7 +104,10 @@
 ) : LifecycleOwner {
     private val logger = Logger(logBuffer, TAG)
 
-    private class CommunalWrapper(context: Context) : FrameLayout(context) {
+    private class CommunalWrapper(
+        context: Context,
+        private val communalSettingsInteractor: CommunalSettingsInteractor,
+    ) : FrameLayout(context) {
         private val consumers: MutableSet<Consumer<Boolean>> = ArraySet()
 
         override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
@@ -121,6 +127,24 @@
                 consumers.clear()
             }
         }
+
+        override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets {
+            if (
+                !communalSettingsInteractor.isV2FlagEnabled() ||
+                    resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
+            ) {
+                return super.onApplyWindowInsets(windowInsets)
+            }
+            val type = WindowInsets.Type.displayCutout()
+            val insets = windowInsets.getInsets(type)
+
+            // Reset horizontal margins added by window insets, so hub can be edge to edge.
+            if (insets.left > 0 || insets.right > 0) {
+                val lp = layoutParams as LayoutParams
+                lp.updateMargins(0, lp.topMargin, 0, lp.bottomMargin)
+            }
+            return WindowInsets.CONSUMED
+        }
     }
 
     /** The container view for the hub. This will not be initialized until [initView] is called. */
@@ -443,7 +467,8 @@
         collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it })
         collectFlow(containerView, communalViewModel.swipeToHubEnabled, { swipeToHubEnabled = it })
 
-        communalContainerWrapper = CommunalWrapper(containerView.context)
+        communalContainerWrapper =
+            CommunalWrapper(containerView.context, communalSettingsInteractor)
         communalContainerWrapper?.addView(communalContainerView)
         logger.d("Hub container initialized")
         return communalContainerWrapper!!
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index fa17b4f..dafb1a5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -498,17 +498,18 @@
     }
 
     private boolean isExpanded(NotificationShadeWindowState state) {
+        boolean visForBlur = !Flags.disableShadeVisibleWithBlur() && state.backgroundBlurRadius > 0;
         boolean isExpanded = !state.forceWindowCollapsed && (state.isKeyguardShowingAndNotOccluded()
                 || state.panelVisible || state.keyguardFadingAway || state.bouncerShowing
                 || state.headsUpNotificationShowing
                 || state.scrimsVisibility != ScrimController.TRANSPARENT)
-                || state.backgroundBlurRadius > 0
+                || visForBlur
                 || state.launchingActivityFromNotification;
         mLogger.logIsExpanded(isExpanded, state.forceWindowCollapsed,
                 state.isKeyguardShowingAndNotOccluded(), state.panelVisible,
                 state.keyguardFadingAway, state.bouncerShowing, state.headsUpNotificationShowing,
                 state.scrimsVisibility != ScrimController.TRANSPARENT,
-                state.backgroundBlurRadius > 0, state.launchingActivityFromNotification);
+                visForBlur, state.launchingActivityFromNotification);
         return isExpanded;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
index 4d68f2e..f653573 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
@@ -33,7 +33,8 @@
 import com.android.systemui.statusbar.notification.icon.IconPack;
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import kotlinx.coroutines.flow.MutableStateFlow;
@@ -51,10 +52,27 @@
     // TODO (b/389839319): implement the row
     private ExpandableNotificationRow mRow;
 
+    private final List<ListEntry> mChildren = new ArrayList<>();
+
+    private final List<ListEntry> mUnmodifiableChildren = Collections.unmodifiableList(mChildren);
+
     public BundleEntry(String key) {
         super(key);
     }
 
+    void addChild(ListEntry child) {
+        mChildren.add(child);
+    }
+
+    @NonNull
+    public List<ListEntry> getChildren() {
+        return mUnmodifiableChildren;
+    }
+
+    /**
+     * @return Null because bundles do not have an associated NotificationEntry.
+     */
+
     @Nullable
     @Override
     public NotificationEntry getRepresentativeEntry() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
index 3fad8f0..1f32b94 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.notification.collection.BundleEntry;
 import com.android.systemui.statusbar.notification.collection.PipelineEntry;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -116,6 +117,9 @@
             NotificationPriorityBucketKt.BUCKET_SILENT) {
         @Override
         public boolean isInSection(PipelineEntry entry) {
+            if (entry instanceof BundleEntry) {
+                return true;
+            }
             return !mHighPriorityProvider.isHighPriority(entry)
                     && entry.getRepresentativeEntry() != null
                     && !entry.getRepresentativeEntry().isAmbient();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index c2f0806..6b32c6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.notification.collection.inflation;
 
 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE;
-import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
@@ -276,7 +275,7 @@
 
         if (LockscreenOtpRedaction.isSingleLineViewEnabled()) {
             if (inflaterParams.isChildInGroup()
-                    && redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) {
+                    && redactionType != REDACTION_TYPE_NONE) {
                 params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE);
             } else {
                 params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index b481455..8da2f76 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -3054,6 +3054,9 @@
 
         mUserLocked = userLocked;
         mPrivateLayout.setUserExpanding(userLocked);
+        if (android.app.Flags.expandingPublicView()) {
+            mPublicLayout.setUserExpanding(userLocked);
+        }
         // This is intentionally not guarded with mIsSummaryWithChildren since we might have had
         // children but not anymore.
         if (mChildrenContainer != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 1932037..676c96e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -293,8 +293,8 @@
                 useExactly = true;
             }
             int spec = MeasureSpec.makeMeasureSpec(size, useExactly
-                            ? MeasureSpec.EXACTLY
-                            : MeasureSpec.AT_MOST);
+                    ? MeasureSpec.EXACTLY
+                    : MeasureSpec.AT_MOST);
             measureChildWithMargins(mExpandedChild, widthMeasureSpec, 0, spec, 0);
             maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight());
         }
@@ -721,14 +721,14 @@
     private int getMinContentHeightHint() {
         if (mIsChildInGroup && isVisibleOrTransitioning(VISIBLE_TYPE_SINGLELINE)) {
             return mContext.getResources().getDimensionPixelSize(
-                        com.android.internal.R.dimen.notification_action_list_height);
+                    com.android.internal.R.dimen.notification_action_list_height);
         }
 
         // Transition between heads-up & expanded, or pinned.
         if (mHeadsUpChild != null && mExpandedChild != null) {
             boolean transitioningBetweenHunAndExpanded =
                     isTransitioningFromTo(VISIBLE_TYPE_HEADSUP, VISIBLE_TYPE_EXPANDED) ||
-                    isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP);
+                            isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP);
             boolean pinned = !isVisibleOrTransitioning(VISIBLE_TYPE_CONTRACTED)
                     && (mIsHeadsUp || mHeadsUpAnimatingAway)
                     && mContainingNotification.canShowHeadsUp();
@@ -758,7 +758,7 @@
         } else if (mContractedChild != null) {
             hint = getViewHeight(VISIBLE_TYPE_CONTRACTED)
                     + mContext.getResources().getDimensionPixelSize(
-                            com.android.internal.R.dimen.notification_action_list_height);
+                    com.android.internal.R.dimen.notification_action_list_height);
         } else {
             hint = getMinHeight();
         }
@@ -1053,8 +1053,8 @@
         // the original type
         final int visibleType = (
                 isGroupExpanded() || mContainingNotification.isUserLocked())
-                    ? calculateVisibleType()
-                    : getVisibleType();
+                ? calculateVisibleType()
+                : getVisibleType();
         return getBackgroundColor(visibleType);
     }
 
@@ -1240,7 +1240,14 @@
                 height = mContentHeight;
             }
             int expandedVisualType = getVisualTypeForHeight(height);
-            int collapsedVisualType = mIsChildInGroup && !isGroupExpanded()
+            final boolean shouldShowSingleLineView = mIsChildInGroup && !isGroupExpanded();
+            final boolean isSingleLineViewPresent = mSingleLineView != null;
+
+            if (shouldShowSingleLineView && !isSingleLineViewPresent) {
+                Log.wtf(TAG, "calculateVisibleType: SingleLineView is not available!");
+            }
+
+            final int collapsedVisualType = shouldShowSingleLineView && isSingleLineViewPresent
                     ? VISIBLE_TYPE_SINGLELINE
                     : getVisualTypeForHeight(mContainingNotification.getCollapsedHeight());
             return mTransformationStartVisibleType == collapsedVisualType
@@ -1261,7 +1268,13 @@
         if (!noExpandedChild && viewHeight == getViewHeight(VISIBLE_TYPE_EXPANDED)) {
             return VISIBLE_TYPE_EXPANDED;
         }
-        if (!mUserExpanding && mIsChildInGroup && !isGroupExpanded()) {
+        final boolean shouldShowSingleLineView = mIsChildInGroup && !isGroupExpanded();
+        final boolean isSingleLinePresent =  mSingleLineView != null;
+        if (shouldShowSingleLineView && !isSingleLinePresent) {
+            Log.wtf(TAG, "getVisualTypeForHeight: singleLineView is not available.");
+        }
+
+        if (!mUserExpanding && shouldShowSingleLineView && isSingleLinePresent) {
             return VISIBLE_TYPE_SINGLELINE;
         }
 
@@ -1276,7 +1289,7 @@
             if (noExpandedChild || (mContractedChild != null
                     && viewHeight <= getViewHeight(VISIBLE_TYPE_CONTRACTED)
                     && (!mIsChildInGroup || isGroupExpanded()
-                            || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) {
+                    || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) {
                 return VISIBLE_TYPE_CONTRACTED;
             } else if (!noExpandedChild) {
                 return VISIBLE_TYPE_EXPANDED;
@@ -1687,7 +1700,7 @@
                             : smartReplies.fromAssistant;
                     boolean editBeforeSending = smartReplies != null
                             && mSmartReplyConstants.getEffectiveEditChoicesBeforeSending(
-                                    smartReplies.remoteInput.getEditChoicesBeforeSending());
+                            smartReplies.remoteInput.getEditChoicesBeforeSending());
 
                     mSmartReplyController.smartSuggestionsAdded(mNotificationEntry, numSmartReplies,
                             numSmartActions, fromAssistant, editBeforeSending);
@@ -2114,8 +2127,8 @@
     public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) {
         boolean needsPaddings = shouldClipToRounding(getVisibleType(), topRounded, bottomRounded);
         if (mUserExpanding) {
-             needsPaddings |= shouldClipToRounding(mTransformationStartVisibleType, topRounded,
-                     bottomRounded);
+            needsPaddings |= shouldClipToRounding(mTransformationStartVisibleType, topRounded,
+                    bottomRounded);
         }
         return needsPaddings;
     }
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 c2c271b..e6bb1b9 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
@@ -64,7 +64,6 @@
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.MathUtils;
-import android.util.Pair;
 import android.view.DisplayCutout;
 import android.view.InputDevice;
 import android.view.LayoutInflater;
@@ -141,7 +140,6 @@
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.policy.ScrollAdapter;
 import com.android.systemui.statusbar.policy.SplitShadeStateController;
-import com.android.systemui.statusbar.ui.SystemBarUtilsProxy;
 import com.android.systemui.util.Assert;
 import com.android.systemui.util.ColorUtilKt;
 import com.android.systemui.util.DumpUtilsKt;
@@ -6004,6 +6002,7 @@
      *                 LockscreenShadeTransitionController resets fraction to 0
      *                 where it remains until the next lockscreen-to-shade transition.
      */
+    @Override
     public void setFractionToShade(float fraction) {
         mAmbientState.setFractionToShade(fraction);
         updateContentHeight();  // Recompute stack height with different section gap.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index bb3abc1..f7f8acf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -1735,6 +1735,7 @@
      *                 they remain until the next lockscreen-to-shade transition.
      */
     public void setTransitionToFullShadeAmount(float fraction) {
+        SceneContainerFlag.assertInLegacyMode();
         mView.setFractionToShade(fraction);
     }
 
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 9c855e9..ac89f3a 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
@@ -107,6 +107,9 @@
     /** sets the current expand fraction */
     fun setExpandFraction(expandFraction: Float)
 
+    /** Sets the fraction of the LockScreen -> Shade transition. */
+    fun setFractionToShade(fraction: Float)
+
     /** sets the current QS expand fraction */
     fun setQsExpandFraction(expandFraction: Float)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index 653344a..40739b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -95,6 +95,11 @@
                     view.setExpandFraction(it.coerceIn(0f, 1f))
                 }
             }
+            launch {
+                viewModel.lockScreenToShadeTransitionProgress.collectTraced {
+                    view.setFractionToShade(it.coerceIn(0f, 1f))
+                }
+            }
             launch { viewModel.qsExpandFraction.collectTraced { view.setQsExpandFraction(it) } }
             launch { viewModel.blurRadius(maxBlurRadius).collect(view::setBlurRadius) }
             launch {
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 c1aa5f1..940b2e5 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
@@ -207,6 +207,44 @@
     val qsExpandFraction: Flow<Float> =
         shadeInteractor.qsExpansion.dumpWhileCollecting("qsExpandFraction")
 
+    /**
+     * Fraction of the LockScreen -> Shade transition. 0..1 while the transition in progress, and
+     * snaps back to 0 when it is Idle.
+     */
+    val lockScreenToShadeTransitionProgress: Flow<Float> =
+        combine(
+                shadeInteractor.shadeExpansion,
+                shadeModeInteractor.shadeMode,
+                sceneInteractor.transitionState,
+            ) { shadeExpansion, _, transitionState ->
+                when (transitionState) {
+                    is Idle -> 0f
+                    is ChangeScene ->
+                        if (
+                            transitionState.isTransitioning(
+                                from = Scenes.Lockscreen,
+                                to = Scenes.Shade,
+                            )
+                        ) {
+                            shadeExpansion
+                        } else {
+                            0f
+                        }
+
+                    is Transition.OverlayTransition ->
+                        if (
+                            transitionState.currentScene == Scenes.Lockscreen &&
+                                transitionState.isTransitioning(to = Overlays.NotificationsShade)
+                        ) {
+                            shadeExpansion
+                        } else {
+                            0f
+                        }
+                }
+            }
+            .distinctUntilChanged()
+            .dumpWhileCollecting("lockScreenToShadeTransitionProgress")
+
     val isOccluded: Flow<Boolean> =
         bouncerInteractor.bouncerExpansion
             .map { it == 1f }
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt
index 3bd8af6..6657c42 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
@@ -31,7 +32,14 @@
 constructor(
     @Background val backgroundDispatcher: CoroutineDispatcher,
     val userRepository: UserRepository,
+    val selectedUserInteractor: SelectedUserInteractor,
 ) {
+    /** Whether the current user is unlocked */
+    val currentUserUnlocked: Flow<Boolean> =
+        selectedUserInteractor.selectedUserInfo.flatMapLatestConflated { user ->
+            isUserUnlocked(user.userHandle)
+        }
+
     fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> =
         userRepository.isUserUnlocked(userHandle).flowOn(backgroundDispatcher)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
index 845be02..60345a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
@@ -24,12 +24,15 @@
 import android.window.RemoteTransition
 import android.window.TransitionFilter
 import android.window.WindowAnimationState
+import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.activity.EmptyTestActivity
+import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.shared.Flags
+import com.android.systemui.shared.Flags as SharedFlags
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
 import com.android.wm.shell.shared.ShellTransitions
@@ -43,7 +46,6 @@
 import kotlin.test.assertEquals
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
 import org.junit.After
 import org.junit.Assert.assertThrows
 import org.junit.Before
@@ -57,8 +59,8 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
-import org.mockito.Spy
 import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.spy
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -66,21 +68,23 @@
 @RunWithLooper
 class ActivityTransitionAnimatorTest : SysuiTestCase() {
     private val kosmos = testKosmos()
-    private val transitionContainer = LinearLayout(mContext)
+
     private val mainExecutor = context.mainExecutor
     private val testTransitionAnimator = fakeTransitionAnimator(mainExecutor)
     private val testShellTransitions = FakeShellTransitions()
+
+    private val Kosmos.underTest by Kosmos.Fixture { activityTransitionAnimator }
+
     @Mock lateinit var callback: ActivityTransitionAnimator.Callback
     @Mock lateinit var listener: ActivityTransitionAnimator.Listener
-    @Spy private val controller = TestTransitionAnimatorController(transitionContainer)
     @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
 
-    private lateinit var underTest: ActivityTransitionAnimator
-    @get:Rule val rule = MockitoJUnit.rule()
+    @get:Rule(order = 0) val mockitoRule = MockitoJUnit.rule()
+    @get:Rule(order = 1) val activityRule = ActivityScenarioRule(EmptyTestActivity::class.java)
 
     @Before
     fun setup() {
-        underTest =
+        kosmos.activityTransitionAnimator =
             ActivityTransitionAnimator(
                 mainExecutor,
                 ActivityTransitionAnimator.TransitionRegister.fromShellTransitions(
@@ -89,19 +93,20 @@
                 testTransitionAnimator,
                 testTransitionAnimator,
                 disableWmTimeout = true,
+                skipReparentTransaction = true,
             )
-        underTest.callback = callback
-        underTest.addListener(listener)
+        kosmos.activityTransitionAnimator.callback = callback
+        kosmos.activityTransitionAnimator.addListener(listener)
     }
 
     @After
     fun tearDown() {
-        underTest.removeListener(listener)
+        kosmos.activityTransitionAnimator.removeListener(listener)
     }
 
     private fun startIntentWithAnimation(
-        animator: ActivityTransitionAnimator = underTest,
-        controller: ActivityTransitionAnimator.Controller? = this.controller,
+        controller: ActivityTransitionAnimator.Controller?,
+        animator: ActivityTransitionAnimator = kosmos.activityTransitionAnimator,
         animate: Boolean = true,
         intentStarter: (RemoteAnimationAdapter?) -> Int,
     ) {
@@ -119,129 +124,152 @@
 
     @Test
     fun animationAdapterIsNullIfControllerIsNull() {
-        var startedIntent = false
-        var animationAdapter: RemoteAnimationAdapter? = null
+        kosmos.runTest {
+            var startedIntent = false
+            var animationAdapter: RemoteAnimationAdapter? = null
 
-        startIntentWithAnimation(controller = null) { adapter ->
-            startedIntent = true
-            animationAdapter = adapter
+            startIntentWithAnimation(controller = null) { adapter ->
+                startedIntent = true
+                animationAdapter = adapter
 
-            ActivityManager.START_SUCCESS
+                ActivityManager.START_SUCCESS
+            }
+
+            assertTrue(startedIntent)
+            assertNull(animationAdapter)
         }
-
-        assertTrue(startedIntent)
-        assertNull(animationAdapter)
     }
 
     @Test
     fun animatesIfActivityOpens() {
-        val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
-        var animationAdapter: RemoteAnimationAdapter? = null
-        startIntentWithAnimation { adapter ->
-            animationAdapter = adapter
-            ActivityManager.START_SUCCESS
-        }
+        kosmos.runTest {
+            val controller = createController()
+            val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+            var animationAdapter: RemoteAnimationAdapter? = null
+            startIntentWithAnimation(controller) { adapter ->
+                animationAdapter = adapter
+                ActivityManager.START_SUCCESS
+            }
 
-        assertNotNull(animationAdapter)
-        waitForIdleSync()
-        verify(controller).onIntentStarted(willAnimateCaptor.capture())
-        assertTrue(willAnimateCaptor.value)
+            assertNotNull(animationAdapter)
+            waitForIdleSync()
+            verify(controller).onIntentStarted(willAnimateCaptor.capture())
+            assertTrue(willAnimateCaptor.value)
+        }
     }
 
     @Test
     fun doesNotAnimateIfActivityIsAlreadyOpen() {
-        val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
-        startIntentWithAnimation { ActivityManager.START_DELIVERED_TO_TOP }
+        kosmos.runTest {
+            val controller = createController()
+            val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+            startIntentWithAnimation(controller) { ActivityManager.START_DELIVERED_TO_TOP }
 
-        waitForIdleSync()
-        verify(controller).onIntentStarted(willAnimateCaptor.capture())
-        assertFalse(willAnimateCaptor.value)
+            waitForIdleSync()
+            verify(controller).onIntentStarted(willAnimateCaptor.capture())
+            assertFalse(willAnimateCaptor.value)
+        }
     }
 
     @Test
     fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() {
-        `when`(callback.isOnKeyguard()).thenReturn(true)
+        kosmos.runTest {
+            `when`(callback.isOnKeyguard()).thenReturn(true)
 
-        val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
-        var animationAdapter: RemoteAnimationAdapter? = null
+            val controller = createController()
+            val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+            var animationAdapter: RemoteAnimationAdapter? = null
 
-        startIntentWithAnimation(underTest) { adapter ->
-            animationAdapter = adapter
-            ActivityManager.START_DELIVERED_TO_TOP
+            startIntentWithAnimation(controller, underTest) { adapter ->
+                animationAdapter = adapter
+                ActivityManager.START_DELIVERED_TO_TOP
+            }
+
+            waitForIdleSync()
+            verify(controller).onIntentStarted(willAnimateCaptor.capture())
+            verify(callback).hideKeyguardWithAnimation(any())
+
+            assertTrue(willAnimateCaptor.value)
+            assertNull(animationAdapter)
         }
-
-        waitForIdleSync()
-        verify(controller).onIntentStarted(willAnimateCaptor.capture())
-        verify(callback).hideKeyguardWithAnimation(any())
-
-        assertTrue(willAnimateCaptor.value)
-        assertNull(animationAdapter)
     }
 
     @Test
     fun doesNotAnimateIfAnimateIsFalse() {
-        val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
-        startIntentWithAnimation(animate = false) { ActivityManager.START_SUCCESS }
+        kosmos.runTest {
+            val controller = createController()
+            val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+            startIntentWithAnimation(controller, animate = false) { ActivityManager.START_SUCCESS }
 
-        waitForIdleSync()
-        verify(controller).onIntentStarted(willAnimateCaptor.capture())
-        assertFalse(willAnimateCaptor.value)
+            waitForIdleSync()
+            verify(controller).onIntentStarted(willAnimateCaptor.capture())
+            assertFalse(willAnimateCaptor.value)
+        }
     }
 
-    @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY)
+    @EnableFlags(SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY)
     @Test
     fun registersReturnIffCookieIsPresent() {
-        `when`(callback.isOnKeyguard()).thenReturn(false)
+        kosmos.runTest {
+            `when`(callback.isOnKeyguard()).thenReturn(false)
 
-        startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP }
-
-        waitForIdleSync()
-        assertTrue(testShellTransitions.remotes.isEmpty())
-        assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
-
-        val controller =
-            object : DelegateTransitionAnimatorController(controller) {
-                override val transitionCookie
-                    get() = ActivityTransitionAnimator.TransitionCookie("testCookie")
+            val controller = createController()
+            startIntentWithAnimation(controller, underTest) {
+                ActivityManager.START_DELIVERED_TO_TOP
             }
 
-        startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP }
+            waitForIdleSync()
+            assertTrue(testShellTransitions.remotes.isEmpty())
+            assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
 
-        waitForIdleSync()
-        assertEquals(1, testShellTransitions.remotes.size)
-        assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
+            val controllerWithCookie =
+                object : DelegateTransitionAnimatorController(controller) {
+                    override val transitionCookie
+                        get() = ActivityTransitionAnimator.TransitionCookie("testCookie")
+                }
+
+            startIntentWithAnimation(controllerWithCookie, underTest) {
+                ActivityManager.START_DELIVERED_TO_TOP
+            }
+
+            waitForIdleSync()
+            assertEquals(1, testShellTransitions.remotes.size)
+            assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
+        }
     }
 
     @EnableFlags(
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
     )
     @Test
     fun registersLongLivedTransition() {
         kosmos.runTest {
-            var factory = controllerFactory()
+            val controller = createController()
+            var factory = controllerFactory(controller)
             underTest.register(factory.cookie, factory, testScope)
             assertEquals(2, testShellTransitions.remotes.size)
 
-            factory = controllerFactory()
+            factory = controllerFactory(controller)
             underTest.register(factory.cookie, factory, testScope)
             assertEquals(4, testShellTransitions.remotes.size)
         }
     }
 
     @EnableFlags(
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
     )
     @Test
     fun registersLongLivedTransitionOverridingPreviousRegistration() {
         kosmos.runTest {
+            val controller = createController()
             val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie")
-            var factory = controllerFactory(cookie)
+            var factory = controllerFactory(controller, cookie)
             underTest.register(cookie, factory, testScope)
             val transitions = testShellTransitions.remotes.values.toList()
 
-            factory = controllerFactory(cookie)
+            factory = controllerFactory(controller, cookie)
             underTest.register(cookie, factory, testScope)
             assertEquals(2, testShellTransitions.remotes.size)
             for (transition in transitions) {
@@ -250,23 +278,25 @@
         }
     }
 
-    @DisableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
+    @DisableFlags(SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
     @Test
     fun doesNotRegisterLongLivedTransitionIfFlagIsDisabled() {
         kosmos.runTest {
-            val factory = controllerFactory(component = null)
+            val factory = controllerFactory(createController(), component = null)
             assertThrows(IllegalStateException::class.java) {
                 underTest.register(factory.cookie, factory, testScope)
             }
         }
     }
 
-    @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
+    @EnableFlags(SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
     @Test
     fun doesNotRegisterLongLivedTransitionIfMissingRequiredProperties() {
         kosmos.runTest {
+            val controller = createController()
+
             // No ComponentName
-            var factory = controllerFactory(component = null)
+            var factory = controllerFactory(controller, component = null)
             assertThrows(IllegalStateException::class.java) {
                 underTest.register(factory.cookie, factory, testScope)
             }
@@ -280,7 +310,7 @@
                     testTransitionAnimator,
                     disableWmTimeout = true,
                 )
-            factory = controllerFactory()
+            factory = controllerFactory(controller)
             assertThrows(IllegalStateException::class.java) {
                 activityTransitionAnimator.register(factory.cookie, factory, testScope)
             }
@@ -288,17 +318,18 @@
     }
 
     @EnableFlags(
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
     )
     @Test
     fun unregistersLongLivedTransition() {
         kosmos.runTest {
+            val controller = createController()
             val cookies = arrayOfNulls<ActivityTransitionAnimator.TransitionCookie>(3)
 
             for (index in 0 until 3) {
                 cookies[index] = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
-                val factory = controllerFactory(cookies[index]!!)
+                val factory = controllerFactory(controller, cookies[index]!!)
                 underTest.register(factory.cookie, factory, testScope)
             }
 
@@ -315,75 +346,98 @@
 
     @Test
     fun doesNotStartIfAnimationIsCancelled() {
-        val runner = underTest.createEphemeralRunner(controller)
-        runner.onAnimationCancelled()
-        runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback)
+        kosmos.runTest {
+            val controller = createController()
+            val runner = underTest.createEphemeralRunner(controller)
+            runner.onAnimationCancelled()
+            runner.onAnimationStart(
+                TRANSIT_NONE,
+                emptyArray(),
+                emptyArray(),
+                emptyArray(),
+                iCallback,
+            )
 
-        waitForIdleSync()
-        verify(controller).onTransitionAnimationCancelled()
-        verify(controller, never()).onTransitionAnimationStart(anyBoolean())
-        verify(listener).onTransitionAnimationCancelled()
-        verify(listener, never()).onTransitionAnimationStart()
-        assertNull(runner.delegate)
+            waitForIdleSync()
+            verify(controller).onTransitionAnimationCancelled()
+            verify(controller, never()).onTransitionAnimationStart(anyBoolean())
+            verify(listener).onTransitionAnimationCancelled()
+            verify(listener, never()).onTransitionAnimationStart()
+            assertNull(runner.delegate)
+        }
     }
 
     @Test
     fun cancelsIfNoOpeningWindowIsFound() {
-        val runner = underTest.createEphemeralRunner(controller)
-        runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback)
+        kosmos.runTest {
+            val controller = createController()
+            val runner = underTest.createEphemeralRunner(controller)
+            runner.onAnimationStart(
+                TRANSIT_NONE,
+                emptyArray(),
+                emptyArray(),
+                emptyArray(),
+                iCallback,
+            )
 
-        waitForIdleSync()
-        verify(controller).onTransitionAnimationCancelled()
-        verify(controller, never()).onTransitionAnimationStart(anyBoolean())
-        verify(listener).onTransitionAnimationCancelled()
-        verify(listener, never()).onTransitionAnimationStart()
-        assertNull(runner.delegate)
+            waitForIdleSync()
+            verify(controller).onTransitionAnimationCancelled()
+            verify(controller, never()).onTransitionAnimationStart(anyBoolean())
+            verify(listener).onTransitionAnimationCancelled()
+            verify(listener, never()).onTransitionAnimationStart()
+            assertNull(runner.delegate)
+        }
     }
 
     @Test
     fun startsAnimationIfWindowIsOpening() {
-        val runner = underTest.createEphemeralRunner(controller)
-        runner.onAnimationStart(
-            TRANSIT_NONE,
-            arrayOf(fakeWindow()),
-            emptyArray(),
-            emptyArray(),
-            iCallback,
-        )
-        waitForIdleSync()
-        verify(listener).onTransitionAnimationStart()
-        verify(controller).onTransitionAnimationStart(anyBoolean())
+        kosmos.runTest {
+            val controller = createController()
+            val runner = underTest.createEphemeralRunner(controller)
+            runner.onAnimationStart(
+                TRANSIT_NONE,
+                arrayOf(fakeWindow()),
+                emptyArray(),
+                emptyArray(),
+                iCallback,
+            )
+            waitForIdleSync()
+            verify(listener).onTransitionAnimationStart()
+            verify(controller).onTransitionAnimationStart(anyBoolean())
+        }
     }
 
     @Test
     fun creatingControllerFromNormalViewThrows() {
-        assertThrows(IllegalArgumentException::class.java) {
-            ActivityTransitionAnimator.Controller.fromView(FrameLayout(mContext))
+        kosmos.runTest {
+            assertThrows(IllegalArgumentException::class.java) {
+                ActivityTransitionAnimator.Controller.fromView(FrameLayout(mContext))
+            }
         }
     }
 
     @DisableFlags(
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
     )
     @Test
     fun creatingRunnerWithLazyInitializationThrows_whenTheFlagsAreDisabled() {
         kosmos.runTest {
             assertThrows(IllegalStateException::class.java) {
-                val factory = controllerFactory()
+                val factory = controllerFactory(createController())
                 underTest.createLongLivedRunner(factory, testScope, forLaunch = true)
             }
         }
     }
 
     @EnableFlags(
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
     )
     @Test
     fun runnerCreatesDelegateLazily_onAnimationStart() {
         kosmos.runTest {
-            val factory = controllerFactory()
+            val factory = controllerFactory(createController())
             val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = true)
             assertNull(runner.delegate)
 
@@ -412,13 +466,13 @@
     }
 
     @EnableFlags(
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
     )
     @Test
     fun runnerCreatesDelegateLazily_onAnimationTakeover() {
         kosmos.runTest {
-            val factory = controllerFactory()
+            val factory = controllerFactory(createController())
             val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = false)
             assertNull(runner.delegate)
 
@@ -446,58 +500,78 @@
     }
 
     @DisableFlags(
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
     )
     @Test
     fun animationTakeoverThrows_whenTheFlagsAreDisabled() {
-        val runner = underTest.createEphemeralRunner(controller)
-        assertThrows(IllegalStateException::class.java) {
-            runner.takeOverAnimation(
-                arrayOf(fakeWindow()),
-                emptyArray(),
-                SurfaceControl.Transaction(),
-                iCallback,
-            )
+        kosmos.runTest {
+            val controller = createController()
+            val runner = underTest.createEphemeralRunner(controller)
+            assertThrows(IllegalStateException::class.java) {
+                runner.takeOverAnimation(
+                    arrayOf(fakeWindow()),
+                    emptyArray(),
+                    SurfaceControl.Transaction(),
+                    iCallback,
+                )
+            }
         }
     }
 
     @DisableFlags(
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
-        Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+        SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
     )
     @Test
     fun disposeRunner_delegateDereferenced() {
-        val runner = underTest.createEphemeralRunner(controller)
-        assertNotNull(runner.delegate)
-        runner.dispose()
-        waitForIdleSync()
-        assertNull(runner.delegate)
+        kosmos.runTest {
+            val controller = createController()
+            val runner = underTest.createEphemeralRunner(controller)
+            assertNotNull(runner.delegate)
+            runner.dispose()
+            waitForIdleSync()
+            assertNull(runner.delegate)
+        }
     }
 
     @Test
     fun concurrentListenerModification_doesNotThrow() {
-        // Need a second listener to trigger the concurrent modification.
-        underTest.addListener(object : ActivityTransitionAnimator.Listener {})
-        `when`(listener.onTransitionAnimationStart()).thenAnswer {
-            underTest.removeListener(listener)
-            listener
+        kosmos.runTest {
+            // Need a second listener to trigger the concurrent modification.
+            underTest.addListener(object : ActivityTransitionAnimator.Listener {})
+            `when`(listener.onTransitionAnimationStart()).thenAnswer {
+                underTest.removeListener(listener)
+                listener
+            }
+
+            val controller = createController()
+            val runner = underTest.createEphemeralRunner(controller)
+            runner.onAnimationStart(
+                TRANSIT_NONE,
+                arrayOf(fakeWindow()),
+                emptyArray(),
+                emptyArray(),
+                iCallback,
+            )
+
+            waitForIdleSync()
+            verify(listener).onTransitionAnimationStart()
         }
+    }
 
-        val runner = underTest.createEphemeralRunner(controller)
-        runner.onAnimationStart(
-            TRANSIT_NONE,
-            arrayOf(fakeWindow()),
-            emptyArray(),
-            emptyArray(),
-            iCallback,
-        )
-
+    private fun createController(): TestTransitionAnimatorController {
+        lateinit var transitionContainer: ViewGroup
+        activityRule.scenario.onActivity { activity ->
+            transitionContainer = LinearLayout(activity)
+            activity.setContentView(transitionContainer)
+        }
         waitForIdleSync()
-        verify(listener).onTransitionAnimationStart()
+        return spy(TestTransitionAnimatorController(transitionContainer))
     }
 
     private fun controllerFactory(
+        controller: ActivityTransitionAnimator.Controller,
         cookie: ActivityTransitionAnimator.TransitionCookie =
             mock(ActivityTransitionAnimator.TransitionCookie::class.java),
         component: ComponentName? = mock(ComponentName::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
index b274f1c..d9c59d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
@@ -88,11 +88,6 @@
 
     companion object {
         private val INTENT = Intent("some.intent.action")
-        private val DRAWABLE =
-            mock<Icon> {
-                whenever(this.contentDescription)
-                    .thenReturn(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
-            }
         private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
 
         @Parameters(
@@ -337,7 +332,17 @@
 
             homeControls.setState(
                 lockScreenState =
-                    KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = DRAWABLE)
+                    KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                        icon =
+                            mock<Icon> {
+                                whenever(contentDescription)
+                                    .thenReturn(
+                                        ContentDescription.Resource(
+                                            res = CONTENT_DESCRIPTION_RESOURCE_ID
+                                        )
+                                    )
+                            }
+                    )
             )
             homeControls.onTriggeredResult =
                 if (startActivity) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
index 175e8d4..e048d46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
@@ -33,8 +33,8 @@
 import android.platform.test.annotations.RequiresFlagsDisabled
 import android.platform.test.annotations.RequiresFlagsEnabled
 import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.platform.test.flag.junit.FlagsParameterization
 import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
 import com.android.settingslib.bluetooth.LocalBluetoothManager
@@ -77,6 +77,8 @@
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
 import org.mockito.kotlin.eq
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 private const val KEY = "TEST_KEY"
 private const val KEY_OLD = "TEST_KEY_OLD"
@@ -89,12 +91,24 @@
 private const val NORMAL_APP_NAME = "NORMAL_APP_NAME"
 
 @SmallTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 @TestableLooper.RunWithLooper
-public class MediaDeviceManagerTest : SysuiTestCase() {
+public class MediaDeviceManagerTest(flags: FlagsParameterization) : SysuiTestCase() {
 
-    private companion object {
+    companion object {
         val OTHER_DEVICE_ICON_STUB = TestStubDrawable()
+
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.progressionOf(
+                com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DEVICE_MANAGER_BACKGROUND_EXECUTION
+            )
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
@@ -187,6 +201,8 @@
     @Test
     fun loadMediaData() {
         manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
         verify(lmmFactory).create(PACKAGE)
     }
 
@@ -195,6 +211,7 @@
         manager.onMediaDataLoaded(KEY, null, mediaData)
         manager.onMediaDataRemoved(KEY, false)
         fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
         verify(lmm).unregisterCallback(any())
         verify(muteAwaitManager).stopListening()
     }
@@ -406,6 +423,8 @@
         manager.onMediaDataLoaded(KEY, null, mediaData)
         // WHEN the notification is removed
         manager.onMediaDataRemoved(KEY, true)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
         // THEN the listener receives key removed event
         verify(listener).onKeyRemoved(eq(KEY), eq(true))
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
index c20a801..a8bfbd1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
@@ -103,6 +103,8 @@
         whenever(internetWifiEntry.hasInternetAccess()).thenReturn(true)
         whenever(wifiEntries.size).thenReturn(1)
         whenever(internetDetailsContentController.getDialogTitleText()).thenReturn(TITLE)
+        whenever(internetDetailsContentController.getSubtitleText(ArgumentMatchers.anyBoolean()))
+            .thenReturn("")
         whenever(internetDetailsContentController.getMobileNetworkTitle(ArgumentMatchers.anyInt()))
             .thenReturn(MOBILE_NETWORK_TITLE)
         whenever(
@@ -128,15 +130,13 @@
                 internetDetailsContentController,
                 canConfigMobileData = true,
                 canConfigWifi = true,
-                coroutineScope = scope,
-                context = mContext,
                 uiEventLogger = mock<UiEventLogger>(),
                 handler = handler,
                 backgroundExecutor = bgExecutor,
                 keyguard = keyguard,
             )
 
-        internetDetailsContentManager.bind(contentView)
+        internetDetailsContentManager.bind(contentView, scope)
         internetDetailsContentManager.adapter = internetAdapter
         internetDetailsContentManager.connectedWifiEntry = internetWifiEntry
         internetDetailsContentManager.wifiEntriesCount = wifiEntries.size
@@ -777,6 +777,26 @@
         }
     }
 
+    @Test
+    fun updateTitleAndSubtitle() {
+        assertThat(internetDetailsContentManager.title).isEqualTo("Internet")
+        assertThat(internetDetailsContentManager.subTitle).isEqualTo("")
+
+        whenever(internetDetailsContentController.getDialogTitleText()).thenReturn("New title")
+        whenever(internetDetailsContentController.getSubtitleText(ArgumentMatchers.anyBoolean()))
+            .thenReturn("New subtitle")
+
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(internetDetailsContentManager.title).isEqualTo("New title")
+            assertThat(internetDetailsContentManager.subTitle).isEqualTo("New subtitle")
+        }
+    }
+
     companion object {
         private const val TITLE = "Internet"
         private const val MOBILE_NETWORK_TITLE = "Mobile Title"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java
new file mode 100644
index 0000000..c231be1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2025 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.ringtone;
+
+import static org.junit.Assert.assertThrows;
+
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.UserHandle;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RingtonePlayerTest extends SysuiTestCase {
+
+    private AudioManager mAudioManager;
+
+    private static final String TAG = "RingtonePlayerTest";
+
+    @Before
+    public void setup() throws Exception {
+        mAudioManager = getContext().getSystemService(AudioManager.class);
+    }
+
+    @Test
+    public void testRingtonePlayerUriUserCheck() {
+        android.media.IRingtonePlayer irp = mAudioManager.getRingtonePlayer();
+        final AudioAttributes aa = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build();
+        // get a UserId that doesn't belong to mine
+        final int otherUserId = UserHandle.myUserId() == 0 ? 10 : 0;
+        // build a URI that I shouldn't have access to
+        final Uri uri = new Uri.Builder()
+                .scheme("content").authority(otherUserId + "@media")
+                .appendPath("external").appendPath("downloads")
+                .appendPath("bogusPathThatDoesNotMatter.mp3")
+                .build();
+        if (android.media.audio.Flags.ringtoneUserUriCheck()) {
+            assertThrows(SecurityException.class, () ->
+                    irp.play(new Binder(), uri, aa, 1.0f /*volume*/, false /*looping*/)
+            );
+
+            assertThrows(SecurityException.class, () ->
+                    irp.getTitle(uri));
+        }
+    }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index a978ecd..8de931a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -204,8 +204,6 @@
 import com.android.wm.shell.taskview.TaskViewTransitions;
 import com.android.wm.shell.transition.Transitions;
 
-import kotlin.Lazy;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -216,9 +214,6 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.stubbing.Answer;
 
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -227,6 +222,10 @@
 import java.util.Optional;
 import java.util.concurrent.Executor;
 
+import kotlin.Lazy;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -602,14 +601,19 @@
         // Get a reference to KeyguardStateController.Callback
         verify(mKeyguardStateController, atLeastOnce())
                 .addCallback(mKeyguardStateControllerCallbackCaptor.capture());
+
+        // Make sure mocks are set up for current user
+        switchUser(ActivityManager.getCurrentUser());
     }
 
     @After
     public void tearDown() throws Exception {
-        ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles());
-        for (int i = 0; i < bubbles.size(); i++) {
-            mBubbleController.removeBubble(bubbles.get(i).getKey(),
-                    Bubbles.DISMISS_NO_LONGER_BUBBLE);
+        if (mBubbleData != null) {
+            ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles());
+            for (int i = 0; i < bubbles.size(); i++) {
+                mBubbleController.removeBubble(bubbles.get(i).getKey(),
+                        Bubbles.DISMISS_NO_LONGER_BUBBLE);
+            }
         }
         mTestableLooper.processAllMessages();
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryKosmos.kt
new file mode 100644
index 0000000..130c298
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2025 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.communal.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.carProjectionRepository by
+    Kosmos.Fixture<CarProjectionRepository> { FakeCarProjectionRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCarProjectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCarProjectionRepository.kt
new file mode 100644
index 0000000..4042342
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCarProjectionRepository.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2025 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.communal.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeCarProjectionRepository : CarProjectionRepository {
+    private val _projectionActive = MutableStateFlow(false)
+    override val projectionActive: Flow<Boolean> = _projectionActive.asStateFlow()
+
+    override suspend fun isProjectionActive(): Boolean {
+        return _projectionActive.value
+    }
+
+    fun setProjectionActive(active: Boolean) {
+        _projectionActive.value = active
+    }
+}
+
+val CarProjectionRepository.fake
+    get() = this as FakeCarProjectionRepository
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorKosmos.kt
new file mode 100644
index 0000000..23bbe36
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.interactor
+
+import com.android.systemui.communal.data.repository.carProjectionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.carProjectionInteractor by Fixture { CarProjectionInteractor(carProjectionRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorKosmos.kt
new file mode 100644
index 0000000..5735cf8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2025 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.communal.domain.interactor
+
+import com.android.systemui.common.domain.interactor.batteryInteractor
+import com.android.systemui.communal.posturing.domain.interactor.posturingInteractor
+import com.android.systemui.dock.dockManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.backgroundCoroutineContext
+
+val Kosmos.communalAutoOpenInteractor by Fixture {
+    CommunalAutoOpenInteractor(
+        communalSettingsInteractor = communalSettingsInteractor,
+        backgroundContext = backgroundCoroutineContext,
+        batteryInteractor = batteryInteractor,
+        posturingInteractor = posturingInteractor,
+        dockManager = dockManager,
+        allowSwipeAlways = false,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index b8b2ec5..316fcbb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -16,17 +16,14 @@
 
 package com.android.systemui.communal.domain.interactor
 
-import android.content.pm.UserInfo
 import android.content.testableContext
 import android.os.userManager
 import com.android.systemui.broadcast.broadcastDispatcher
-import com.android.systemui.common.domain.interactor.batteryInteractor
+import com.android.systemui.communal.data.model.SuppressionReason
 import com.android.systemui.communal.data.repository.communalMediaRepository
 import com.android.systemui.communal.data.repository.communalSmartspaceRepository
 import com.android.systemui.communal.data.repository.communalWidgetRepository
-import com.android.systemui.communal.posturing.domain.interactor.posturingInteractor
 import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
-import com.android.systemui.dock.dockManager
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -39,13 +36,9 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.activityStarter
-import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.settings.userTracker
 import com.android.systemui.statusbar.phone.fakeManagedProfileController
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.data.repository.fakeUserRepository
-import com.android.systemui.user.domain.interactor.userLockedInteractor
 import com.android.systemui.util.mockito.mock
 
 val Kosmos.communalInteractor by Fixture {
@@ -70,10 +63,6 @@
         logBuffer = logcatLogBuffer("CommunalInteractor"),
         tableLogBuffer = mock(),
         managedProfileController = fakeManagedProfileController,
-        batteryInteractor = batteryInteractor,
-        dockManager = dockManager,
-        posturingInteractor = posturingInteractor,
-        userLockedInteractor = userLockedInteractor,
     )
 }
 
@@ -86,28 +75,28 @@
     )
 }
 
-suspend fun Kosmos.setCommunalEnabled(enabled: Boolean): UserInfo {
+fun Kosmos.setCommunalEnabled(enabled: Boolean) {
     fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, enabled)
-    return if (enabled) {
-        fakeUserRepository.asMainUser()
-    } else {
-        fakeUserRepository.asDefaultUser()
-    }
+    val suppressionReasons =
+        if (enabled) {
+            emptyList()
+        } else {
+            listOf(SuppressionReason.ReasonUnknown())
+        }
+    communalSettingsInteractor.setSuppressionReasons(suppressionReasons)
 }
 
-suspend fun Kosmos.setCommunalV2Enabled(enabled: Boolean): UserInfo {
+fun Kosmos.setCommunalV2Enabled(enabled: Boolean) {
     setCommunalV2ConfigEnabled(enabled)
     return setCommunalEnabled(enabled)
 }
 
-suspend fun Kosmos.setCommunalAvailable(available: Boolean): UserInfo {
-    val user = setCommunalEnabled(available)
+fun Kosmos.setCommunalAvailable(available: Boolean) {
+    setCommunalEnabled(available)
     fakeKeyguardRepository.setKeyguardShowing(available)
-    fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, available)
-    return user
 }
 
-suspend fun Kosmos.setCommunalV2Available(available: Boolean): UserInfo {
+fun Kosmos.setCommunalV2Available(available: Boolean) {
     setCommunalV2ConfigEnabled(available)
-    return setCommunalAvailable(available)
+    setCommunalAvailable(available)
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt
index fb983f7..d2fbb51 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt
@@ -24,7 +24,6 @@
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.settings.userTracker
 import com.android.systemui.user.domain.interactor.selectedUserInteractor
-import com.android.systemui.util.mockito.mock
 
 val Kosmos.communalSettingsInteractor by Fixture {
     CommunalSettingsInteractor(
@@ -34,6 +33,5 @@
         repository = communalSettingsRepository,
         userInteractor = selectedUserInteractor,
         userTracker = userTracker,
-        tableLogBuffer = mock(),
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.java
deleted file mode 100644
index b99310b..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.java
+++ /dev/null
@@ -1,66 +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 com.android.systemui.dock;
-
-/**
- * A rudimentary fake for DockManager.
- */
-public class DockManagerFake implements DockManager {
-    DockEventListener mCallback;
-    AlignmentStateListener mAlignmentListener;
-    private boolean mDocked;
-
-    @Override
-    public void addListener(DockEventListener callback) {
-        this.mCallback = callback;
-    }
-
-    @Override
-    public void removeListener(DockEventListener callback) {
-        this.mCallback = null;
-    }
-
-    @Override
-    public void addAlignmentStateListener(AlignmentStateListener listener) {
-        mAlignmentListener = listener;
-    }
-
-    @Override
-    public void removeAlignmentStateListener(AlignmentStateListener listener) {
-        mAlignmentListener = listener;
-    }
-
-    @Override
-    public boolean isDocked() {
-        return mDocked;
-    }
-
-    /** Sets the docked state */
-    public void setIsDocked(boolean docked) {
-        mDocked = docked;
-    }
-
-    @Override
-    public boolean isHidden() {
-        return false;
-    }
-
-    /** Notifies callbacks of dock state change */
-    public void setDockEvent(int event) {
-        mCallback.onEvent(event);
-    }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.kt
new file mode 100644
index 0000000..6a43c40
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2025 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.dock
+
+import com.android.systemui.dock.DockManager.AlignmentStateListener
+
+/** A rudimentary fake for DockManager. */
+class DockManagerFake : DockManager {
+    private val callbacks = mutableSetOf<DockManager.DockEventListener>()
+    private val alignmentListeners = mutableSetOf<AlignmentStateListener>()
+    private var docked = false
+
+    override fun addListener(callback: DockManager.DockEventListener) {
+        callbacks.add(callback)
+    }
+
+    override fun removeListener(callback: DockManager.DockEventListener) {
+        callbacks.remove(callback)
+    }
+
+    override fun addAlignmentStateListener(listener: AlignmentStateListener) {
+        alignmentListeners.add(listener)
+    }
+
+    override fun removeAlignmentStateListener(listener: AlignmentStateListener) {
+        alignmentListeners.remove(listener)
+    }
+
+    override fun isDocked(): Boolean {
+        return docked
+    }
+
+    /** Sets the docked state */
+    fun setIsDocked(docked: Boolean) {
+        this.docked = docked
+    }
+
+    override fun isHidden(): Boolean {
+        return false
+    }
+
+    /** Notifies callbacks of dock state change */
+    fun setDockEvent(event: Int) {
+        for (callback in callbacks) {
+            callback.onEvent(event)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
index bdfa875..9b0a983 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
-import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
 import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
@@ -43,6 +42,5 @@
             wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor,
             communalSettingsInteractor = communalSettingsInteractor,
             communalSceneInteractor = communalSceneInteractor,
-            communalInteractor = communalInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
index 985044c..511bede 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
-import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
@@ -42,7 +41,6 @@
             communalSettingsInteractor = communalSettingsInteractor,
             swipeToDismissInteractor = swipeToDismissInteractor,
             keyguardOcclusionInteractor = keyguardOcclusionInteractor,
-            communalInteractor = communalInteractor,
             communalSceneInteractor = communalSceneInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
index 255a780..1130592 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.content.applicationContext
+import android.os.powerManager
 import android.view.accessibility.accessibilityManagerWrapper
 import com.android.internal.logging.uiEventLogger
 import com.android.systemui.broadcast.broadcastDispatcher
@@ -26,6 +27,8 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.shade.pulsingGestureListener
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
+import com.android.systemui.util.time.fakeSystemClock
 
 val Kosmos.keyguardTouchHandlingInteractor by
     Kosmos.Fixture {
@@ -40,5 +43,8 @@
             accessibilityManager = accessibilityManagerWrapper,
             pulsingGestureListener = pulsingGestureListener,
             faceAuthInteractor = deviceEntryFaceAuthInteractor,
+            secureSettingsRepository = userAwareSecureSettingsRepository,
+            powerManager = powerManager,
+            systemClock = fakeSystemClock,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt
index 4f8d5a1..9457de1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt
@@ -18,18 +18,14 @@
 
 import com.android.systemui.plugins.qs.TileDetailsViewModel
 
-class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel() {
+class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel {
     private var _clickOnSettingsButton = 0
 
     override fun clickOnSettingsButton() {
         _clickOnSettingsButton++
     }
 
-    override fun getTitle(): String {
-        return tileSpec ?: " Fake title"
-    }
+    override val title = tileSpec ?: " Fake title"
 
-    override fun getSubTitle(): String {
-        return tileSpec ?: "Fake sub title"
-    }
+    override val subTitle = tileSpec ?: "Fake sub title"
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt
index 75ca311..4aa4a2b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt
@@ -18,6 +18,7 @@
 
 import android.content.applicationContext
 import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.development.ui.viewmodel.buildNumberViewModelFactory
 import com.android.systemui.globalactions.globalActionsDialogLite
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.footerActionsInteractor
@@ -29,6 +30,7 @@
             override fun create(): ToolbarViewModel {
                 return ToolbarViewModel(
                     editModeButtonViewModelFactory,
+                    buildNumberViewModelFactory,
                     footerActionsInteractor,
                     { globalActionsDialogLite },
                     falsingInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt
index 933c351..6bb908a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt
@@ -22,5 +22,9 @@
 
 val Kosmos.userLockedInteractor by
     Kosmos.Fixture {
-        UserLockedInteractor(backgroundDispatcher = testDispatcher, userRepository = userRepository)
+        UserLockedInteractor(
+            backgroundDispatcher = testDispatcher,
+            userRepository = userRepository,
+            selectedUserInteractor = selectedUserInteractor,
+        )
     }
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index ccbc46f..5424ac3b 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -16,7 +16,7 @@
     srcs: [
         "texts/ravenwood-common-policies.txt",
     ],
-    visibility: ["//visibility:private"],
+    visibility: [":__subpackages__"],
 }
 
 filegroup {
@@ -44,6 +44,22 @@
 }
 
 filegroup {
+    name: "ravenwood-standard-annotations",
+    srcs: [
+        "texts/ravenwood-standard-annotations.txt",
+    ],
+    visibility: [":__subpackages__"],
+}
+
+filegroup {
+    name: "ravenizer-standard-options",
+    srcs: [
+        "texts/ravenizer-standard-options.txt",
+    ],
+    visibility: [":__subpackages__"],
+}
+
+filegroup {
     name: "ravenwood-annotation-allowed-classes",
     srcs: [
         "texts/ravenwood-annotation-allowed-classes.txt",
diff --git a/ravenwood/Framework.bp b/ravenwood/Framework.bp
index e366771..f5b075b 100644
--- a/ravenwood/Framework.bp
+++ b/ravenwood/Framework.bp
@@ -33,6 +33,7 @@
         ":ravenwood-common-policies",
         ":ravenwood-framework-policies",
         ":ravenwood-standard-options",
+        ":ravenwood-standard-annotations",
         ":ravenwood-annotation-allowed-classes",
     ],
     out: [
@@ -44,6 +45,7 @@
 
 framework_minus_apex_cmd = "$(location hoststubgen) " +
     "@$(location :ravenwood-standard-options) " +
+    "@$(location :ravenwood-standard-annotations) " +
     "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
     "--out-jar $(location ravenwood.jar) " +
     "--in-jar $(location :framework-minus-apex-for-host) " +
@@ -178,6 +180,7 @@
     tools: ["hoststubgen"],
     cmd: "$(location hoststubgen) " +
         "@$(location :ravenwood-standard-options) " +
+        "@$(location :ravenwood-standard-annotations) " +
 
         "--debug-log $(location hoststubgen_services.core.log) " +
         "--stats-file $(location hoststubgen_services.core_stats.csv) " +
@@ -196,6 +199,7 @@
         ":ravenwood-common-policies",
         ":ravenwood-services-policies",
         ":ravenwood-standard-options",
+        ":ravenwood-standard-annotations",
         ":ravenwood-annotation-allowed-classes",
     ],
     out: [
@@ -247,6 +251,7 @@
     tools: ["hoststubgen"],
     cmd: "$(location hoststubgen) " +
         "@$(location :ravenwood-standard-options) " +
+        "@$(location :ravenwood-standard-annotations) " +
 
         "--debug-log $(location hoststubgen_core-icu4j-for-host.log) " +
         "--stats-file $(location hoststubgen_core-icu4j-for-host_stats.csv) " +
@@ -265,6 +270,7 @@
         ":ravenwood-common-policies",
         ":icu-ravenwood-policies",
         ":ravenwood-standard-options",
+        ":ravenwood-standard-annotations",
     ],
     out: [
         "ravenwood.jar",
@@ -301,6 +307,7 @@
     tools: ["hoststubgen"],
     cmd: "$(location hoststubgen) " +
         "@$(location :ravenwood-standard-options) " +
+        "@$(location :ravenwood-standard-annotations) " +
 
         "--debug-log $(location framework-configinfrastructure.log) " +
         "--stats-file $(location framework-configinfrastructure_stats.csv) " +
@@ -319,6 +326,7 @@
         ":ravenwood-common-policies",
         ":framework-configinfrastructure-ravenwood-policies",
         ":ravenwood-standard-options",
+        ":ravenwood-standard-annotations",
     ],
     out: [
         "ravenwood.jar",
@@ -355,6 +363,7 @@
     tools: ["hoststubgen"],
     cmd: "$(location hoststubgen) " +
         "@$(location :ravenwood-standard-options) " +
+        "@$(location :ravenwood-standard-annotations) " +
 
         "--debug-log $(location framework-statsd.log) " +
         "--stats-file $(location framework-statsd_stats.csv) " +
@@ -373,6 +382,7 @@
         ":ravenwood-common-policies",
         ":framework-statsd-ravenwood-policies",
         ":ravenwood-standard-options",
+        ":ravenwood-standard-annotations",
     ],
     out: [
         "ravenwood.jar",
@@ -409,6 +419,7 @@
     tools: ["hoststubgen"],
     cmd: "$(location hoststubgen) " +
         "@$(location :ravenwood-standard-options) " +
+        "@$(location :ravenwood-standard-annotations) " +
 
         "--debug-log $(location framework-graphics.log) " +
         "--stats-file $(location framework-graphics_stats.csv) " +
@@ -427,6 +438,7 @@
         ":ravenwood-common-policies",
         ":framework-graphics-ravenwood-policies",
         ":ravenwood-standard-options",
+        ":ravenwood-standard-annotations",
     ],
     out: [
         "ravenwood.jar",
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index df63cb9..1148539 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -150,6 +150,10 @@
       "host": true
     },
     {
+      "name": "RavenwoodCoreTest-light",
+      "host": true
+    },
+    {
       "name": "RavenwoodMinimumTest",
       "host": true
     },
@@ -168,6 +172,10 @@
     {
       "name": "RavenwoodServicesTest",
       "host": true
+    },
+    {
+      "name": "UinputTestsRavenwood",
+      "host": true
     }
     // AUTO-GENERATED-END
   ],
diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java
new file mode 100644
index 0000000..cdfd4a8
--- /dev/null
+++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import org.junit.Test;
+
+import java.io.FileDescriptor;
+import java.util.LinkedHashMap;
+import java.util.regex.Pattern;
+
+public class RavenwoodJdkPatchTest {
+
+    @Test
+    public void testUnicodeRegex() {
+        var pattern = Pattern.compile("\\w+");
+        assertTrue(pattern.matcher("über").matches());
+    }
+
+    @Test
+    public void testLinkedHashMapEldest() {
+        var map = new LinkedHashMap<String, String>();
+        map.put("a", "b");
+        map.put("x", "y");
+        assertEquals(map.entrySet().iterator().next(), map.eldest());
+    }
+
+    @Test
+    public void testFileDescriptorGetSetInt() throws ErrnoException {
+        FileDescriptor fd = Os.open("/dev/zero", OsConstants.O_RDONLY, 0);
+        try {
+            int fdRaw = fd.getInt$();
+            assertNotEquals(-1, fdRaw);
+            fd.setInt$(-1);
+            assertEquals(-1, fd.getInt$());
+            fd.setInt$(fdRaw);
+            Os.close(fd);
+            assertEquals(-1, fd.getInt$());
+        } finally {
+            Os.close(fd);
+        }
+    }
+}
diff --git a/ravenwood/texts/ravenizer-standard-options.txt b/ravenwood/texts/ravenizer-standard-options.txt
new file mode 100644
index 0000000..cef736f
--- /dev/null
+++ b/ravenwood/texts/ravenizer-standard-options.txt
@@ -0,0 +1,13 @@
+# File containing standard options to Ravenizer for Ravenwood
+
+# Keep all classes / methods / fields in tests and its target
+--default-keep
+
+--delete-finals
+
+# Include standard annotations
+@jar:texts/ravenwood-standard-annotations.txt
+
+# Apply common policies
+--policy-override-file
+    jar:texts/ravenwood-common-policies.txt
diff --git a/ravenwood/texts/ravenwood-standard-annotations.txt b/ravenwood/texts/ravenwood-standard-annotations.txt
new file mode 100644
index 0000000..75ec5ca
--- /dev/null
+++ b/ravenwood/texts/ravenwood-standard-annotations.txt
@@ -0,0 +1,37 @@
+# Standard annotations.
+# Note, each line is a single argument, so we need newlines after each `--xxx-annotation`.
+--keep-annotation
+    android.ravenwood.annotation.RavenwoodKeep
+
+--keep-annotation
+    android.ravenwood.annotation.RavenwoodKeepPartialClass
+
+--keep-class-annotation
+    android.ravenwood.annotation.RavenwoodKeepWholeClass
+
+--throw-annotation
+    android.ravenwood.annotation.RavenwoodThrow
+
+--remove-annotation
+    android.ravenwood.annotation.RavenwoodRemove
+
+--ignore-annotation
+    android.ravenwood.annotation.RavenwoodIgnore
+
+--partially-allowed-annotation
+    android.ravenwood.annotation.RavenwoodPartiallyAllowlisted
+
+--substitute-annotation
+    android.ravenwood.annotation.RavenwoodReplace
+
+--redirect-annotation
+    android.ravenwood.annotation.RavenwoodRedirect
+
+--redirection-class-annotation
+    android.ravenwood.annotation.RavenwoodRedirectionClass
+
+--class-load-hook-annotation
+    android.ravenwood.annotation.RavenwoodClassLoadHook
+
+--keep-static-initializer-annotation
+    android.ravenwood.annotation.RavenwoodKeepStaticInitializer
diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt
index 2336575..0a65025 100644
--- a/ravenwood/texts/ravenwood-standard-options.txt
+++ b/ravenwood/texts/ravenwood-standard-options.txt
@@ -15,41 +15,3 @@
 
 #--default-class-load-hook
 #    com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-
-# Standard annotations.
-# Note, each line is a single argument, so we need newlines after each `--xxx-annotation`.
---keep-annotation
-    android.ravenwood.annotation.RavenwoodKeep
-
---keep-annotation
-    android.ravenwood.annotation.RavenwoodKeepPartialClass
-
---keep-class-annotation
-    android.ravenwood.annotation.RavenwoodKeepWholeClass
-
---throw-annotation
-    android.ravenwood.annotation.RavenwoodThrow
-
---remove-annotation
-    android.ravenwood.annotation.RavenwoodRemove
-
---ignore-annotation
-    android.ravenwood.annotation.RavenwoodIgnore
-
---partially-allowed-annotation
-    android.ravenwood.annotation.RavenwoodPartiallyAllowlisted
-
---substitute-annotation
-    android.ravenwood.annotation.RavenwoodReplace
-
---redirect-annotation
-    android.ravenwood.annotation.RavenwoodRedirect
-
---redirection-class-annotation
-    android.ravenwood.annotation.RavenwoodRedirectionClass
-
---class-load-hook-annotation
-    android.ravenwood.annotation.RavenwoodClassLoadHook
-
---keep-static-initializer-annotation
-    android.ravenwood.annotation.RavenwoodKeepStaticInitializer
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt
index f59e143..ae0a008 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt
@@ -15,8 +15,6 @@
  */
 package com.android.hoststubgen
 
-import java.io.File
-
 /**
  * We will not print the stack trace for exceptions implementing it.
  */
@@ -64,9 +62,6 @@
 class InputFileNotFoundException(filename: String) :
     ArgumentsException("File '$filename' not found")
 
-fun String.ensureFileExists(): String {
-    if (!File(this).exists()) {
-        throw InputFileNotFoundException(this)
-    }
-    return this
-}
+/** Thrown when a JAR resource does not exist. */
+class JarResourceNotFoundException(path: String) :
+    ArgumentsException("JAR resource '$path' not found")
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt
index 4fe21ea..98f96a8 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt
@@ -16,6 +16,7 @@
 package com.android.hoststubgen
 
 import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.findAnyAnnotation
 import com.android.hoststubgen.filters.AnnotationBasedFilter
 import com.android.hoststubgen.filters.ClassWidePolicyPropagatingFilter
 import com.android.hoststubgen.filters.ConstantFilter
@@ -26,21 +27,25 @@
 import com.android.hoststubgen.filters.OutputFilter
 import com.android.hoststubgen.filters.SanitizationFilter
 import com.android.hoststubgen.filters.TextFileFilterPolicyBuilder
+import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
 import com.android.hoststubgen.utils.ClassPredicate
 import com.android.hoststubgen.visitors.BaseAdapter
+import com.android.hoststubgen.visitors.ImplGeneratingAdapter
 import com.android.hoststubgen.visitors.PackageRedirectRemapper
+import java.io.PrintWriter
 import org.objectweb.asm.ClassReader
 import org.objectweb.asm.ClassVisitor
 import org.objectweb.asm.ClassWriter
 import org.objectweb.asm.commons.ClassRemapper
 import org.objectweb.asm.util.CheckClassAdapter
+import org.objectweb.asm.util.TraceClassVisitor
 
 /**
  * This class implements bytecode transformation of HostStubGen.
  */
 class HostStubGenClassProcessor(
     private val options: HostStubGenClassProcessorOptions,
-    private val allClasses: ClassNodes,
+    val allClasses: ClassNodes,
     private val errors: HostStubGenErrors = HostStubGenErrors(),
     private val stats: HostStubGenStats? = null,
 ) {
@@ -48,6 +53,7 @@
     val remapper = FilterRemapper(filter)
 
     private val packageRedirector = PackageRedirectRemapper(options.packageRedirects)
+    private val processedAnnotation = setOf(HostStubGenProcessedAsKeep.CLASS_DESCRIPTOR)
 
     /**
      * Build the filter, which decides what classes/methods/fields should be put in stub or impl
@@ -130,15 +136,10 @@
         return filter
     }
 
-    fun processClassBytecode(bytecode: ByteArray): ByteArray {
-        val cr = ClassReader(bytecode)
+    private fun buildVisitor(base: ClassVisitor, className: String): ClassVisitor {
+        // Connect to the base visitor
+        var outVisitor: ClassVisitor = base
 
-        // COMPUTE_FRAMES wouldn't be happy if code uses
-        val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES
-        val cw = ClassWriter(flags)
-
-        // Connect to the class writer
-        var outVisitor: ClassVisitor = cw
         if (options.enableClassChecker.get) {
             outVisitor = CheckClassAdapter(outVisitor)
         }
@@ -149,15 +150,59 @@
         val visitorOptions = BaseAdapter.Options(
             errors = errors,
             stats = stats,
-            enablePreTrace = options.enablePreTrace.get,
-            enablePostTrace = options.enablePostTrace.get,
             deleteClassFinals = options.deleteFinals.get,
             deleteMethodFinals = options.deleteFinals.get,
         )
-        outVisitor = BaseAdapter.getVisitor(
-            cr.className, allClasses, outVisitor, filter,
-            packageRedirector, visitorOptions
-        )
+
+        val verbosePrinter = PrintWriter(log.getWriter(LogLevel.Verbose))
+
+        // Inject TraceClassVisitor for debugging.
+        if (options.enablePostTrace.get) {
+            outVisitor = TraceClassVisitor(outVisitor, verbosePrinter)
+        }
+
+        // Handle --package-redirect
+        if (!packageRedirector.isEmpty) {
+            // Don't apply the remapper on redirect-from classes.
+            // Otherwise, if the target jar actually contains the "from" classes (which
+            // may or may not be the case) they'd be renamed.
+            // But we update all references in other places, so, a method call to a "from" class
+            // would be replaced with the "to" class. All type references (e.g. variable types)
+            // will be updated too.
+            if (!packageRedirector.isTarget(className)) {
+                outVisitor = ClassRemapper(outVisitor, packageRedirector)
+            } else {
+                log.v(
+                    "Class $className is a redirect-from class, not applying" +
+                            " --package-redirect"
+                )
+            }
+        }
+
+        outVisitor = ImplGeneratingAdapter(allClasses, outVisitor, filter, visitorOptions)
+
+        // Inject TraceClassVisitor for debugging.
+        if (options.enablePreTrace.get) {
+            outVisitor = TraceClassVisitor(outVisitor, verbosePrinter)
+        }
+
+        return outVisitor
+    }
+
+    fun processClassBytecode(bytecode: ByteArray): ByteArray {
+        val cr = ClassReader(bytecode)
+
+        // If the class was already processed previously, skip
+        val clz = allClasses.getClass(cr.className)
+        if (clz.findAnyAnnotation(processedAnnotation) != null) {
+            return bytecode
+        }
+
+        // COMPUTE_FRAMES wouldn't be happy if code uses
+        val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES
+        val cw = ClassWriter(flags)
+
+        val outVisitor = buildVisitor(cw, cr.className)
 
         cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
         return cw.toByteArray()
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt
index c7c45e6..e7166f1 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt
@@ -18,6 +18,7 @@
 import com.android.hoststubgen.filters.FilterPolicy
 import com.android.hoststubgen.utils.ArgIterator
 import com.android.hoststubgen.utils.BaseOptions
+import com.android.hoststubgen.utils.FileOrResource
 import com.android.hoststubgen.utils.SetOnce
 
 private fun parsePackageRedirect(fromColonTo: String): Pair<String, String> {
@@ -53,7 +54,7 @@
     var defaultClassLoadHook: SetOnce<String?> = SetOnce(null),
     var defaultMethodCallHook: SetOnce<String?> = SetOnce(null),
 
-    var policyOverrideFiles: MutableList<String> = mutableListOf(),
+    var policyOverrideFiles: MutableList<FileOrResource> = mutableListOf(),
 
     var defaultPolicy: SetOnce<FilterPolicy> = SetOnce(FilterPolicy.Remove),
 
@@ -73,15 +74,14 @@
         return name
     }
 
-    override fun parseOption(option: String, ai: ArgIterator): Boolean {
+    override fun parseOption(option: String, args: ArgIterator): Boolean {
         // Define some shorthands...
-        fun nextArg(): String = ai.nextArgRequired(option)
+        fun nextArg(): String = args.nextArgRequired(option)
         fun MutableSet<String>.addUniqueAnnotationArg(): String =
             nextArg().also { this += ensureUniqueAnnotation(it) }
 
         when (option) {
-            "--policy-override-file" ->
-                policyOverrideFiles.add(nextArg().ensureFileExists())
+            "--policy-override-file" -> policyOverrideFiles.add(FileOrResource(nextArg()))
 
             "--default-remove" -> defaultPolicy.set(FilterPolicy.Remove)
             "--default-throw" -> defaultPolicy.set(FilterPolicy.Throw)
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
index b41ce0f..112ef01e 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
@@ -217,7 +217,7 @@
 
 fun isAnonymousInnerClass(cn: ClassNode): Boolean {
     // TODO: Is there a better way?
-    return cn.name.matches(numericalInnerClassName)
+    return cn.outerClass != null && cn.name.matches(numericalInnerClassName)
 }
 
 /**
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt
index b8b0d8a..7f36aca 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt
@@ -19,9 +19,9 @@
  * Base class for an [OutputFilter] that uses another filter as a fallback.
  */
 abstract class DelegatingFilter(
-        // fallback shouldn't be used by subclasses directly, so make it private.
-        // They should instead be calling into `super` or `outermostFilter`.
-        private val fallback: OutputFilter
+    // fallback shouldn't be used by subclasses directly, so make it private.
+    // They should instead be calling into `super` or `outermostFilter`.
+    private val fallback: OutputFilter
 ) : OutputFilter() {
     init {
         fallback.outermostFilter = this
@@ -50,24 +50,24 @@
     }
 
     override fun getPolicyForField(
-            className: String,
-            fieldName: String
+        className: String,
+        fieldName: String
     ): FilterPolicyWithReason {
         return fallback.getPolicyForField(className, fieldName)
     }
 
     override fun getPolicyForMethod(
-            className: String,
-            methodName: String,
-            descriptor: String
+        className: String,
+        methodName: String,
+        descriptor: String
     ): FilterPolicyWithReason {
         return fallback.getPolicyForMethod(className, methodName, descriptor)
     }
 
     override fun getRenameTo(
-            className: String,
-            methodName: String,
-            descriptor: String
+        className: String,
+        methodName: String,
+        descriptor: String
     ): String? {
         return fallback.getRenameTo(className, methodName, descriptor)
     }
@@ -97,13 +97,12 @@
     }
 
     override fun getMethodCallReplaceTo(
-        callerClassName: String,
-        callerMethodName: String,
         className: String,
         methodName: String,
         descriptor: String,
     ): MethodReplaceTarget? {
         return fallback.getMethodCallReplaceTo(
-            callerClassName, callerMethodName, className, methodName, descriptor)
+            className, methodName, descriptor
+        )
     }
 }
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
index 474da6d..d44d016 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
@@ -16,7 +16,6 @@
 package com.android.hoststubgen.filters
 
 import com.android.hoststubgen.HostStubGenErrors
-import com.android.hoststubgen.HostStubGenInternalException
 import com.android.hoststubgen.asm.CLASS_INITIALIZER_DESC
 import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME
 import com.android.hoststubgen.asm.ClassNodes
@@ -37,19 +36,15 @@
  * TODO: Do we need a way to make anonymous class methods and lambdas "throw"?
  */
 class ImplicitOutputFilter(
-        private val errors: HostStubGenErrors,
-        private val classes: ClassNodes,
-        fallback: OutputFilter
+    private val errors: HostStubGenErrors,
+    private val classes: ClassNodes,
+    fallback: OutputFilter
 ) : DelegatingFilter(fallback) {
-    private fun getClassImplicitPolicy(className: String, cn: ClassNode): FilterPolicyWithReason? {
+    private fun getClassImplicitPolicy(cn: ClassNode): FilterPolicyWithReason? {
         if (isAnonymousInnerClass(cn)) {
             log.forDebug {
 //                log.d("  anon-inner class: ${className} outer: ${cn.outerClass}  ")
             }
-            if (cn.outerClass == null) {
-                throw HostStubGenInternalException(
-                        "outerClass is null for anonymous inner class")
-            }
             // If the outer class needs to be in impl, it should be in impl too.
             val outerPolicy = outermostFilter.getPolicyForClass(cn.outerClass)
             if (outerPolicy.policy.needsInOutput) {
@@ -65,15 +60,15 @@
         val cn = classes.getClass(className)
 
         // Use the implicit policy, if any.
-        getClassImplicitPolicy(className, cn)?.let { return it }
+        getClassImplicitPolicy(cn)?.let { return it }
 
         return fallback
     }
 
     override fun getPolicyForMethod(
-            className: String,
-            methodName: String,
-            descriptor: String
+        className: String,
+        methodName: String,
+        descriptor: String
     ): FilterPolicyWithReason {
         val fallback = super.getPolicyForMethod(className, methodName, descriptor)
         val classPolicy = outermostFilter.getPolicyForClass(className)
@@ -84,12 +79,14 @@
         // "keep" instead.
         // Unless it's an enum -- in that case, the below code would handle it.
         if (!cn.isEnum() &&
-                fallback.policy == FilterPolicy.Throw &&
-                methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC) {
+            fallback.policy == FilterPolicy.Throw &&
+            methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC
+        ) {
             // TODO Maybe show a warning?? But that'd be too noisy with --default-throw.
             return FilterPolicy.Ignore.withReason(
                 "'throw' on static initializer is handled as 'ignore'" +
-                        " [original throw reason: ${fallback.reason}]")
+                        " [original throw reason: ${fallback.reason}]"
+            )
         }
 
         log.d("Class ${cn.name} Class policy: $classPolicy")
@@ -120,7 +117,8 @@
                     // For synthetic methods (such as lambdas), let's just inherit the class's
                     // policy.
                     return memberPolicy.withReason(classPolicy.reason).wrapReason(
-                            "is-synthetic-method")
+                        "is-synthetic-method"
+                    )
                 }
             }
         }
@@ -129,8 +127,8 @@
     }
 
     override fun getPolicyForField(
-            className: String,
-            fieldName: String
+        className: String,
+        fieldName: String
     ): FilterPolicyWithReason {
         val fallback = super.getPolicyForField(className, fieldName)
 
@@ -161,4 +159,4 @@
 
         return fallback
     }
-}
\ No newline at end of file
+}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
index fc885d6..59da3da 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
@@ -28,10 +28,12 @@
     private val classes: ClassNodes,
     fallback: OutputFilter,
 ) : DelegatingFilter(fallback) {
-    private val mPolicies: MutableMap<String, FilterPolicyWithReason> = mutableMapOf()
-    private val mRenames: MutableMap<String, String> = mutableMapOf()
-    private val mRedirectionClasses: MutableMap<String, String> = mutableMapOf()
-    private val mClassLoadHooks: MutableMap<String, String> = mutableMapOf()
+    private val mPolicies = mutableMapOf<String, FilterPolicyWithReason>()
+    private val mRenames = mutableMapOf<String, String>()
+    private val mRedirectionClasses = mutableMapOf<String, String>()
+    private val mClassLoadHooks = mutableMapOf<String, String>()
+    private val mMethodCallReplaceSpecs = mutableListOf<MethodCallReplaceSpec>()
+    private val mTypeRenameSpecs = mutableListOf<TypeRenameSpec>()
 
     private fun getClassKey(className: String): String {
         return className.toHumanReadableClassName()
@@ -45,10 +47,6 @@
         return getClassKey(className) + "." + methodName + ";" + signature
     }
 
-    override fun getPolicyForClass(className: String): FilterPolicyWithReason {
-        return mPolicies[getClassKey(className)] ?: super.getPolicyForClass(className)
-    }
-
     private fun checkClass(className: String) {
         if (classes.findClass(className) == null) {
             log.w("Unknown class $className")
@@ -74,6 +72,10 @@
         }
     }
 
+    override fun getPolicyForClass(className: String): FilterPolicyWithReason {
+        return mPolicies[getClassKey(className)] ?: super.getPolicyForClass(className)
+    }
+
     fun setPolicyForClass(className: String, policy: FilterPolicyWithReason) {
         checkClass(className)
         mPolicies[getClassKey(className)] = policy
@@ -81,7 +83,7 @@
 
     override fun getPolicyForField(className: String, fieldName: String): FilterPolicyWithReason {
         return mPolicies[getFieldKey(className, fieldName)]
-                ?: super.getPolicyForField(className, fieldName)
+            ?: super.getPolicyForField(className, fieldName)
     }
 
     fun setPolicyForField(className: String, fieldName: String, policy: FilterPolicyWithReason) {
@@ -90,21 +92,21 @@
     }
 
     override fun getPolicyForMethod(
-            className: String,
-            methodName: String,
-            descriptor: String,
-            ): FilterPolicyWithReason {
+        className: String,
+        methodName: String,
+        descriptor: String,
+    ): FilterPolicyWithReason {
         return mPolicies[getMethodKey(className, methodName, descriptor)]
             ?: mPolicies[getMethodKey(className, methodName, "*")]
             ?: super.getPolicyForMethod(className, methodName, descriptor)
     }
 
     fun setPolicyForMethod(
-            className: String,
-            methodName: String,
-            descriptor: String,
-            policy: FilterPolicyWithReason,
-            ) {
+        className: String,
+        methodName: String,
+        descriptor: String,
+        policy: FilterPolicyWithReason,
+    ) {
         checkMethod(className, methodName, descriptor)
         mPolicies[getMethodKey(className, methodName, descriptor)] = policy
     }
@@ -123,7 +125,7 @@
 
     override fun getRedirectionClass(className: String): String? {
         return mRedirectionClasses[getClassKey(className)]
-                ?: super.getRedirectionClass(className)
+            ?: super.getRedirectionClass(className)
     }
 
     fun setRedirectionClass(from: String, to: String) {
@@ -135,11 +137,52 @@
     }
 
     override fun getClassLoadHooks(className: String): List<String> {
-        return addNonNullElement(super.getClassLoadHooks(className),
-            mClassLoadHooks[getClassKey(className)])
+        return addNonNullElement(
+            super.getClassLoadHooks(className),
+            mClassLoadHooks[getClassKey(className)]
+        )
     }
 
     fun setClassLoadHook(className: String, methodName: String) {
         mClassLoadHooks[getClassKey(className)] = methodName.toHumanReadableMethodName()
     }
+
+    override fun hasAnyMethodCallReplace(): Boolean {
+        return mMethodCallReplaceSpecs.isNotEmpty() || super.hasAnyMethodCallReplace()
+    }
+
+    override fun getMethodCallReplaceTo(
+        className: String,
+        methodName: String,
+        descriptor: String,
+    ): MethodReplaceTarget? {
+        // Maybe use 'Tri' if we end up having too many replacements.
+        mMethodCallReplaceSpecs.forEach {
+            if (className == it.fromClass &&
+                methodName == it.fromMethod
+            ) {
+                if (it.fromDescriptor == "*" || descriptor == it.fromDescriptor) {
+                    return MethodReplaceTarget(it.toClass, it.toMethod)
+                }
+            }
+        }
+        return super.getMethodCallReplaceTo(className, methodName, descriptor)
+    }
+
+    fun setMethodCallReplaceSpec(spec: MethodCallReplaceSpec) {
+        mMethodCallReplaceSpecs.add(spec)
+    }
+
+    override fun remapType(className: String): String? {
+        mTypeRenameSpecs.forEach {
+            if (it.typeInternalNamePattern.matcher(className).matches()) {
+                return it.typeInternalNamePrefix + className
+            }
+        }
+        return super.remapType(className)
+    }
+
+    fun setRemapTypeSpec(spec: TypeRenameSpec) {
+        mTypeRenameSpecs.add(spec)
+    }
 }
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt
index f99ce90..c47bb30 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt
@@ -41,10 +41,10 @@
     abstract fun getPolicyForField(className: String, fieldName: String): FilterPolicyWithReason
 
     abstract fun getPolicyForMethod(
-            className: String,
-            methodName: String,
-            descriptor: String,
-            ): FilterPolicyWithReason
+        className: String,
+        methodName: String,
+        descriptor: String,
+    ): FilterPolicyWithReason
 
     /**
      * If a given method is a substitute-from method, return the substitute-to method name.
@@ -108,8 +108,6 @@
      * If a method call should be forwarded to another method, return the target's class / method.
      */
     open fun getMethodCallReplaceTo(
-        callerClassName: String,
-        callerMethodName: String,
         className: String,
         methodName: String,
         descriptor: String,
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index dd353e9..97fc353 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -22,13 +22,13 @@
 import com.android.hoststubgen.asm.toJvmClassName
 import com.android.hoststubgen.log
 import com.android.hoststubgen.normalizeTextLine
+import com.android.hoststubgen.utils.FileOrResource
 import com.android.hoststubgen.whitespaceRegex
-import org.objectweb.asm.tree.ClassNode
 import java.io.BufferedReader
-import java.io.FileReader
 import java.io.PrintWriter
 import java.io.Reader
 import java.util.regex.Pattern
+import org.objectweb.asm.tree.ClassNode
 
 /**
  * Print a class node as a "keep" policy.
@@ -58,6 +58,23 @@
     RFile,
 }
 
+data class MethodCallReplaceSpec(
+    val fromClass: String,
+    val fromMethod: String,
+    val fromDescriptor: String,
+    val toClass: String,
+    val toMethod: String,
+)
+
+/**
+ * When a package name matches [typeInternalNamePattern], we prepend [typeInternalNamePrefix]
+ * to it.
+ */
+data class TypeRenameSpec(
+    val typeInternalNamePattern: Pattern,
+    val typeInternalNamePrefix: String,
+)
+
 /**
  * This receives [TextFileFilterPolicyBuilder] parsing result.
  */
@@ -99,7 +116,7 @@
         className: String,
         methodName: String,
         methodDesc: String,
-        replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+        replaceSpec: MethodCallReplaceSpec,
     )
 }
 
@@ -116,9 +133,6 @@
     private var featureFlagsPolicy: FilterPolicyWithReason? = null
     private var syspropsPolicy: FilterPolicyWithReason? = null
     private var rFilePolicy: FilterPolicyWithReason? = null
-    private val typeRenameSpec = mutableListOf<TextFilePolicyRemapperFilter.TypeRenameSpec>()
-    private val methodReplaceSpec =
-        mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>()
 
     /**
      * Fields for a filter chain used for "partial allowlisting", which are used by
@@ -126,47 +140,34 @@
      */
     private val annotationAllowedInMemoryFilter: InMemoryOutputFilter
     val annotationAllowedMembersFilter: OutputFilter
+        get() = annotationAllowedInMemoryFilter
 
     private val annotationAllowedPolicy = FilterPolicy.AnnotationAllowed.withReason(FILTER_REASON)
 
     init {
         // Create a filter that checks "partial allowlisting".
-        var aaf: OutputFilter = ConstantFilter(FilterPolicy.Remove, "default disallowed")
-
-        aaf = InMemoryOutputFilter(classes, aaf)
-        annotationAllowedInMemoryFilter = aaf
-
-        annotationAllowedMembersFilter = annotationAllowedInMemoryFilter
+        val filter = ConstantFilter(FilterPolicy.Remove, "default disallowed")
+        annotationAllowedInMemoryFilter = InMemoryOutputFilter(classes, filter)
     }
 
     /**
      * Parse a given policy file. This method can be called multiple times to read from
      * multiple files. To get the resulting filter, use [createOutputFilter]
      */
-    fun parse(file: String) {
+    fun parse(file: FileOrResource) {
         // We may parse multiple files, but we reuse the same parser, because the parser
         // will make sure there'll be no dupplicating "special class" policies.
-        parser.parse(FileReader(file), file, Processor())
+        parser.parse(file.open(), file.path, Processor())
     }
 
     /**
      * Generate the resulting [OutputFilter].
      */
     fun createOutputFilter(): OutputFilter {
-        var ret: OutputFilter = imf
-        if (typeRenameSpec.isNotEmpty()) {
-            ret = TextFilePolicyRemapperFilter(typeRenameSpec, ret)
-        }
-        if (methodReplaceSpec.isNotEmpty()) {
-            ret = TextFilePolicyMethodReplaceFilter(methodReplaceSpec, classes, ret)
-        }
-
         // Wrap the in-memory-filter with AHF.
-        ret = AndroidHeuristicsFilter(
-            classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, ret
+        return AndroidHeuristicsFilter(
+            classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, imf
         )
-
-        return ret
     }
 
     private inner class Processor : PolicyFileProcessor {
@@ -180,9 +181,7 @@
         }
 
         override fun onRename(pattern: Pattern, prefix: String) {
-            typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec(
-                pattern, prefix
-            )
+            imf.setRemapTypeSpec(TypeRenameSpec(pattern, prefix))
         }
 
         override fun onClassStart(className: String) {
@@ -284,12 +283,12 @@
             className: String,
             methodName: String,
             methodDesc: String,
-            replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+            replaceSpec: MethodCallReplaceSpec,
         ) {
             // Keep the source method, because the target method may call it.
             imf.setPolicyForMethod(className, methodName, methodDesc,
                 FilterPolicy.Keep.withReason(FILTER_REASON))
-            methodReplaceSpec.add(replaceSpec)
+            imf.setMethodCallReplaceSpec(replaceSpec)
         }
     }
 }
@@ -630,13 +629,13 @@
             if (classAndMethod != null) {
                 // If the substitution target contains a ".", then
                 // it's a method call redirect.
-                val spec = TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec(
-                        currentClassName!!.toJvmClassName(),
-                        methodName,
-                        signature,
-                        classAndMethod.first.toJvmClassName(),
-                        classAndMethod.second,
-                    )
+                val spec = MethodCallReplaceSpec(
+                    className.toJvmClassName(),
+                    methodName,
+                    signature,
+                    classAndMethod.first.toJvmClassName(),
+                    classAndMethod.second,
+                )
                 processor.onMethodOutClassReplace(
                     className,
                     methodName,
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt
deleted file mode 100644
index a3f934c..0000000
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.hoststubgen.filters
-
-import com.android.hoststubgen.asm.ClassNodes
-
-/**
- * Filter used by TextFileFilterPolicyParser for "method call relacement".
- */
-class TextFilePolicyMethodReplaceFilter(
-    val spec: List<MethodCallReplaceSpec>,
-    val classes: ClassNodes,
-    val fallback: OutputFilter,
-) : DelegatingFilter(fallback) {
-
-    data class MethodCallReplaceSpec(
-        val fromClass: String,
-        val fromMethod: String,
-        val fromDescriptor: String,
-        val toClass: String,
-        val toMethod: String,
-    )
-
-    override fun hasAnyMethodCallReplace(): Boolean {
-        return true
-    }
-
-    override fun getMethodCallReplaceTo(
-        callerClassName: String,
-        callerMethodName: String,
-        className: String,
-        methodName: String,
-        descriptor: String,
-    ): MethodReplaceTarget? {
-        // Maybe use 'Tri' if we end up having too many replacements.
-        spec.forEach {
-            if (className == it.fromClass &&
-                methodName == it.fromMethod
-                ) {
-                if (it.fromDescriptor == "*" || descriptor == it.fromDescriptor) {
-                    return MethodReplaceTarget(it.toClass, it.toMethod)
-                }
-            }
-        }
-        return null
-    }
-}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt
deleted file mode 100644
index bc90d12..0000000
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.hoststubgen.filters
-
-import java.util.regex.Pattern
-
-/**
- * A filter that provides a simple "jarjar" functionality via [mapType]
- */
-class TextFilePolicyRemapperFilter(
-    val typeRenameSpecs: List<TypeRenameSpec>,
-    fallback: OutputFilter,
-) : DelegatingFilter(fallback) {
-    /**
-     * When a package name matches [typeInternalNamePattern], we prepend [typeInternalNamePrefix]
-     * to it.
-     */
-    data class TypeRenameSpec(
-        val typeInternalNamePattern: Pattern,
-        val typeInternalNamePrefix: String,
-    )
-
-    override fun remapType(className: String): String? {
-        typeRenameSpecs.forEach {
-            if (it.typeInternalNamePattern.matcher(className).matches()) {
-                return it.typeInternalNamePrefix + className
-            }
-        }
-        return null
-    }
-}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt
index 0b17879..d086992 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt
@@ -16,11 +16,16 @@
 package com.android.hoststubgen.utils
 
 import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
+import com.android.hoststubgen.InputFileNotFoundException
+import com.android.hoststubgen.JarResourceNotFoundException
 import com.android.hoststubgen.log
 import com.android.hoststubgen.normalizeTextLine
-import java.io.BufferedReader
+import java.io.File
 import java.io.FileReader
+import java.io.InputStreamReader
+import java.io.Reader
+
+const val JAR_RESOURCE_PREFIX = "jar:"
 
 /**
  * Base class for parsing arguments from commandline.
@@ -73,7 +78,7 @@
      *
      * Subclasses override/extend this method to support more options.
      */
-    abstract fun parseOption(option: String, ai: ArgIterator): Boolean
+    abstract fun parseOption(option: String, args: ArgIterator): Boolean
 
     abstract fun dumpFields(): String
 }
@@ -112,7 +117,9 @@
 
     companion object {
         fun withAtFiles(args: List<String>): ArgIterator {
-            return ArgIterator(expandAtFiles(args))
+            val expanded = mutableListOf<String>()
+            expandAtFiles(args.asSequence(), expanded)
+            return ArgIterator(expanded)
         }
 
         /**
@@ -125,34 +132,30 @@
          *
          * The file can contain '#' as comments.
          */
-        private fun expandAtFiles(args: List<String>): List<String> {
-            val ret = mutableListOf<String>()
-
+        private fun expandAtFiles(args: Sequence<String>, out: MutableList<String>) {
             args.forEach { arg ->
                 if (arg.startsWith("@@")) {
-                    ret += arg.substring(1)
+                    out.add(arg.substring(1))
                     return@forEach
                 } else if (!arg.startsWith('@')) {
-                    ret += arg
+                    out.add(arg)
                     return@forEach
                 }
+
                 // Read from the file, and add each line to the result.
-                val filename = arg.substring(1).ensureFileExists()
+                val file = FileOrResource(arg.substring(1))
 
-                log.v("Expanding options file $filename")
+                log.v("Expanding options file ${file.path}")
 
-                BufferedReader(FileReader(filename)).use { reader ->
-                    while (true) {
-                        var line = reader.readLine() ?: break // EOF
+                val fileArgs = file
+                    .open()
+                    .buffered()
+                    .lineSequence()
+                    .map(::normalizeTextLine)
+                    .filter(CharSequence::isNotEmpty)
 
-                        line = normalizeTextLine(line)
-                        if (line.isNotEmpty()) {
-                            ret += line
-                        }
-                    }
-                }
+                expandAtFiles(fileArgs, out)
             }
-            return ret
         }
     }
 }
@@ -204,3 +207,37 @@
         }
     }
 }
+
+/**
+ * A path either points to a file in filesystem, or an entry in the JAR.
+ */
+class FileOrResource(val path: String) {
+    init {
+        path.ensureFileExists()
+    }
+
+    /**
+     * Either read from filesystem, or read from JAR resources.
+     */
+    fun open(): Reader {
+        return if (path.startsWith(JAR_RESOURCE_PREFIX)) {
+            val path = path.removePrefix(JAR_RESOURCE_PREFIX)
+            InputStreamReader(this::class.java.classLoader.getResourceAsStream(path)!!)
+        } else {
+            FileReader(path)
+        }
+    }
+}
+
+fun String.ensureFileExists(): String {
+    if (this.startsWith(JAR_RESOURCE_PREFIX)) {
+        val cl = FileOrResource::class.java.classLoader
+        val path = this.removePrefix(JAR_RESOURCE_PREFIX)
+        if (cl.getResource(path) == null) {
+            throw JarResourceNotFoundException(path)
+        }
+    } else if (!File(this).exists()) {
+        throw InputFileNotFoundException(this)
+    }
+    return this
+}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt
index a08d1d6..769b769 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt
@@ -17,7 +17,6 @@
 
 import com.android.hoststubgen.HostStubGenErrors
 import com.android.hoststubgen.HostStubGenStats
-import com.android.hoststubgen.LogLevel
 import com.android.hoststubgen.asm.ClassNodes
 import com.android.hoststubgen.asm.UnifiedVisitor
 import com.android.hoststubgen.asm.getPackageNameFromFullClassName
@@ -26,13 +25,10 @@
 import com.android.hoststubgen.filters.OutputFilter
 import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
 import com.android.hoststubgen.log
-import java.io.PrintWriter
 import org.objectweb.asm.ClassVisitor
 import org.objectweb.asm.FieldVisitor
 import org.objectweb.asm.MethodVisitor
 import org.objectweb.asm.Opcodes
-import org.objectweb.asm.commons.ClassRemapper
-import org.objectweb.asm.util.TraceClassVisitor
 
 const val OPCODE_VERSION = Opcodes.ASM9
 
@@ -49,8 +45,6 @@
     data class Options(
         val errors: HostStubGenErrors,
         val stats: HostStubGenStats?,
-        val enablePreTrace: Boolean,
-        val enablePostTrace: Boolean,
         val deleteClassFinals: Boolean,
         val deleteMethodFinals: Boolean,
         // We don't remove finals from fields, because final fields have a stronger memory
@@ -253,50 +247,4 @@
         substituted: Boolean,
         superVisitor: MethodVisitor?,
     ): MethodVisitor?
-
-    companion object {
-        fun getVisitor(
-            classInternalName: String,
-            classes: ClassNodes,
-            nextVisitor: ClassVisitor,
-            filter: OutputFilter,
-            packageRedirector: PackageRedirectRemapper,
-            options: Options,
-        ): ClassVisitor {
-            var next = nextVisitor
-
-            val verbosePrinter = PrintWriter(log.getWriter(LogLevel.Verbose))
-
-            // Inject TraceClassVisitor for debugging.
-            if (options.enablePostTrace) {
-                next = TraceClassVisitor(next, verbosePrinter)
-            }
-
-            // Handle --package-redirect
-            if (!packageRedirector.isEmpty) {
-                // Don't apply the remapper on redirect-from classes.
-                // Otherwise, if the target jar actually contains the "from" classes (which
-                // may or may not be the case) they'd be renamed.
-                // But we update all references in other places, so, a method call to a "from" class
-                // would be replaced with the "to" class. All type references (e.g. variable types)
-                // will be updated too.
-                if (!packageRedirector.isTarget(classInternalName)) {
-                    next = ClassRemapper(next, packageRedirector)
-                } else {
-                    log.v(
-                        "Class $classInternalName is a redirect-from class, not applying" +
-                                " --package-redirect"
-                    )
-                }
-            }
-
-            next = ImplGeneratingAdapter(classes, next, filter, options)
-
-            // Inject TraceClassVisitor for debugging.
-            if (options.enablePreTrace) {
-                next = TraceClassVisitor(next, verbosePrinter)
-            }
-            return next
-        }
-    }
 }
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
index b8a3576..617385a 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
@@ -396,7 +396,7 @@
             }
 
             val to = filter.getMethodCallReplaceTo(
-                currentClassName, callerMethodName, owner, name, descriptor
+                owner, name, descriptor
             )
 
             if (to == null
diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index 8bb454f..d9cc54a 100644
--- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -18,6 +18,7 @@
 import com.android.hoststubgen.utils.ArgIterator
 import com.android.hoststubgen.utils.IntSetOnce
 import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
 
 /**
  * Options that can be set from command line arguments.
@@ -61,9 +62,9 @@
         }
     }
 
-    override fun parseOption(option: String, ai: ArgIterator): Boolean {
+    override fun parseOption(option: String, args: ArgIterator): Boolean {
         // Define some shorthands...
-        fun nextArg(): String = ai.nextArgRequired(option)
+        fun nextArg(): String = args.nextArgRequired(option)
 
         when (option) {
             // TODO: Write help
@@ -94,7 +95,7 @@
                 }
             }
 
-            else -> return super.parseOption(option, ai)
+            else -> return super.parseOption(option, args)
         }
 
         return true
diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt
index f7fd080..58bd9e9 100644
--- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt
+++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt
@@ -16,10 +16,10 @@
 package com.android.platform.test.ravenwood.ravenhelper.policytoannot
 
 import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
 import com.android.hoststubgen.utils.ArgIterator
 import com.android.hoststubgen.utils.BaseOptions
 import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
 
 /**
  * Options for the "ravenhelper pta" subcommand.
@@ -41,8 +41,8 @@
     var dumpOperations: SetOnce<Boolean> = SetOnce(false),
 ) : BaseOptions() {
 
-    override fun parseOption(option: String, ai: ArgIterator): Boolean {
-        fun nextArg(): String = ai.nextArgRequired(option)
+    override fun parseOption(option: String, args: ArgIterator): Boolean {
+        fun nextArg(): String = args.nextArgRequired(option)
 
         when (option) {
             // TODO: Write help
diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt
index fd6f732..5ce9a23 100644
--- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt
+++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt
@@ -19,10 +19,10 @@
 import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME
 import com.android.hoststubgen.asm.toJvmClassName
 import com.android.hoststubgen.filters.FilterPolicyWithReason
+import com.android.hoststubgen.filters.MethodCallReplaceSpec
 import com.android.hoststubgen.filters.PolicyFileProcessor
 import com.android.hoststubgen.filters.SpecialClass
 import com.android.hoststubgen.filters.TextFileFilterPolicyParser
-import com.android.hoststubgen.filters.TextFilePolicyMethodReplaceFilter
 import com.android.hoststubgen.log
 import com.android.hoststubgen.utils.ClassPredicate
 import com.android.platform.test.ravenwood.ravenhelper.SubcommandHandler
@@ -448,7 +448,7 @@
             className: String,
             methodName: String,
             methodDesc: String,
-            replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+            replaceSpec: MethodCallReplaceSpec,
         ) {
             // This can't be converted to an annotation.
             classHasMember = true
diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt
index 8b95843..6e0b7b8 100644
--- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt
+++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt
@@ -16,10 +16,10 @@
 package com.android.platform.test.ravenwood.ravenhelper.sourcemap
 
 import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
 import com.android.hoststubgen.utils.ArgIterator
 import com.android.hoststubgen.utils.BaseOptions
 import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
 
 /**
  * Options for the "ravenhelper map" subcommand.
@@ -38,8 +38,8 @@
     var text: SetOnce<String?> = SetOnce(null),
 ) : BaseOptions() {
 
-    override fun parseOption(option: String, ai: ArgIterator): Boolean {
-        fun nextArg(): String = ai.nextArgRequired(option)
+    override fun parseOption(option: String, args: ArgIterator): Boolean {
+        fun nextArg(): String = args.nextArgRequired(option)
 
         when (option) {
             // TODO: Write help
diff --git a/ravenwood/tools/ravenizer/Android.bp b/ravenwood/tools/ravenizer/Android.bp
index 957e206..93cda4e 100644
--- a/ravenwood/tools/ravenizer/Android.bp
+++ b/ravenwood/tools/ravenizer/Android.bp
@@ -15,5 +15,10 @@
         "hoststubgen-lib",
         "ravenwood-junit-for-ravenizer",
     ],
+    java_resources: [
+        ":ravenizer-standard-options",
+        ":ravenwood-standard-annotations",
+        ":ravenwood-common-policies",
+    ],
     visibility: ["//visibility:public"],
 }
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
index e67c730..04e3bda2 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
@@ -16,23 +16,22 @@
 package com.android.platform.test.ravenwood.ravenizer
 
 import com.android.hoststubgen.GeneralUserErrorException
+import com.android.hoststubgen.HostStubGenClassProcessor
 import com.android.hoststubgen.asm.ClassNodes
 import com.android.hoststubgen.asm.zipEntryNameToClassName
 import com.android.hoststubgen.executableName
 import com.android.hoststubgen.log
 import com.android.platform.test.ravenwood.ravenizer.adapter.RunnerRewritingAdapter
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.FileOutputStream
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+import java.util.zip.ZipOutputStream
 import org.objectweb.asm.ClassReader
 import org.objectweb.asm.ClassVisitor
 import org.objectweb.asm.ClassWriter
 import org.objectweb.asm.util.CheckClassAdapter
-import java.io.BufferedInputStream
-import java.io.BufferedOutputStream
-import java.io.FileOutputStream
-import java.io.InputStream
-import java.io.OutputStream
-import java.util.zip.ZipEntry
-import java.util.zip.ZipFile
-import java.util.zip.ZipOutputStream
 
 /**
  * Various stats on Ravenizer.
@@ -41,7 +40,7 @@
     /** Total end-to-end time. */
     var totalTime: Double = .0,
 
-    /** Time took to build [ClasNodes] */
+    /** Time took to build [ClassNodes] */
     var loadStructureTime: Double = .0,
 
     /** Time took to validate the classes */
@@ -50,14 +49,17 @@
     /** Total real time spent for converting the jar file */
     var totalProcessTime: Double = .0,
 
-    /** Total real time spent for converting class files (except for I/O time). */
-    var totalConversionTime: Double = .0,
+    /** Total real time spent for ravenizing class files (excluding I/O time). */
+    var totalRavenizeTime: Double = .0,
+
+    /** Total real time spent for processing class files HSG style (excluding I/O time). */
+    var totalHostStubGenTime: Double = .0,
 
     /** Total real time spent for copying class files without modification. */
     var totalCopyTime: Double = .0,
 
     /** # of entries in the input jar file */
-    var totalEntiries: Int = 0,
+    var totalEntries: Int = 0,
 
     /** # of *.class files in the input jar file */
     var totalClasses: Int = 0,
@@ -67,14 +69,15 @@
 ) {
     override fun toString(): String {
         return """
-            RavenizerStats{
+            RavenizerStats {
               totalTime=$totalTime,
               loadStructureTime=$loadStructureTime,
               validationTime=$validationTime,
               totalProcessTime=$totalProcessTime,
-              totalConversionTime=$totalConversionTime,
+              totalRavenizeTime=$totalRavenizeTime,
+              totalHostStubGenTime=$totalHostStubGenTime,
               totalCopyTime=$totalCopyTime,
-              totalEntiries=$totalEntiries,
+              totalEntries=$totalEntries,
               totalClasses=$totalClasses,
               processedClasses=$processedClasses,
             }
@@ -90,12 +93,18 @@
         val stats = RavenizerStats()
 
         stats.totalTime = log.nTime {
+            val allClasses = ClassNodes.loadClassStructures(options.inJar.get) {
+                stats.loadStructureTime = it
+            }
+            val processor = HostStubGenClassProcessor(options, allClasses)
+
             process(
                 options.inJar.get,
                 options.outJar.get,
                 options.enableValidation.get,
                 options.fatalValidation.get,
                 options.stripMockito.get,
+                processor,
                 stats,
             )
         }
@@ -108,15 +117,13 @@
         enableValidation: Boolean,
         fatalValidation: Boolean,
         stripMockito: Boolean,
+        processor: HostStubGenClassProcessor,
         stats: RavenizerStats,
     ) {
-        var allClasses = ClassNodes.loadClassStructures(inJar) {
-            time -> stats.loadStructureTime = time
-        }
         if (enableValidation) {
             stats.validationTime = log.iTime("Validating classes") {
-                if (!validateClasses(allClasses)) {
-                    var message = "Invalid test class(es) detected." +
+                if (!validateClasses(processor.allClasses)) {
+                    val message = "Invalid test class(es) detected." +
                             " See error log for details."
                     if (fatalValidation) {
                         throw RavenizerInvalidTestException(message)
@@ -126,7 +133,7 @@
                 }
             }
         }
-        if (includeUnsupportedMockito(allClasses)) {
+        if (includeUnsupportedMockito(processor.allClasses)) {
             log.w("Unsupported Mockito detected in $inJar!")
         }
 
@@ -134,7 +141,7 @@
             ZipFile(inJar).use { inZip ->
                 val inEntries = inZip.entries()
 
-                stats.totalEntiries = inZip.size()
+                stats.totalEntries = inZip.size()
 
                 ZipOutputStream(BufferedOutputStream(FileOutputStream(outJar))).use { outZip ->
                     while (inEntries.hasMoreElements()) {
@@ -159,9 +166,9 @@
                             stats.totalClasses += 1
                         }
 
-                        if (className != null && shouldProcessClass(allClasses, className)) {
-                            stats.processedClasses += 1
-                            processSingleClass(inZip, entry, outZip, allClasses, stats)
+                        if (className != null &&
+                            shouldProcessClass(processor.allClasses, className)) {
+                            processSingleClass(inZip, entry, outZip, processor, stats)
                         } else {
                             // Too slow, let's use merge_zips to bring back the original classes.
                             copyZipEntry(inZip, entry, outZip, stats)
@@ -201,14 +208,22 @@
         inZip: ZipFile,
         entry: ZipEntry,
         outZip: ZipOutputStream,
-        allClasses: ClassNodes,
+        processor: HostStubGenClassProcessor,
         stats: RavenizerStats,
     ) {
+        stats.processedClasses += 1
         val newEntry = ZipEntry(entry.name)
         outZip.putNextEntry(newEntry)
 
         BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
-            processSingleClass(entry, bis, outZip, allClasses, stats)
+            var classBytes = bis.readBytes()
+            stats.totalRavenizeTime += log.vTime("Ravenize ${entry.name}") {
+                classBytes = ravenizeSingleClass(entry, classBytes, processor.allClasses)
+            }
+            stats.totalHostStubGenTime += log.vTime("HostStubGen ${entry.name}") {
+                classBytes = processor.processClassBytecode(classBytes)
+            }
+            outZip.write(classBytes)
         }
         outZip.closeEntry()
     }
@@ -217,41 +232,34 @@
      * Whether a class needs to be processed. This must be kept in sync with [processSingleClass].
      */
     private fun shouldProcessClass(classes: ClassNodes, classInternalName: String): Boolean {
-        return !classInternalName.shouldByBypassed()
+        return !classInternalName.shouldBypass()
                 && RunnerRewritingAdapter.shouldProcess(classes, classInternalName)
     }
 
-    private fun processSingleClass(
+    private fun ravenizeSingleClass(
         entry: ZipEntry,
-        input: InputStream,
-        output: OutputStream,
+        input: ByteArray,
         allClasses: ClassNodes,
-        stats: RavenizerStats,
-    ) {
-        val cr = ClassReader(input)
+    ): ByteArray {
+        val classInternalName = zipEntryNameToClassName(entry.name)
+            ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}")
 
-        lateinit var data: ByteArray
-        stats.totalConversionTime += log.vTime("Modify ${entry.name}") {
+        val flags = ClassWriter.COMPUTE_MAXS
+        val cw = ClassWriter(flags)
+        var outVisitor: ClassVisitor = cw
 
-            val classInternalName = zipEntryNameToClassName(entry.name)
-                ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}")
-            val flags = ClassWriter.COMPUTE_MAXS
-            val cw = ClassWriter(flags)
-            var outVisitor: ClassVisitor = cw
-
-            val enableChecker = false
-            if (enableChecker) {
-                outVisitor = CheckClassAdapter(outVisitor)
-            }
-
-            // This must be kept in sync with shouldProcessClass.
-            outVisitor = RunnerRewritingAdapter.maybeApply(
-                classInternalName, allClasses, outVisitor)
-
-            cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
-
-            data = cw.toByteArray()
+        val enableChecker = false
+        if (enableChecker) {
+            outVisitor = CheckClassAdapter(outVisitor)
         }
-        output.write(data)
+
+        // This must be kept in sync with shouldProcessClass.
+        outVisitor = RunnerRewritingAdapter.maybeApply(
+            classInternalName, allClasses, outVisitor)
+
+        val cr = ClassReader(input)
+        cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
+
+        return cw.toByteArray()
     }
 }
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
index 7f4829e..8a09e6d 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
@@ -21,6 +21,7 @@
 import com.android.hoststubgen.executableName
 import com.android.hoststubgen.log
 import com.android.hoststubgen.runMainWithBoilerplate
+import com.android.hoststubgen.utils.JAR_RESOURCE_PREFIX
 import java.nio.file.Paths
 import kotlin.io.path.exists
 
@@ -36,6 +37,10 @@
  */
 private val RAVENIZER_DOTFILE = System.getenv("HOME") + "/.ravenizer-unsafe"
 
+/**
+ * This is the name of the standard option text file embedded inside ravenizer.jar.
+ */
+private const val RAVENIZER_STANDARD_OPTIONS = "texts/ravenizer-standard-options.txt"
 
 /**
  * Entry point.
@@ -45,12 +50,12 @@
     log.setConsoleLogLevel(LogLevel.Info)
 
     runMainWithBoilerplate {
-        var newArgs = args.asList()
+        val newArgs = args.toMutableList()
+        newArgs.add(0, "@$JAR_RESOURCE_PREFIX$RAVENIZER_STANDARD_OPTIONS")
+
         if (Paths.get(RAVENIZER_DOTFILE).exists()) {
             log.i("Reading options from $RAVENIZER_DOTFILE")
-            newArgs = args.toMutableList().apply {
-                add(0, "@$RAVENIZER_DOTFILE")
-            }
+            newArgs.add(0, "@$RAVENIZER_DOTFILE")
         }
 
         val options = RavenizerOptions().apply { parseArgs(newArgs) }
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
index 2c03654..5d278bb 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
@@ -16,10 +16,10 @@
 package com.android.platform.test.ravenwood.ravenizer
 
 import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
+import com.android.hoststubgen.HostStubGenClassProcessorOptions
 import com.android.hoststubgen.utils.ArgIterator
-import com.android.hoststubgen.utils.BaseOptions
 import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
 
 class RavenizerOptions(
     /** Input jar file*/
@@ -36,10 +36,10 @@
 
     /** Whether to remove mockito and dexmaker classes. */
     var stripMockito: SetOnce<Boolean> = SetOnce(false),
-) : BaseOptions() {
+) : HostStubGenClassProcessorOptions() {
 
-    override fun parseOption(option: String, ai: ArgIterator): Boolean {
-        fun nextArg(): String = ai.nextArgRequired(option)
+    override fun parseOption(option: String, args: ArgIterator): Boolean {
+        fun nextArg(): String = args.nextArgRequired(option)
 
         when (option) {
             // TODO: Write help
@@ -57,7 +57,7 @@
             "--strip-mockito" -> stripMockito.set(true)
             "--no-strip-mockito" -> stripMockito.set(false)
 
-            else -> return false
+            else -> return super.parseOption(option, args)
         }
 
         return true
@@ -79,6 +79,6 @@
             enableValidation=$enableValidation,
             fatalValidation=$fatalValidation,
             stripMockito=$stripMockito,
-        """.trimIndent()
+        """.trimIndent() + '\n' + super.dumpFields()
     }
 }
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
index 6092fcc..b394a76 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
@@ -15,8 +15,8 @@
  */
 package com.android.platform.test.ravenwood.ravenizer
 
-import android.platform.test.annotations.internal.InnerRunner
 import android.platform.test.annotations.NoRavenizer
+import android.platform.test.annotations.internal.InnerRunner
 import android.platform.test.ravenwood.RavenwoodAwareTestRunner
 import com.android.hoststubgen.asm.ClassNodes
 import com.android.hoststubgen.asm.findAnyAnnotation
@@ -85,7 +85,7 @@
 /**
  * Classes that should never be modified.
  */
-fun String.shouldByBypassed(): Boolean {
+fun String.shouldBypass(): Boolean {
     if (this.isRavenwoodClass()) {
         return true
     }
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
index 61e254b..d252b4d 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
@@ -20,8 +20,8 @@
 import com.android.hoststubgen.asm.startsWithAny
 import com.android.hoststubgen.asm.toHumanReadableClassName
 import com.android.hoststubgen.log
-import org.objectweb.asm.tree.ClassNode
 import java.util.regex.Pattern
+import org.objectweb.asm.tree.ClassNode
 
 fun validateClasses(classes: ClassNodes): Boolean {
     var allOk = true
@@ -37,7 +37,7 @@
  *
  */
 fun checkClass(cn: ClassNode, classes: ClassNodes): Boolean {
-    if (cn.name.shouldByBypassed()) {
+    if (cn.name.shouldBypass()) {
         // Class doesn't need to be checked.
         return true
     }
@@ -145,4 +145,4 @@
 
 private fun isAllowListedLegacyTest(targetClass: ClassNode): Boolean {
     return allowListedLegacyTests.contains(targetClass.name)
-}
\ No newline at end of file
+}
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index b52b3dab..35db3c6 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -260,6 +260,16 @@
 }
 
 flag {
+    name: "pointer_up_motion_event_in_touch_exploration"
+    namespace: "accessibility"
+    description: "Allows POINTER_UP motionEvents to trigger during touch exploration."
+    bug: "374930391"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "proxy_use_apps_on_virtual_device_listener"
     namespace: "accessibility"
     description: "Fixes race condition described in b/286587811"
@@ -336,3 +346,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "hearing_input_change_when_comm_device"
+    namespace: "accessibility"
+    description: "Listen to the CommunicationDeviceChanged to show hearing device input notification."
+    bug: "394070235"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index c49151d..6c26c1f 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -521,15 +521,6 @@
                         @Nullable IBinder focusedToken) {
                     return AccessibilityManagerService.this.handleKeyGestureEvent(event);
                 }
-
-                @Override
-                public boolean isKeyGestureSupported(int gestureType) {
-                    return switch (gestureType) {
-                        case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
-                             KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK -> true;
-                        default -> false;
-                    };
-                }
             };
 
     @VisibleForTesting
@@ -5619,7 +5610,7 @@
         }
 
         private boolean isValidDisplay(@Nullable Display display) {
-            if (display == null || display.getType() == Display.TYPE_OVERLAY) {
+            if (display == null) {
                 return false;
             }
             // Private virtual displays are created by the ap and is not allowed to access by other
diff --git a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
index 10dffb5..805d7f8 100644
--- a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
@@ -65,9 +65,9 @@
     private final Executor mCallbackExecutor;
 
     public HearingDevicePhoneCallNotificationController(@NonNull Context context) {
-        mTelephonyListener = new CallStateListener(context);
         mTelephonyManager = context.getSystemService(TelephonyManager.class);
         mCallbackExecutor = Executors.newSingleThreadExecutor();
+        mTelephonyListener = new CallStateListener(context, mCallbackExecutor);
     }
 
     @VisibleForTesting
@@ -109,14 +109,29 @@
                 AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_BUILTIN_MIC, "");
 
         private final Context mContext;
+        private final Executor mCommDeviceChangedExecutor;
+        private final AudioManager.OnCommunicationDeviceChangedListener mCommDeviceChangedListener;
         private NotificationManager mNotificationManager;
         private AudioManager mAudioManager;
         private BroadcastReceiver mHearingDeviceActionReceiver;
         private BluetoothDevice mHearingDevice;
+        private boolean mIsCommDeviceChangedRegistered = false;
         private boolean mIsNotificationShown = false;
 
-        CallStateListener(@NonNull Context context) {
+        CallStateListener(@NonNull Context context, @NonNull Executor executor) {
             mContext = context;
+            mCommDeviceChangedExecutor = executor;
+            mCommDeviceChangedListener = device -> {
+                if (device == null) {
+                    return;
+                }
+                mHearingDevice = getSupportedInputHearingDeviceInfo(List.of(device));
+                if (mHearingDevice != null) {
+                    showNotificationIfNeeded();
+                } else {
+                    dismissNotificationIfNeeded();
+                }
+            };
         }
 
         @Override
@@ -134,6 +149,11 @@
             }
 
             if (state == TelephonyManager.CALL_STATE_IDLE) {
+                if (mIsCommDeviceChangedRegistered) {
+                    mIsCommDeviceChangedRegistered = false;
+                    mAudioManager.removeOnCommunicationDeviceChangedListener(
+                            mCommDeviceChangedListener);
+                }
                 dismissNotificationIfNeeded();
 
                 if (mHearingDevice != null) {
@@ -143,10 +163,23 @@
                 mHearingDevice = null;
             }
             if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
-                mHearingDevice = getSupportedInputHearingDeviceInfo(
-                        mAudioManager.getAvailableCommunicationDevices());
-                if (mHearingDevice != null) {
-                    showNotificationIfNeeded();
+                if (com.android.server.accessibility.Flags.hearingInputChangeWhenCommDevice()) {
+                    AudioDeviceInfo commDevice = mAudioManager.getCommunicationDevice();
+                    mHearingDevice = getSupportedInputHearingDeviceInfo(List.of(commDevice));
+                    if (mHearingDevice != null) {
+                        showNotificationIfNeeded();
+                    } else {
+                        mAudioManager.addOnCommunicationDeviceChangedListener(
+                                mCommDeviceChangedExecutor,
+                                mCommDeviceChangedListener);
+                        mIsCommDeviceChangedRegistered = true;
+                    }
+                } else {
+                    mHearingDevice = getSupportedInputHearingDeviceInfo(
+                            mAudioManager.getAvailableCommunicationDevices());
+                    if (mHearingDevice != null) {
+                        showNotificationIfNeeded();
+                    }
                 }
             }
         }
@@ -264,6 +297,10 @@
                             PendingIntent.FLAG_IMMUTABLE);
                 }
                 case ACTION_BLUETOOTH_DEVICE_DETAILS -> {
+                    if (mHearingDevice == null) {
+                        return null;
+                    }
+
                     Bundle bundle = new Bundle();
                     bundle.putString(KEY_BLUETOOTH_ADDRESS, mHearingDevice.getAddress());
                     intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
index cc93d08..a71224a 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -124,6 +124,22 @@
                 }
             };
 
+    @VisibleForTesting
+    final AutoclickScrollPanel.ScrollPanelControllerInterface mScrollPanelController =
+            new AutoclickScrollPanel.ScrollPanelControllerInterface() {
+                @Override
+                public void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) {
+                    // TODO(b/388845721): Perform actual scroll.
+                }
+
+                @Override
+                public void exitScrollMode() {
+                    if (mAutoclickScrollPanel != null) {
+                        mAutoclickScrollPanel.hide();
+                    }
+                }
+            };
+
     public AutoclickController(Context context, int userId, AccessibilityTraceManager trace) {
         mTrace = trace;
         mContext = context;
@@ -168,7 +184,8 @@
         mWindowManager = mContext.getSystemService(WindowManager.class);
         mAutoclickTypePanel =
                 new AutoclickTypePanel(mContext, mWindowManager, mUserId, clickPanelController);
-        mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager);
+        mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager,
+                mScrollPanelController);
 
         mAutoclickTypePanel.show();
         mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams());
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
index 86f79a8..e79be50 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
@@ -18,6 +18,7 @@
 
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 
+import android.annotation.IntDef;
 import android.content.Context;
 import android.graphics.PixelFormat;
 import android.view.Gravity;
@@ -25,23 +26,97 @@
 import android.view.View;
 import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.widget.ImageButton;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.R;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 public class AutoclickScrollPanel {
+    public static final int DIRECTION_UP = 0;
+    public static final int DIRECTION_DOWN = 1;
+    public static final int DIRECTION_LEFT = 2;
+    public static final int DIRECTION_RIGHT = 3;
+
+    @IntDef({
+            DIRECTION_UP,
+            DIRECTION_DOWN,
+            DIRECTION_LEFT,
+            DIRECTION_RIGHT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ScrollDirection {}
+
     private final Context mContext;
     private final View mContentView;
     private final WindowManager mWindowManager;
+    private ScrollPanelControllerInterface mScrollPanelController;
+
+    // Scroll panel buttons.
+    private final ImageButton mUpButton;
+    private final ImageButton mDownButton;
+    private final ImageButton mLeftButton;
+    private final ImageButton mRightButton;
+    private final ImageButton mExitButton;
+
     private boolean mInScrollMode = false;
 
-    public AutoclickScrollPanel(Context context, WindowManager windowManager) {
+    /**
+     * Interface for handling scroll operations.
+     */
+    public interface ScrollPanelControllerInterface {
+        /**
+         * Called when a scroll direction is hovered.
+         *
+         * @param direction The direction to scroll: one of {@link ScrollDirection} values.
+         */
+        void handleScroll(@ScrollDirection int direction);
+
+        /**
+         * Called when the exit button is hovered.
+         */
+        void exitScrollMode();
+    }
+
+    public AutoclickScrollPanel(Context context, WindowManager windowManager,
+            ScrollPanelControllerInterface controller) {
         mContext = context;
         mWindowManager = windowManager;
+        mScrollPanelController = controller;
         mContentView = LayoutInflater.from(context).inflate(
                 R.layout.accessibility_autoclick_scroll_panel, null);
+
+        // Initialize buttons.
+        mUpButton = mContentView.findViewById(R.id.scroll_up);
+        mLeftButton = mContentView.findViewById(R.id.scroll_left);
+        mRightButton = mContentView.findViewById(R.id.scroll_right);
+        mDownButton = mContentView.findViewById(R.id.scroll_down);
+        mExitButton = mContentView.findViewById(R.id.scroll_exit);
+
+        initializeButtonState();
+    }
+
+    /**
+     * Sets up hover listeners for scroll panel buttons.
+     */
+    private void initializeButtonState() {
+        // Set up hover listeners for direction buttons.
+        setupHoverListenerForDirectionButton(mUpButton, DIRECTION_UP);
+        setupHoverListenerForDirectionButton(mLeftButton, DIRECTION_LEFT);
+        setupHoverListenerForDirectionButton(mRightButton, DIRECTION_RIGHT);
+        setupHoverListenerForDirectionButton(mDownButton, DIRECTION_DOWN);
+
+        // Set up hover listener for exit button.
+        mExitButton.setOnHoverListener((v, event) -> {
+            if (mScrollPanelController != null) {
+                mScrollPanelController.exitScrollMode();
+            }
+            return true;
+        });
     }
 
     /**
@@ -67,6 +142,19 @@
     }
 
     /**
+     * Sets up a hover listener for a direction button.
+     */
+    private void setupHoverListenerForDirectionButton(ImageButton button,
+            @ScrollDirection int direction) {
+        button.setOnHoverListener((v, event) -> {
+            if (mScrollPanelController != null) {
+                mScrollPanelController.handleScroll(direction);
+            }
+            return true;
+        });
+    }
+
+    /**
      * Retrieves the layout params for AutoclickScrollPanel, used when it's added to the Window
      * Manager.
      */
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index fb32943..b02fe27 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -653,6 +653,14 @@
             case ACTION_UP:
                 handleActionUp(event, rawEvent, policyFlags);
                 break;
+            case ACTION_POINTER_UP:
+                if (com.android.server.accessibility.Flags
+                        .pointerUpMotionEventInTouchExploration()) {
+                    if (mState.isServiceDetectingGestures()) {
+                        mAms.sendMotionEventToListeningServices(rawEvent);
+                    }
+                }
+                break;
             default:
                 break;
         }
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
index cd46b38..568abd1 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
@@ -26,6 +26,8 @@
 import android.view.MotionEvent;
 import android.view.accessibility.AccessibilityEvent;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.server.accessibility.AccessibilityManagerService;
 
 /**
@@ -73,7 +75,8 @@
     private int mState = STATE_CLEAR;
     // Helper class to track received pointers.
     // Todo: collapse or hide this class so multiple classes don't modify it.
-    private final ReceivedPointerTracker mReceivedPointerTracker;
+    @VisibleForTesting
+    public final ReceivedPointerTracker mReceivedPointerTracker;
     // The most recently received motion event.
     private MotionEvent mLastReceivedEvent;
     // The accompanying raw event without any transformations.
@@ -219,8 +222,19 @@
                 startTouchInteracting();
                 break;
             case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END:
-                setState(STATE_CLEAR);
-                // We will clear when we actually handle the next ACTION_DOWN.
+                // When interaction ends, check if there are still down pointers.
+                // If there are any down pointers, go directly to TouchExploring instead.
+                if (com.android.server.accessibility.Flags
+                        .pointerUpMotionEventInTouchExploration()) {
+                    if (mReceivedPointerTracker.mReceivedPointersDown > 0) {
+                        startTouchExploring();
+                    } else {
+                        setState(STATE_CLEAR);
+                        // We will clear when we actually handle the next ACTION_DOWN.
+                    }
+                } else {
+                    setState(STATE_CLEAR);
+                }
                 break;
             case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:
                 startTouchExploring();
@@ -419,7 +433,8 @@
         private final PointerDownInfo[] mReceivedPointers = new PointerDownInfo[MAX_POINTER_COUNT];
 
         // Which pointers are down.
-        private int mReceivedPointersDown;
+        @VisibleForTesting
+        public int mReceivedPointersDown;
 
         // The edge flags of the last received down event.
         private int mLastReceivedDownEdgeFlags;
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index 6ccf5e4..5956667 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -39,6 +39,14 @@
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__INLINE;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__MENU;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__UNKNOWN_AUTOFILL_DISPLAY_PRESENTATION_TYPE;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_DELAY_AFTER_ANIMATION_END;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_FILL_DIALOG_DISABLED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_LAST_TRIGGERED_ID_CHANGED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_SCREEN_HAS_CREDMAN_FIELD;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_AFTER_DELAY;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_SINCE_IME_ANIMATED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_UNKNOWN;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_WAIT_FOR_IME_ANIMATION;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_ACTIVITY_FINISHED;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_FILL_REQUEST_FAILED;
@@ -157,8 +165,24 @@
             DETECTION_PREFER_PCC
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface DetectionPreference {
-    }
+    public @interface DetectionPreference {}
+
+    /**
+     * The fill dialog not shown reason. These are wrappers around
+     * {@link com.android.os.AtomsProto.AutofillPresentationEventReported.FillDialogNotShownReason}.
+     */
+    @IntDef(prefix = {"FILL_DIALOG_NOT_SHOWN_REASON"}, value = {
+        FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN,
+        FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED,
+        FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD,
+        FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED,
+        FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION,
+        FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED,
+        FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END,
+        FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface FillDialogNotShownReason {}
 
     public static final int NOT_SHOWN_REASON_ANY_SHOWN =
             AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN;
@@ -219,6 +243,25 @@
     public static final int DETECTION_PREFER_PCC =
             AUTOFILL_FILL_RESPONSE_REPORTED__DETECTION_PREFERENCE__DETECTION_PREFER_PCC;
 
+    // Values for AutofillFillResponseReported.fill_dialog_not_shown_reason
+    public static final int FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_UNKNOWN;
+    public static final int FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_FILL_DIALOG_DISABLED;
+    public static final int FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_SCREEN_HAS_CREDMAN_FIELD;
+    public static final int FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_LAST_TRIGGERED_ID_CHANGED;
+    public static final int FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_WAIT_FOR_IME_ANIMATION;
+    public static final int FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_SINCE_IME_ANIMATED;
+    public static final int FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_DELAY_AFTER_ANIMATION_END;
+    public static final int FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_AFTER_DELAY;
+
+
     private static final int DEFAULT_VALUE_INT = -1;
 
     private final int mSessionId;
@@ -871,6 +914,43 @@
     }
 
     /**
+     * Set fill_dialog_not_shown_reason
+     * @param reason
+     */
+    public void maybeSetFillDialogNotShownReason(@FillDialogNotShownReason int reason) {
+        mEventInternal.ifPresent(event -> {
+            if ((event.mFillDialogNotShownReason
+                    == FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END
+                    || event.mFillDialogNotShownReason
+                    == FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION) && reason
+                    == FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED) {
+                event.mFillDialogNotShownReason = FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY;
+            } else {
+                event.mFillDialogNotShownReason = reason;
+            }
+        });
+    }
+
+    /**
+     * Set fill_dialog_ready_to_show_ms
+     * @param val
+     */
+    public void maybeSetFillDialogReadyToShowMs(long val) {
+        mEventInternal.ifPresent(event -> {
+            event.mFillDialogReadyToShowMs = (int) (val - mSessionStartTimestamp);
+        });
+    }
+
+    /**
+     * Set ime_animation_finish_ms
+     * @param val
+     */
+    public void maybeSetImeAnimationFinishMs(long val) {
+        mEventInternal.ifPresent(event -> {
+            event.mImeAnimationFinishMs = (int) (val - mSessionStartTimestamp);
+        });
+    }
+    /**
      * Set the log contains relayout metrics.
      * This is being added as a temporary measure to add logging.
      * In future, when we map Session's old view states to the new autofill id's as part of fixing
@@ -959,7 +1039,13 @@
                     + " event.notExpiringResponseDuringAuthCount="
                     + event.mFixExpireResponseDuringAuthCount
                     + " event.notifyViewEnteredIgnoredDuringAuthCount="
-                    + event.mNotifyViewEnteredIgnoredDuringAuthCount);
+                    + event.mNotifyViewEnteredIgnoredDuringAuthCount
+                    + " event.fillDialogNotShownReason="
+                    + event.mFillDialogNotShownReason
+                    + " event.fillDialogReadyToShowMs="
+                    + event.mFillDialogReadyToShowMs
+                    + " event.imeAnimationFinishMs="
+                    + event.mImeAnimationFinishMs);
         }
 
         // TODO(b/234185326): Distinguish empty responses from other no presentation reasons.
@@ -1020,7 +1106,10 @@
                 event.mViewFilledSuccessfullyOnRefillCount,
                 event.mViewFailedOnRefillCount,
                 event.mFixExpireResponseDuringAuthCount,
-                event.mNotifyViewEnteredIgnoredDuringAuthCount);
+                event.mNotifyViewEnteredIgnoredDuringAuthCount,
+                event.mFillDialogNotShownReason,
+                event.mFillDialogReadyToShowMs,
+                event.mImeAnimationFinishMs);
         mEventInternal = Optional.empty();
     }
 
@@ -1087,6 +1176,9 @@
         // Following are not logged and used only for internal logic
         boolean shouldResetShownCount = false;
         boolean mHasRelayoutLog = false;
+        @FillDialogNotShownReason int mFillDialogNotShownReason = FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN;
+        int mFillDialogReadyToShowMs = DEFAULT_VALUE_INT;
+        int mImeAnimationFinishMs = DEFAULT_VALUE_INT;
         PresentationStatsEventInternal() {}
     }
 
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 6515b23..ff3bf2a 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -81,6 +81,13 @@
 import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_RESULT_SUCCESS;
 import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_TYPE_DATASET_AUTHENTICATION;
 import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_TYPE_FULL_AUTHENTICATION;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION;
 import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_ANY_SHOWN;
 import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_NO_FOCUS;
 import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_REQUEST_FAILED;
@@ -5622,6 +5629,10 @@
                 synchronized (mLock) {
                     final ViewState currentView = mViewStates.get(mCurrentViewId);
                     currentView.setState(ViewState.STATE_FILL_DIALOG_SHOWN);
+                    // Set fill_dialog_not_shown_reason to unknown (a.k.a shown). It is needed due
+                    // to possible SHOW_FILL_DIALOG_WAIT.
+                    mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+                            FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN);
                 }
                 // Just show fill dialog once per fill request, so disabled after shown.
                 // Note: Cannot disable before requestShowFillDialog() because the method
@@ -5725,6 +5736,15 @@
 
     private boolean isFillDialogUiEnabled() {
         synchronized (mLock) {
+            if (mSessionFlags.mFillDialogDisabled) {
+                mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+                        FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED);
+            }
+            if (mSessionFlags.mScreenHasCredmanField) {
+                // Prefer to log "HAS_CREDMAN_FIELD" over "FILL_DIALOG_DISABLED".
+                mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+                        FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD);
+            }
             return !mSessionFlags.mFillDialogDisabled && !mSessionFlags.mScreenHasCredmanField;
         }
     }
@@ -5789,6 +5809,8 @@
                     || !ArrayUtils.contains(mLastFillDialogTriggerIds, filledId)) {
                 // Last fill dialog triggered ids are changed.
                 if (sDebug) Log.w(TAG, "Last fill dialog triggered ids are changed.");
+                mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+                        FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED);
                 return SHOW_FILL_DIALOG_NO;
             }
 
@@ -5815,6 +5837,8 @@
                     // we need to wait for animation to happen. We can't return from here yet.
                     // This is the situation #2 described above.
                     Log.d(TAG, "Waiting for ime animation to complete before showing fill dialog");
+                    mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+                            FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION);
                     mFillDialogRunnable = createFillDialogEvalRunnable(
                             response, filledId, filterText, flags);
                     return SHOW_FILL_DIALOG_WAIT;
@@ -5824,9 +5848,15 @@
                 // max of start input time or the ime finish time
                 long effectiveDuration = currentTimestampMs
                         - Math.max(mLastInputStartTime, mImeAnimationFinishTimeMs);
+                mPresentationStatsEventLogger.maybeSetFillDialogReadyToShowMs(
+                        currentTimestampMs);
+                mPresentationStatsEventLogger.maybeSetImeAnimationFinishMs(
+                        Math.max(mLastInputStartTime, mImeAnimationFinishTimeMs));
                 if (effectiveDuration >= mFillDialogTimeoutMs) {
                     Log.d(TAG, "Fill dialog not shown since IME has been up for more time than "
                             + mFillDialogTimeoutMs + "ms");
+                    mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+                            FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED);
                     return SHOW_FILL_DIALOG_NO;
                 } else if (effectiveDuration < mFillDialogMinWaitAfterImeAnimationMs) {
                     // we need to wait for some time after animation ends
@@ -5834,6 +5864,8 @@
                             response, filledId, filterText, flags);
                     mHandler.postDelayed(runnable,
                             mFillDialogMinWaitAfterImeAnimationMs - effectiveDuration);
+                    mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+                            FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END);
                     return SHOW_FILL_DIALOG_WAIT;
                 }
             }
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 14d9d3f..decac40 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -122,10 +122,10 @@
 }
 
 genrule {
-  name: "statslog-mediarouter-java-gen",
-  tools: ["stats-log-api-gen"],
-  cmd: "$(location stats-log-api-gen) --java $(out) --module mediarouter --javaPackage com.android.server.media --javaClass MediaRouterStatsLog",
-  out: ["com/android/server/media/MediaRouterStatsLog.java"],
+    name: "statslog-mediarouter-java-gen",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --java $(out) --module mediarouter --javaPackage com.android.server.media --javaClass MediaRouterStatsLog",
+    out: ["com/android/server/media/MediaRouterStatsLog.java"],
 }
 
 java_library_static {
@@ -138,6 +138,7 @@
         "ondeviceintelligence_conditionally",
     ],
     srcs: [
+        ":android.hardware.audio.effect-V1-java-source",
         ":android.hardware.tv.hdmi.connection-V1-java-source",
         ":android.hardware.tv.hdmi.earc-V1-java-source",
         ":android.hardware.tv.mediaquality-V1-java-source",
diff --git a/services/core/java/com/android/server/am/ConnectionRecord.java b/services/core/java/com/android/server/am/ConnectionRecord.java
index 31704c4..4e1d77c 100644
--- a/services/core/java/com/android/server/am/ConnectionRecord.java
+++ b/services/core/java/com/android/server/am/ConnectionRecord.java
@@ -142,6 +142,10 @@
                 | Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS);
     }
 
+    @Override
+    public boolean transmitsCpuTime() {
+        return !hasFlag(Context.BIND_ALLOW_FREEZE);
+    }
 
     public long getFlags() {
         return flags;
@@ -273,6 +277,9 @@
         if (hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) {
             sb.append("CAPS ");
         }
+        if (hasFlag(Context.BIND_ALLOW_FREEZE)) {
+            sb.append("!CPU ");
+        }
         if (serviceDead) {
             sb.append("DEAD ");
         }
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 336a35e..fa35da3 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2802,7 +2802,7 @@
         // we check the final procstate, and remove it if the procsate is below BFGS.
         capability |= getBfslCapabilityFromClient(client);
 
-        capability |= getCpuCapabilityFromClient(client);
+        capability |= getCpuCapabilityFromClient(cr, client);
 
         if (cr.notHasFlag(Context.BIND_WAIVE_PRIORITY)) {
             if (cr.hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) {
@@ -3259,7 +3259,7 @@
         // we check the final procstate, and remove it if the procsate is below BFGS.
         capability |= getBfslCapabilityFromClient(client);
 
-        capability |= getCpuCapabilityFromClient(client);
+        capability |= getCpuCapabilityFromClient(conn, client);
 
         if (clientProcState >= PROCESS_STATE_CACHED_ACTIVITY) {
             // If the other app is cached for any reason, for purposes here
@@ -3502,10 +3502,13 @@
     /**
      * @return the CPU capability from a client (of a service binding or provider).
      */
-    private static int getCpuCapabilityFromClient(ProcessRecord client) {
-        // Just grant CPU capability every time
-        // TODO(b/370817323): Populate with reasons to not propagate cpu capability across bindings.
-        return client.mState.getCurCapability() & PROCESS_CAPABILITY_CPU_TIME;
+    private static int getCpuCapabilityFromClient(OomAdjusterModernImpl.Connection conn,
+            ProcessRecord client) {
+        if (conn == null || conn.transmitsCpuTime()) {
+            return client.mState.getCurCapability() & PROCESS_CAPABILITY_CPU_TIME;
+        } else {
+            return 0;
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
index 1b7e8f0..7e7b568 100644
--- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
+++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
@@ -635,6 +635,15 @@
          * Returns true if this connection can propagate capabilities.
          */
         boolean canAffectCapabilities();
+
+        /**
+         * Returns whether this connection transmits PROCESS_CAPABILITY_CPU_TIME to the host, if the
+         * client possesses it.
+         */
+        default boolean transmitsCpuTime() {
+            // Always lend this capability by default.
+            return true;
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 4b5f06b..8ef79a91 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1472,8 +1472,8 @@
         mAudioService.postAccessoryPlugMediaUnmute(device);
     }
 
-    /*package*/ int getVolumeForDeviceIgnoreMute(int streamType, int device) {
-        return mAudioService.getVolumeForDeviceIgnoreMute(streamType, device);
+    /*package*/ int getVssVolumeForDevice(int streamType, int device) {
+        return mAudioService.getVssVolumeForDevice(streamType, device);
     }
 
     /*package*/ int getMaxVssVolumeForStream(int streamType) {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 2e6d984..829d9ea 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -2482,7 +2482,7 @@
     @GuardedBy("mDevicesLock")
     private void makeHearingAidDeviceAvailable(
             String address, String name, int streamType, String eventSource) {
-        final int hearingAidVolIndex = mDeviceBroker.getVolumeForDeviceIgnoreMute(streamType,
+        final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType,
                 DEVICE_OUT_HEARING_AID);
         mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType);
 
@@ -2672,7 +2672,7 @@
             }
 
             final int leAudioVolIndex = (volumeIndex == -1)
-                    ? mDeviceBroker.getVolumeForDeviceIgnoreMute(streamType, device)
+                    ? mDeviceBroker.getVssVolumeForDevice(streamType, device)
                     : volumeIndex;
             final int maxIndex = mDeviceBroker.getMaxVssVolumeForStream(streamType);
             mDeviceBroker.postSetLeAudioVolumeIndex(leAudioVolIndex, maxIndex, streamType);
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index a43e4d9..7664561 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -529,7 +529,7 @@
      */
     private InputDeviceVolumeHelper mInputDeviceVolumeHelper;
 
-    /*package*/ int getVolumeForDeviceIgnoreMute(int stream, int device) {
+    /*package*/ int getVssVolumeForDevice(int stream, int device) {
         final VolumeStreamState streamState = mStreamStates.get(stream);
         return streamState != null ? streamState.getIndex(device) : -1;
     }
@@ -4997,6 +4997,8 @@
         pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:"
                 + disablePrescaleAbsoluteVolume());
         pw.println("\tcom.android.media.audio.setStreamVolumeOrder - EOL");
+        pw.println("\tandroid.media.audio.ringtoneUserUriCheck:"
+                + android.media.audio.Flags.ringtoneUserUriCheck());
         pw.println("\tandroid.media.audio.roForegroundAudioControl:"
                 + roForegroundAudioControl());
         pw.println("\tandroid.media.audio.scoManagedByAudio:"
@@ -5098,7 +5100,7 @@
         }
 
         final int device = absVolumeDevices.toArray(new Integer[0])[0].intValue();
-        final int index = getVolumeForDeviceIgnoreMute(streamType, device);
+        final int index = getStreamVolume(streamType, device);
 
         if (DEBUG_VOL) {
             Slog.i(TAG, "onUpdateContextualVolumes streamType: " + streamType
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index 67afff7..643f330 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -724,7 +724,7 @@
                 int device = mAudioService.getDeviceForStream(AudioSystem.STREAM_MUSIC);
                 if (safeDevicesContains(device) && isStreamActive) {
                     scheduleMusicActiveCheck();
-                    int index = mAudioService.getVolumeForDeviceIgnoreMute(AudioSystem.STREAM_MUSIC,
+                    int index = mAudioService.getVssVolumeForDevice(AudioSystem.STREAM_MUSIC,
                             device);
                     if (index > safeMediaVolumeIndex(device)) {
                         // Approximate cumulative active music time
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index c2fecf2..d9db178 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -568,6 +568,7 @@
         }
         mWindowManagerCallbacks = callbacks;
         registerLidSwitchCallbackInternal(mWindowManagerCallbacks);
+        mKeyGestureController.setWindowManagerCallbacks(callbacks);
     }
 
     public void setWiredAccessoryCallbacks(WiredAccessoryCallbacks callbacks) {
@@ -2756,24 +2757,6 @@
                     @Nullable IBinder focussedToken) {
                 return InputManagerService.this.handleKeyGestureEvent(event);
             }
-
-            @Override
-            public boolean isKeyGestureSupported(int gestureType) {
-                switch (gestureType) {
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS:
-                        return true;
-                    default:
-                        return false;
-
-                }
-            }
         });
     }
 
@@ -3371,6 +3354,11 @@
          */
         @Nullable
         SurfaceControl createSurfaceForGestureMonitor(String name, int displayId);
+
+        /**
+         * Provide information on whether the keyguard is currently locked or not.
+         */
+        boolean isKeyguardLocked(int displayId);
     }
 
     /**
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index ef5babf..395c773 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -62,8 +62,10 @@
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
+import android.view.ViewConfiguration;
 
 import com.android.internal.R;
+import com.android.internal.accessibility.AccessibilityShortcutController;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.IShortcutService;
@@ -104,6 +106,7 @@
     private static final int MSG_NOTIFY_KEY_GESTURE_EVENT = 1;
     private static final int MSG_PERSIST_CUSTOM_GESTURES = 2;
     private static final int MSG_LOAD_CUSTOM_GESTURES = 3;
+    private static final int MSG_ACCESSIBILITY_SHORTCUT = 4;
 
     // must match: config_settingsKeyBehavior in config.xml
     private static final int SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0;
@@ -122,12 +125,15 @@
     static final int POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS = 2;
 
     private final Context mContext;
+    private InputManagerService.WindowManagerCallbacks mWindowManagerCallbacks;
     private final Handler mHandler;
     private final Handler mIoHandler;
     private final int mSystemPid;
     private final KeyCombinationManager mKeyCombinationManager;
     private final SettingsObserver mSettingsObserver;
     private final AppLaunchShortcutManager mAppLaunchShortcutManager;
+    @VisibleForTesting
+    final AccessibilityShortcutController mAccessibilityShortcutController;
     private final InputGestureManager mInputGestureManager;
     private final DisplayManager mDisplayManager;
     @GuardedBy("mInputDataStore")
@@ -175,8 +181,14 @@
 
     private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled();
 
-    KeyGestureController(Context context, Looper looper, Looper ioLooper,
+    public KeyGestureController(Context context, Looper looper, Looper ioLooper,
             InputDataStore inputDataStore) {
+        this(context, looper, ioLooper, inputDataStore, new Injector());
+    }
+
+    @VisibleForTesting
+    KeyGestureController(Context context, Looper looper, Looper ioLooper,
+            InputDataStore inputDataStore, Injector injector) {
         mContext = context;
         mHandler = new Handler(looper, this::handleMessage);
         mIoHandler = new Handler(ioLooper, this::handleIoMessage);
@@ -197,6 +209,8 @@
         mSettingsObserver = new SettingsObserver(mHandler);
         mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext);
         mInputGestureManager = new InputGestureManager(mContext);
+        mAccessibilityShortcutController = injector.getAccessibilityShortcutController(mContext,
+                mHandler);
         mDisplayManager = Objects.requireNonNull(mContext.getSystemService(DisplayManager.class));
         mInputDataStore = inputDataStore;
         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
@@ -295,8 +309,8 @@
                         KeyEvent.KEYCODE_VOLUME_UP) {
                     @Override
                     public boolean preCondition() {
-                        return isKeyGestureSupported(
-                                KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD);
+                        return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
+                                mWindowManagerCallbacks.isKeyguardLocked(DEFAULT_DISPLAY));
                     }
 
                     @Override
@@ -376,15 +390,15 @@
                             KeyEvent.KEYCODE_DPAD_DOWN) {
                         @Override
                         public boolean preCondition() {
-                            return isKeyGestureSupported(
-                                    KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD);
+                            return mAccessibilityShortcutController
+                                    .isAccessibilityShortcutAvailable(false);
                         }
 
                         @Override
                         public void execute() {
                             handleMultiKeyGesture(
                                     new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
-                                    KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+                                    KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
                                     KeyGestureEvent.ACTION_GESTURE_START, 0);
                         }
 
@@ -392,7 +406,7 @@
                         public void cancel() {
                             handleMultiKeyGesture(
                                     new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
-                                    KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+                                    KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
                                     KeyGestureEvent.ACTION_GESTURE_COMPLETE,
                                     KeyGestureEvent.FLAG_CANCELLED);
                         }
@@ -438,6 +452,7 @@
         mSettingsObserver.observe();
         mAppLaunchShortcutManager.systemRunning();
         mInputGestureManager.systemRunning();
+        initKeyGestures();
 
         int userId;
         synchronized (mUserLock) {
@@ -447,6 +462,27 @@
         mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget();
     }
 
+    @SuppressLint("MissingPermission")
+    private void initKeyGestures() {
+        InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
+        im.registerKeyGestureEventHandler((event, focusedToken) -> {
+            switch (event.getKeyGestureType()) {
+                case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+                    if (event.getAction() == KeyGestureEvent.ACTION_GESTURE_START) {
+                        mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+                        mHandler.sendMessageDelayed(
+                                mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT),
+                                getAccessibilityShortcutTimeout());
+                    } else {
+                        mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+                    }
+                    return true;
+                default:
+                    return false;
+            }
+        });
+    }
+
     public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
         if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) {
             return false;
@@ -971,17 +1007,6 @@
         return false;
     }
 
-    private boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
-        synchronized (mKeyGestureHandlerRecords) {
-            for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) {
-                if (handler.isKeyGestureSupported(gestureType)) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
     public void notifyKeyGestureCompleted(int deviceId, int[] keycodes, int modifierState,
             @KeyGestureEvent.KeyGestureType int gestureType) {
         // TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally
@@ -1019,9 +1044,16 @@
         synchronized (mUserLock) {
             mCurrentUserId = userId;
         }
+        mAccessibilityShortcutController.setCurrentUser(userId);
         mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget();
     }
 
+
+    public void setWindowManagerCallbacks(
+            @NonNull InputManagerService.WindowManagerCallbacks callbacks) {
+        mWindowManagerCallbacks = callbacks;
+    }
+
     private boolean isDefaultDisplayOn() {
         Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
         if (defaultDisplay == null) {
@@ -1068,6 +1100,9 @@
                 AidlKeyGestureEvent event = (AidlKeyGestureEvent) msg.obj;
                 notifyKeyGestureEvent(event);
                 break;
+            case MSG_ACCESSIBILITY_SHORTCUT:
+                mAccessibilityShortcutController.performAccessibilityShortcut();
+                break;
         }
         return true;
     }
@@ -1347,17 +1382,6 @@
             }
             return false;
         }
-
-        public boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
-            try {
-                return mKeyGestureHandler.isKeyGestureSupported(gestureType);
-            } catch (RemoteException ex) {
-                Slog.w(TAG, "Failed to identify if key gesture type is supported by the "
-                        + "process " + mPid + ", assuming it died.", ex);
-                binderDied();
-            }
-            return false;
-        }
     }
 
     private class SettingsObserver extends ContentObserver {
@@ -1413,6 +1437,25 @@
         return event;
     }
 
+    private long getAccessibilityShortcutTimeout() {
+        synchronized (mUserLock) {
+            final ViewConfiguration config = ViewConfiguration.get(mContext);
+            final boolean hasDialogShown = Settings.Secure.getIntForUser(
+                    mContext.getContentResolver(),
+                    Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, mCurrentUserId) != 0;
+            final boolean skipTimeoutRestriction =
+                    Settings.Secure.getIntForUser(mContext.getContentResolver(),
+                            Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION,
+                            0, mCurrentUserId) != 0;
+
+            // If users manually set the volume key shortcut for any accessibility service, the
+            // system would bypass the timeout restriction of the shortcut dialog.
+            return hasDialogShown || skipTimeoutRestriction
+                    ? config.getAccessibilityShortcutKeyTimeoutAfterConfirmation()
+                    : config.getAccessibilityShortcutKeyTimeout();
+        }
+    }
+
     public void dump(IndentingPrintWriter ipw) {
         ipw.println("KeyGestureController:");
         ipw.increaseIndent();
@@ -1459,4 +1502,12 @@
         mAppLaunchShortcutManager.dump(ipw);
         mInputGestureManager.dump(ipw);
     }
+
+    @VisibleForTesting
+    static class Injector {
+        AccessibilityShortcutController getAccessibilityShortcutController(Context context,
+                Handler handler) {
+            return new AccessibilityShortcutController(context, handler, UserHandle.USER_SYSTEM);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
index 02987a9..15f186b0 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
@@ -132,8 +132,8 @@
                 @Nullable EditorInfo editorInfo, IRemoteInputConnection inputConnection,
                 IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
                 int unverifiedTargetSdkVersion, @UserIdInt int userId,
-                @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq,
-                boolean useAsyncShowHideMethod);
+                @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible,
+                int startInputSeq, boolean useAsyncShowHideMethod);
 
         InputBindResult startInputOrWindowGainedFocus(
                 @StartInputReason int startInputReason, IInputMethodClient client,
@@ -142,7 +142,7 @@
                 @Nullable EditorInfo editorInfo, IRemoteInputConnection inputConnection,
                 IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
                 int unverifiedTargetSdkVersion, @UserIdInt int userId,
-                @NonNull ImeOnBackInvokedDispatcher imeDispatcher);
+                @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible);
 
         void showInputMethodPickerFromClient(IInputMethodClient client, int auxiliarySubtypeMode);
 
@@ -324,11 +324,11 @@
             IRemoteInputConnection inputConnection,
             IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
             int unverifiedTargetSdkVersion, @UserIdInt int userId,
-            @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) {
         return mCallback.startInputOrWindowGainedFocus(
                 startInputReason, client, windowToken, startInputFlags, softInputMode,
                 windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection,
-                unverifiedTargetSdkVersion, userId, imeDispatcher);
+                unverifiedTargetSdkVersion, userId, imeDispatcher, imeRequestedVisible);
     }
 
     @Override
@@ -340,13 +340,13 @@
             IRemoteInputConnection inputConnection,
             IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
             int unverifiedTargetSdkVersion, @UserIdInt int userId,
-            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq,
-            boolean useAsyncShowHideMethod) {
+            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible,
+            int startInputSeq, boolean useAsyncShowHideMethod) {
         mCallback.startInputOrWindowGainedFocusAsync(
                 startInputReason, client, windowToken, startInputFlags, softInputMode,
                 windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection,
-                unverifiedTargetSdkVersion, userId, imeDispatcher, startInputSeq,
-                useAsyncShowHideMethod);
+                unverifiedTargetSdkVersion, userId, imeDispatcher, imeRequestedVisible,
+                startInputSeq, useAsyncShowHideMethod);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index 69353be..2c07a31 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -443,7 +443,8 @@
     }
 
     @GuardedBy("ImfLock.class")
-    ImeVisibilityResult computeState(ImeTargetWindowState state, boolean allowVisible) {
+    ImeVisibilityResult computeState(ImeTargetWindowState state, boolean allowVisible,
+            boolean imeRequestedVisible) {
         // TODO: Output the request IME visibility state according to the requested window state
         final int softInputVisibility = state.mSoftInputModeState & SOFT_INPUT_MASK_STATE;
         // Should we auto-show the IME even if the caller has not
@@ -576,7 +577,8 @@
                         SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR);
             }
         }
-        if (!state.hasEditorFocused() && mInputShown && state.isStartInputByGainFocus()
+        if (!state.hasEditorFocused() && (mInputShown || (Flags.refactorInsetsController()
+                && imeRequestedVisible)) && state.isStartInputByGainFocus()
                 && mService.mInputMethodDeviceConfigs.shouldHideImeWhenNoEditorFocus()) {
             // Hide the soft-keyboard when the system do nothing for softInputModeState
             // of the window being gained focus without an editor. This behavior benefits
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 68ad8f7..2375775 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -3725,8 +3725,8 @@
             IRemoteInputConnection inputConnection,
             IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
             int unverifiedTargetSdkVersion, @UserIdInt int userId,
-            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq,
-            boolean useAsyncShowHideMethod) {
+            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible,
+            int startInputSeq, boolean useAsyncShowHideMethod) {
         // implemented by ZeroJankProxy
     }
 
@@ -3739,7 +3739,7 @@
             IRemoteInputConnection inputConnection,
             IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
             int unverifiedTargetSdkVersion, @UserIdInt int userId,
-            @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) {
         if (UserHandle.getCallingUserId() != userId) {
             mContext.enforceCallingOrSelfPermission(
                     Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
@@ -3870,7 +3870,8 @@
                     result = startInputOrWindowGainedFocusInternalLocked(startInputReason,
                             client, windowToken, startInputFlags, softInputMode, windowFlags,
                             editorInfo, inputConnection, remoteAccessibilityInputConnection,
-                            unverifiedTargetSdkVersion, bindingController, imeDispatcher, cs);
+                            unverifiedTargetSdkVersion, bindingController, imeDispatcher, cs,
+                            imeRequestedVisible);
                 } finally {
                     Binder.restoreCallingIdentity(ident);
                 }
@@ -3899,7 +3900,8 @@
             IRemoteInputConnection inputContext,
             @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
             int unverifiedTargetSdkVersion, @NonNull InputMethodBindingController bindingController,
-            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, @NonNull ClientState cs) {
+            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, @NonNull ClientState cs,
+            boolean imeRequestedVisible) {
         ProtoLog.v(IMMS_DEBUG, "startInputOrWindowGainedFocusInternalLocked: reason=%s"
                     + " client=%s"
                     + " inputContext=%s"
@@ -3910,12 +3912,13 @@
                     + " unverifiedTargetSdkVersion=%s"
                     + " bindingController=%s"
                     + " imeDispatcher=%s"
-                    + " cs=%s",
+                    + " cs=%s"
+                    + " imeRequestedVisible=%s",
                 InputMethodDebug.startInputReasonToString(startInputReason), client.asBinder(),
                 inputContext, editorInfo, InputMethodDebug.startInputFlagsToString(startInputFlags),
                 InputMethodDebug.softInputModeToString(softInputMode),
                 Integer.toHexString(windowFlags), unverifiedTargetSdkVersion, bindingController,
-                imeDispatcher, cs);
+                imeDispatcher, cs, imeRequestedVisible);
 
         final int userId = bindingController.getUserId();
         final var userData = getUserData(userId);
@@ -3963,7 +3966,8 @@
         InputBindResult res = null;
 
         final ImeVisibilityResult imeVisRes = visibilityStateComputer.computeState(windowState,
-                isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags));
+                isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags),
+                imeRequestedVisible);
         if (imeVisRes != null) {
             boolean isShow = false;
             switch (imeVisRes.getReason()) {
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index 7252925..12c1d9c 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -234,15 +234,15 @@
             IRemoteInputConnection inputConnection,
             IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
             int unverifiedTargetSdkVersion, @UserIdInt int userId,
-            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq,
-            boolean useAsyncShowHideMethod) {
+            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible,
+            int startInputSeq, boolean useAsyncShowHideMethod) {
         offload(() -> {
             InputBindResult result = mInner.startInputOrWindowGainedFocus(startInputReason, client,
                     windowToken, startInputFlags, softInputMode, windowFlags,
                     editorInfo,
                     inputConnection, remoteAccessibilityInputConnection,
                     unverifiedTargetSdkVersion,
-                    userId, imeDispatcher);
+                    userId, imeDispatcher, imeRequestedVisible);
             sendOnStartInputResult(client, result, startInputSeq);
             // For first-time client bind, MSG_BIND should arrive after MSG_START_INPUT_RESULT.
             if (result.result == InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION) {
@@ -269,7 +269,7 @@
             IRemoteInputConnection inputConnection,
             IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
             int unverifiedTargetSdkVersion, @UserIdInt int userId,
-            @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+            @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) {
         // Should never be called when flag is enabled i.e. when this proxy is used.
         return null;
     }
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 9e38435..ad108f6 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -28,6 +28,7 @@
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
+import android.hardware.audio.effect.DefaultExtension;
 import android.hardware.tv.mediaquality.AmbientBacklightColorFormat;
 import android.hardware.tv.mediaquality.IMediaQuality;
 import android.hardware.tv.mediaquality.IPictureProfileAdjustmentListener;
@@ -57,6 +58,7 @@
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.IBinder;
+import android.os.Parcel;
 import android.os.PersistableBundle;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
@@ -365,13 +367,21 @@
 
             try {
                 if (mMediaQuality != null) {
+                    PictureParameters pp = new PictureParameters();
                     PictureParameter[] pictureParameters = MediaQualityUtils
                             .convertPersistableBundleToPictureParameterList(params);
 
-                    PictureParameters pp = new PictureParameters();
+                    PersistableBundle vendorPictureParameters = params
+                            .getPersistableBundle(BaseParameters.VENDOR_PARAMETERS);
+                    Parcel parcel = Parcel.obtain();
+                    if (vendorPictureParameters != null) {
+                        setVendorPictureParameters(pp, parcel, vendorPictureParameters);
+                    }
+
                     pp.pictureParameters = pictureParameters;
 
                     mMediaQuality.sendDefaultPictureParameters(pp);
+                    parcel.recycle();
                     return true;
                 }
             } catch (RemoteException e) {
@@ -1419,11 +1429,19 @@
                     MediaQualityUtils.convertPersistableBundleToPictureParameterList(
                             params);
 
+            PersistableBundle vendorPictureParameters = params
+                    .getPersistableBundle(BaseParameters.VENDOR_PARAMETERS);
+            Parcel parcel = Parcel.obtain();
+            if (vendorPictureParameters != null) {
+                setVendorPictureParameters(pictureParameters, parcel, vendorPictureParameters);
+            }
+
             android.hardware.tv.mediaquality.PictureProfile toReturn =
                     new android.hardware.tv.mediaquality.PictureProfile();
             toReturn.pictureProfileId = id;
             toReturn.parameters = pictureParameters;
 
+            parcel.recycle();
             return toReturn;
         }
 
@@ -1729,4 +1747,16 @@
             return android.hardware.tv.mediaquality.IMediaQualityCallback.Stub.VERSION;
         }
     }
+
+    private void setVendorPictureParameters(
+            PictureParameters pictureParameters,
+            Parcel parcel,
+            PersistableBundle vendorPictureParameters) {
+        vendorPictureParameters.writeToParcel(parcel, 0);
+        byte[] vendorBundleToByteArray = parcel.marshall();
+        DefaultExtension defaultExtension = new DefaultExtension();
+        defaultExtension.bytes = Arrays.copyOf(
+                vendorBundleToByteArray, vendorBundleToByteArray.length);
+        pictureParameters.vendorPictureParameters.setParcelable(defaultExtension);
+    }
 }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 46dc758..3230e89 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -4240,66 +4240,14 @@
         if (!useKeyGestureEventHandler()) {
             return;
         }
-        mInputManager.registerKeyGestureEventHandler(new InputManager.KeyGestureEventHandler() {
-            @Override
-            public boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
-                    @Nullable IBinder focusedToken) {
-                boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event,
-                        focusedToken);
-                if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch(
-                        (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
-                    mPowerKeyHandled = true;
-                }
-                return handled;
+        mInputManager.registerKeyGestureEventHandler((event, focusedToken) -> {
+            boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event,
+                    focusedToken);
+            if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch(
+                    (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
+                mPowerKeyHandled = true;
             }
-
-            @Override
-            public boolean isKeyGestureSupported(int gestureType) {
-                switch (gestureType) {
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_HOME:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_BACK:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK:
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS:
-                        return true;
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
-                        return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
-                                isKeyguardLocked());
-                    case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
-                        return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
-                                false);
-                    default:
-                        return false;
-                }
-            }
+            return handled;
         });
     }
 
@@ -4457,13 +4405,6 @@
                     cancelPendingScreenshotChordAction();
                 }
                 return true;
-            case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
-                if (start) {
-                    interceptAccessibilityShortcutChord();
-                } else {
-                    cancelPendingAccessibilityShortcutAction();
-                }
-                return true;
             case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
                 if (start) {
                     interceptRingerToggleChord();
@@ -4481,14 +4422,6 @@
                     cancelGlobalActionsAction();
                 }
                 return true;
-                // TODO (b/358569822): Consolidate TV and non-TV gestures into same KeyGestureEvent
-            case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
-                if (start) {
-                    interceptAccessibilityGestureTv();
-                } else {
-                    cancelAccessibilityGestureTv();
-                }
-                return true;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
                 if (start) {
                     interceptBugreportGestureTv();
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 1299a4d..f243d4f 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -153,7 +153,7 @@
             final DisplayContent dc = mService.mRoot.getDisplayContent(displayId);
             if (dc != null) {
                 final Display display = dc.getDisplay();
-                if (display != null && display.getType() != Display.TYPE_OVERLAY) {
+                if (display != null) {
                     final DisplayMagnifier magnifier = new DisplayMagnifier(
                             mService, dc, display, callbacks);
                     magnifier.notifyImeWindowVisibilityChanged(
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 3f24da9..51025d2 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -60,6 +60,7 @@
 import com.android.server.wm.ActivityStarter.Factory;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -97,6 +98,9 @@
     /** Whether an {@link ActivityStarter} is currently executing (starting an Activity). */
     private boolean mInExecution = false;
 
+    /** The {@link TaskDisplayArea}s that are currently starting home activity. */
+    private ArrayList<TaskDisplayArea> mHomeLaunchingTaskDisplayAreas = new ArrayList<>();
+
     /**
      * TODO(b/64750076): Capture information necessary for dump and
      * {@link #postStartActivityProcessingForLastStarter} rather than keeping the entire object
@@ -162,6 +166,11 @@
 
     void startHomeActivity(Intent intent, ActivityInfo aInfo, String reason,
             TaskDisplayArea taskDisplayArea) {
+        if (mHomeLaunchingTaskDisplayAreas.contains(taskDisplayArea)) {
+            Slog.e(TAG, "Abort starting home on " + taskDisplayArea + " recursively.");
+            return;
+        }
+
         final ActivityOptions options = ActivityOptions.makeBasic();
         options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN);
         if (!ActivityRecord.isResolverActivity(aInfo.name)) {
@@ -186,13 +195,18 @@
             mSupervisor.endDeferResume();
         }
 
-        mLastHomeActivityStartResult = obtainStarter(intent, "startHomeActivity: " + reason)
-                .setOutActivity(tmpOutRecord)
-                .setCallingUid(0)
-                .setActivityInfo(aInfo)
-                .setActivityOptions(options.toBundle(),
-                        Binder.getCallingPid(), Binder.getCallingUid())
-                .execute();
+        try {
+            mHomeLaunchingTaskDisplayAreas.add(taskDisplayArea);
+            mLastHomeActivityStartResult = obtainStarter(intent, "startHomeActivity: " + reason)
+                    .setOutActivity(tmpOutRecord)
+                    .setCallingUid(0)
+                    .setActivityInfo(aInfo)
+                    .setActivityOptions(options.toBundle(),
+                            Binder.getCallingPid(), Binder.getCallingUid())
+                    .execute();
+        } finally {
+            mHomeLaunchingTaskDisplayAreas.remove(taskDisplayArea);
+        }
         mLastHomeActivityStartRecord = tmpOutRecord[0];
         if (rootHomeTask.mInResumeTopActivity) {
             // If we are in resume section already, home activity will be initialized, but not
@@ -479,9 +493,9 @@
                             }
                         } catch (SecurityException securityException) {
                             ActivityStarter.logAndThrowExceptionForIntentRedirect(mService.mContext,
-                                    "Creator URI Grant Caused Exception.", intent, creatorUid,
-                                    creatorPackage, filterCallingUid, callingPackage,
-                                    securityException);
+                                    ActivityStarter.INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION,
+                                    intent, creatorUid, creatorPackage, filterCallingUid,
+                                    callingPackage, securityException);
                         }
                     }
                     if ((aInfo.applicationInfo.privateFlags
@@ -720,6 +734,12 @@
             }
         }
 
+        if (!mHomeLaunchingTaskDisplayAreas.isEmpty()) {
+            dumped = true;
+            pw.print(prefix);
+            pw.println("mHomeLaunchingTaskDisplayAreas:" + mHomeLaunchingTaskDisplayAreas);
+        }
+
         if (!dumped) {
             pw.print(prefix);
             pw.println("(nothing)");
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 233f913..a84a008 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -65,6 +65,7 @@
 import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_CONFIGURATION;
 import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_TASKS;
 import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS;
+import static com.android.internal.util.FrameworkStatsLog.INTENT_REDIRECT_BLOCKED;
 import static com.android.server.pm.PackageArchiver.isArchivingEnabled;
 import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW;
@@ -140,6 +141,7 @@
 import com.android.internal.app.HeavyWeightSwitcherActivity;
 import com.android.internal.app.IVoiceInteractor;
 import com.android.internal.protolog.ProtoLog;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.UiThread;
 import com.android.server.am.ActivityManagerService.IntentCreatorToken;
 import com.android.server.am.PendingIntentRecord;
@@ -623,7 +625,7 @@
             if ((intent.getExtendedFlags() & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN)
                     != 0) {
                 logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext,
-                        "Unparceled intent does not have a creator token set.", intent,
+                        ActivityStarter.INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN, intent,
                         intentCreatorUid, intentCreatorPackage, resolvedCallingUid,
                         resolvedCallingPackage, null);
             }
@@ -659,9 +661,9 @@
                             }
                         } catch (SecurityException securityException) {
                             logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext,
-                                    "Creator URI Grant Caused Exception.", intent, intentCreatorUid,
-                                    intentCreatorPackage, resolvedCallingUid,
-                                    resolvedCallingPackage, securityException);
+                                    ActivityStarter.INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION,
+                                    intent, intentCreatorUid, intentCreatorPackage,
+                                    resolvedCallingUid, resolvedCallingPackage, securityException);
                         }
                     }
                 } else {
@@ -683,9 +685,9 @@
                             }
                         } catch (SecurityException securityException) {
                             logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext,
-                                    "Creator URI Grant Caused Exception.", intent, intentCreatorUid,
-                                    intentCreatorPackage, resolvedCallingUid,
-                                    resolvedCallingPackage, securityException);
+                                    ActivityStarter.INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION,
+                                    intent, intentCreatorUid, intentCreatorPackage,
+                                    resolvedCallingUid, resolvedCallingPackage, securityException);
                         }
                     }
                 }
@@ -1109,8 +1111,11 @@
             if (sourceRecord != null) {
                 if (requestCode >= 0 && !sourceRecord.finishing) {
                     resultRecord = sourceRecord;
+                    request.logMessage.append(" (rr=");
+                } else {
+                    request.logMessage.append(" (sr=");
                 }
-                request.logMessage.append(" (sr=" + System.identityHashCode(sourceRecord) + ")");
+                request.logMessage.append(System.identityHashCode(sourceRecord) + ")");
             }
         }
 
@@ -1261,27 +1266,27 @@
                         request.ignoreTargetSecurity, inTask != null, null, resultRecord,
                         resultRootTask)) {
                     abort = logAndAbortForIntentRedirect(mService.mContext,
-                            "Creator checkStartAnyActivityPermission Caused abortion.",
+                            ActivityStarter.INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION,
                             intent, intentCreatorUid, intentCreatorPackage, callingUid,
                             callingPackage);
                 }
             } catch (SecurityException e) {
                 logAndThrowExceptionForIntentRedirect(mService.mContext,
-                        "Creator checkStartAnyActivityPermission Caused Exception.",
+                        ActivityStarter.INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION,
                         intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage,
                         e);
             }
             if (!mService.mIntentFirewall.checkStartActivity(intent, intentCreatorUid,
                     0, resolvedType, aInfo.applicationInfo)) {
                 abort = logAndAbortForIntentRedirect(mService.mContext,
-                        "Creator IntentFirewall.checkStartActivity Caused abortion.",
+                        ActivityStarter.INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY,
                         intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage);
             }
 
             if (!mService.getPermissionPolicyInternal().checkStartActivity(intent,
                     intentCreatorUid, intentCreatorPackage)) {
                 abort = logAndAbortForIntentRedirect(mService.mContext,
-                        "Creator PermissionPolicyService.checkStartActivity Caused abortion.",
+                        ActivityStarter.INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY,
                         intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage);
             }
         }
@@ -3626,13 +3631,41 @@
         pw.println(mInTaskFragment);
     }
 
+    /**
+     * Error codes for intent redirect.
+     *
+     * @hide
+     */
+    @IntDef(prefix = {"INTENT_REDIRECT_"}, value = {
+            INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN,
+            INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION,
+            INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION,
+            INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION,
+            INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY,
+            INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface IntentRedirectErrorCode {
+    }
+
+    /**
+     * Error codes for intent redirect issues
+     */
+    static final int INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN = 1;
+    static final int INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION = 2;
+    static final int INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION = 3;
+    static final int INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION = 4;
+    static final int INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY = 5;
+    static final int INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY = 6;
+
     static void logAndThrowExceptionForIntentRedirect(@NonNull Context context,
-            @NonNull String message, @NonNull Intent intent, int intentCreatorUid,
+            @IntentRedirectErrorCode int errorCode, @NonNull Intent intent, int intentCreatorUid,
             @Nullable String intentCreatorPackage, int callingUid, @Nullable String callingPackage,
             @Nullable SecurityException originalException) {
-        String msg = getIntentRedirectPreventedLogMessage(message, intent, intentCreatorUid,
+        String msg = getIntentRedirectPreventedLogMessage(errorCode, intent, intentCreatorUid,
                 intentCreatorPackage, callingUid, callingPackage);
         Slog.wtf(TAG, msg);
+        FrameworkStatsLog.write(INTENT_REDIRECT_BLOCKED, intentCreatorUid, callingUid, errorCode);
         if (preventIntentRedirectShowToast()) {
             UiThread.getHandler().post(
                     () -> Toast.makeText(context,
@@ -3646,12 +3679,13 @@
     }
 
     private static boolean logAndAbortForIntentRedirect(@NonNull Context context,
-            @NonNull String message, @NonNull Intent intent, int intentCreatorUid,
+            @IntentRedirectErrorCode int errorCode, @NonNull Intent intent, int intentCreatorUid,
             @Nullable String intentCreatorPackage, int callingUid,
             @Nullable String callingPackage) {
-        String msg = getIntentRedirectPreventedLogMessage(message, intent, intentCreatorUid,
+        String msg = getIntentRedirectPreventedLogMessage(errorCode, intent, intentCreatorUid,
                 intentCreatorPackage, callingUid, callingPackage);
         Slog.wtf(TAG, msg);
+        FrameworkStatsLog.write(INTENT_REDIRECT_BLOCKED, intentCreatorUid, callingUid, errorCode);
         if (preventIntentRedirectShowToast()) {
             UiThread.getHandler().post(
                     () -> Toast.makeText(context,
@@ -3662,11 +3696,38 @@
                 ENABLE_PREVENT_INTENT_REDIRECT_TAKE_ACTION, callingUid);
     }
 
-    private static String getIntentRedirectPreventedLogMessage(@NonNull String message,
+    private static String getIntentRedirectPreventedLogMessage(
+            @IntentRedirectErrorCode int errorCode,
             @NonNull Intent intent, int intentCreatorUid, @Nullable String intentCreatorPackage,
             int callingUid, @Nullable String callingPackage) {
+        String message = getIntentRedirectErrorMessageFromCode(errorCode);
         return "[IntentRedirect Hardening] " + message + " intentCreatorUid: " + intentCreatorUid
                 + "; intentCreatorPackage: " + intentCreatorPackage + "; callingUid: " + callingUid
                 + "; callingPackage: " + callingPackage + "; intent: " + intent;
     }
+
+    private static String getIntentRedirectErrorMessageFromCode(
+            @IntentRedirectErrorCode int errorCode) {
+        return switch (errorCode) {
+            case INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN ->
+                    "INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN"
+                        + " (Unparceled intent does not have a creator token set, throw exception.)";
+            case INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION ->
+                    "INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION"
+                            + " (Creator URI permission grant throw exception.)";
+            case INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION ->
+                    "INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION"
+                            + " (Creator checkStartAnyActivityPermission, throw exception)";
+            case INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION ->
+                    "INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION"
+                            + " (Creator checkStartAnyActivityPermission, abort)";
+            case INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY ->
+                    "INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY"
+                            + " (Creator IntentFirewall.checkStartActivity, abort)";
+            case INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY ->
+                    "INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY"
+                            + " (Creator PermissionPolicyService.checkStartActivity, abort)";
+            default -> "Unknown error code: " + errorCode;
+        };
+    }
 }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 353ccd5..42b63d1 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -7104,6 +7104,10 @@
         public void setAnimatingTypes(@InsetsType int animatingTypes) {
             if (mAnimatingTypes != animatingTypes) {
                 mAnimatingTypes = animatingTypes;
+
+                if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+                    getInsetsStateController().onAnimatingTypesChanged(this);
+                }
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 2cac63c..040bbe4 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -263,8 +263,8 @@
         boolean oldVisibility = mSource.isVisible();
         super.updateVisibility();
         if (Flags.refactorInsetsController()) {
-            if (mSource.isVisible() && !oldVisibility && mImeRequester != null) {
-                reportImeDrawnForOrganizerIfNeeded(mImeRequester);
+            if (mSource.isVisible() && !oldVisibility && mControlTarget != null) {
+                reportImeDrawnForOrganizerIfNeeded(mControlTarget);
             }
         }
         onSourceChanged();
@@ -288,11 +288,15 @@
                 // If insets target is not available (e.g. RemoteInsetsControlTarget), use current
                 // IME input target to update IME request state. For example, switch from a task
                 // with showing IME to a split-screen task without showing IME.
-                InsetsTarget insetsTarget = target.getWindow();
-                if (insetsTarget == null && mServerVisible) {
-                    insetsTarget = mDisplayContent.getImeInputTarget();
+                InputTarget imeInputTarget = mDisplayContent.getImeInputTarget();
+                if (imeInputTarget != target && imeInputTarget != null) {
+                    // The controlTarget should be updated with the visibility of the
+                    // current IME input target.
+                    reportImeInputTargetStateToControlTarget(imeInputTarget, target,
+                            statsToken);
+                } else {
+                    invokeOnImeRequestedChangedListener(target, statsToken);
                 }
-                invokeOnImeRequestedChangedListener(insetsTarget, statsToken);
             }
         }
     }
@@ -328,8 +332,7 @@
             if (changed) {
                 ImeTracker.forLogging().onProgress(statsToken,
                         ImeTracker.PHASE_SERVER_UPDATE_CLIENT_VISIBILITY);
-                invokeOnImeRequestedChangedListener(mDisplayContent.getImeInputTarget(),
-                        statsToken);
+                invokeOnImeRequestedChangedListener(controlTarget, statsToken);
             } else {
                 // TODO(b/353463205) check cancelled / failed
                 ImeTracker.forLogging().onCancelled(statsToken,
@@ -383,7 +386,8 @@
                 // not all virtual displays have an ImeInsetsSourceProvider, so it is not
                 // guaranteed that the IME will be started when the control target reports its
                 // requested visibility back. Thus, invoking the listener here.
-                invokeOnImeRequestedChangedListener(imeInsetsTarget, statsToken);
+                invokeOnImeRequestedChangedListener((InsetsControlTarget) imeInsetsTarget,
+                        statsToken);
             } else {
                 ImeTracker.forLogging().onFailed(statsToken,
                         ImeTracker.PHASE_WM_SET_REMOTE_TARGET_IME_VISIBILITY);
@@ -392,18 +396,21 @@
     }
 
     // TODO(b/353463205) check callers to see if we can make statsToken @NonNull
-    private void invokeOnImeRequestedChangedListener(InsetsTarget insetsTarget,
+    private void invokeOnImeRequestedChangedListener(InsetsControlTarget controlTarget,
             @Nullable ImeTracker.Token statsToken) {
         final var imeListener = mDisplayContent.mWmService.mOnImeRequestedChangedListener;
         if (imeListener != null) {
-            if (insetsTarget != null) {
+            if (controlTarget != null) {
+                final boolean imeAnimating = Flags.reportAnimatingInsetsTypes()
+                        && (controlTarget.getAnimatingTypes() & WindowInsets.Type.ime()) != 0;
                 ImeTracker.forLogging().onProgress(statsToken,
                         ImeTracker.PHASE_WM_POSTING_CHANGED_IME_VISIBILITY);
                 mDisplayContent.mWmService.mH.post(() -> {
                     ImeTracker.forLogging().onProgress(statsToken,
                             ImeTracker.PHASE_WM_INVOKING_IME_REQUESTED_LISTENER);
-                    imeListener.onImeRequestedChanged(insetsTarget.getWindowToken(),
-                            insetsTarget.isRequestedVisible(WindowInsets.Type.ime()), statsToken);
+                    imeListener.onImeRequestedChanged(controlTarget.getWindowToken(),
+                            controlTarget.isRequestedVisible(WindowInsets.Type.ime())
+                                    || imeAnimating, statsToken);
                 });
             } else {
                 ImeTracker.forLogging().onFailed(statsToken,
@@ -416,6 +423,21 @@
         }
     }
 
+    @Override
+    void onAnimatingTypesChanged(InsetsControlTarget caller) {
+        if (Flags.reportAnimatingInsetsTypes()) {
+            final InsetsControlTarget controlTarget = getControlTarget();
+            // If the IME is not being requested anymore and the animation is finished, we need to
+            // invoke the listener, to let IMS eventually know
+            if (caller != null && caller == controlTarget && !caller.isRequestedVisible(
+                    WindowInsets.Type.ime())
+                    && (caller.getAnimatingTypes() & WindowInsets.Type.ime()) == 0) {
+                // TODO(b/353463205) check statsToken
+                invokeOnImeRequestedChangedListener(caller, null);
+            }
+        }
+    }
+
     private void reportImeDrawnForOrganizerIfNeeded(@NonNull InsetsControlTarget caller) {
         final WindowState callerWindow = caller.getWindow();
         if (callerWindow == null) {
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 7751ac3..a4bc5cb 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -343,6 +343,13 @@
         }
     }
 
+    @Override
+    public boolean isKeyguardLocked(int displayId) {
+        synchronized (mService.mGlobalLock) {
+            return mService.mAtmService.mKeyguardController.isKeyguardLocked(displayId);
+        }
+    }
+
     /** Waits until the built-in input devices have been configured. */
     public boolean waitForInputDevicesReady(long timeoutMillis) {
         synchronized (mInputDevicesReadyMonitor) {
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index b748902..1b693fc 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -673,6 +673,9 @@
                 mServerVisible, mClientVisible);
     }
 
+    void onAnimatingTypesChanged(InsetsControlTarget caller) {
+    }
+
     protected boolean isLeashReadyForDispatching() {
         return isLeashInitialized();
     }
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 5e0395f..810e48f 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -393,6 +393,13 @@
         }
     }
 
+    void onAnimatingTypesChanged(InsetsControlTarget target) {
+        for (int i = mProviders.size() - 1; i >= 0; i--) {
+            final InsetsSourceProvider provider = mProviders.valueAt(i);
+            provider.onAnimatingTypesChanged(target);
+        }
+    }
+
     private void notifyPendingInsetsControlChanged() {
         if (mPendingTargetProvidersMap.isEmpty()) {
             return;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index e98b2b7..8587b5a 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3831,10 +3831,11 @@
         pw.print(ActivityInfo.resizeModeToString(mResizeMode));
         pw.print(" mSupportsPictureInPicture="); pw.print(mSupportsPictureInPicture);
         pw.print(" isResizeable="); pw.println(isResizeable());
-        pw.print(" isPerceptible="); pw.println(mIsPerceptible);
+        pw.print(prefix); pw.print("isPerceptible="); pw.println(mIsPerceptible);
         pw.print(prefix); pw.print("lastActiveTime="); pw.print(lastActiveTime);
         pw.println(" (inactive for " + (getInactiveDuration() / 1000) + "s)");
-        pw.print(prefix); pw.println(" isTrimmable=" + mIsTrimmableFromRecents);
+        pw.print(prefix); pw.print("isTrimmable=" + mIsTrimmableFromRecents);
+        pw.print(" isForceHidden="); pw.println(isForceHidden());
         if (mLaunchAdjacentDisabled) {
             pw.println(prefix + "mLaunchAdjacentDisabled=true");
         }
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 2dabb25..960f5be 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -24,7 +24,6 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
@@ -2453,7 +2452,8 @@
             inOutConfig.windowConfiguration.setAppBounds(mTmpFullBounds);
             outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
 
-            if (!customContainerPolicy && windowingMode != WINDOWING_MODE_FREEFORM) {
+            // Floating tasks shouldn't be restricted by containing app bounds.
+            if (!customContainerPolicy && !isFloating(windowingMode)) {
                 final Rect containingAppBounds;
                 if (insideParentBounds) {
                     containingAppBounds = useOverrideInsetsForConfig
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 1022d18..ce91fc5 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -862,6 +862,12 @@
             mWmService.scheduleAnimationLocked();
 
             mAnimatingTypes = animatingTypes;
+
+            if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+                final InsetsStateController insetsStateController =
+                        getDisplayContent().getInsetsStateController();
+                insetsStateController.onAnimatingTypesChanged(this);
+            }
         }
     }
 
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index ae9a34e..c1d8382 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -73,10 +73,7 @@
     static_libs: [
         "androidx.annotation_annotation",
         "androidx.test.rules",
-        "framework",
-        "ravenwood-runtime",
-        "ravenwood-utils",
-        "services",
+        "services.core",
     ],
     libs: [
         "android.test.base.stubs.system",
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index 05615f6..2339a94 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -287,6 +287,7 @@
                 mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */,
                 mTargetSdkVersion /* unverifiedTargetSdkVersion */,
                 mUserId /* userId */,
-                mMockImeOnBackInvokedDispatcher /* imeDispatcher */);
+                mMockImeOnBackInvokedDispatcher /* imeDispatcher */,
+                true /* imeRequestedVisible */);
     }
 }
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
index 70eeae6..aa77919 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
@@ -267,7 +267,8 @@
             // visibility state will be preserved to the current window state.
             final ImeTargetWindowState stateWithUnChangedFlag = initImeTargetWindowState(
                     mWindowToken);
-            mComputer.computeState(stateWithUnChangedFlag, true /* allowVisible */);
+            mComputer.computeState(stateWithUnChangedFlag, true /* allowVisible */,
+                    true /* imeRequestedVisible */);
             assertThat(stateWithUnChangedFlag.isRequestedImeVisible()).isEqualTo(
                     lastState.isRequestedImeVisible());
         }
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
index 11abc94..b81b570 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
@@ -320,7 +320,8 @@
                 mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */,
                 mTargetSdkVersion /* unverifiedTargetSdkVersion */,
                 mUserId /* userId */,
-                mMockImeOnBackInvokedDispatcher /* imeDispatcher */);
+                mMockImeOnBackInvokedDispatcher /* imeDispatcher */,
+                true /* imeRequestedVisible */);
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 5d8f578..e094111 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -746,6 +746,36 @@
     @SuppressWarnings("GuardedBy")
     @Test
     @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY)
+    public void testUpdateOomAdjFreezeState_bindingWithAllowFreeze() {
+        ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+                MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
+        WindowProcessController wpc = app.getWindowProcessController();
+        doReturn(true).when(wpc).hasVisibleActivities();
+
+        final ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+                MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+
+        // App with a visible activity binds to app2 without any special flag.
+        bindService(app2, app, null, null, 0, mock(IBinder.class));
+
+        final ProcessRecord app3 = spy(makeDefaultProcessRecord(MOCKAPP3_PID, MOCKAPP3_UID,
+                MOCKAPP3_PROCESSNAME, MOCKAPP3_PACKAGENAME, false));
+
+        // App with a visible activity binds to app3 with ALLOW_FREEZE.
+        bindService(app3, app, null, null, Context.BIND_ALLOW_FREEZE, mock(IBinder.class));
+
+        setProcessesToLru(app, app2, app3);
+
+        updateOomAdj(app);
+
+        assertCpuTime(app);
+        assertCpuTime(app2);
+        assertNoCpuTime(app3);
+    }
+
+    @SuppressWarnings("GuardedBy")
+    @Test
+    @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY)
     @DisableFlags(Flags.FLAG_PROTOTYPE_AGGRESSIVE_FREEZING)
     public void testUpdateOomAdjFreezeState_bindingFromFgs() {
         final ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
index efea214..63c572a 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
@@ -171,7 +171,7 @@
             HearingDevicePhoneCallNotificationController.CallStateListener {
 
         TestCallStateListener(@NonNull Context context) {
-            super(context);
+            super(context, context.getMainExecutor());
         }
 
         @Override
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java
index f445b50..02361ff 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java
@@ -28,7 +28,12 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableContext;
 import android.testing.TestableLooper;
+import android.view.MotionEvent;
+import android.view.View;
 import android.view.WindowManager;
+import android.widget.ImageButton;
+
+import com.android.internal.R;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -49,12 +54,31 @@
             new TestableContext(getInstrumentation().getContext());
 
     @Mock private WindowManager mMockWindowManager;
+    @Mock private AutoclickScrollPanel.ScrollPanelControllerInterface mMockScrollPanelController;
+
     private AutoclickScrollPanel mScrollPanel;
 
+    // Scroll panel buttons.
+    private ImageButton mUpButton;
+    private ImageButton mDownButton;
+    private ImageButton mLeftButton;
+    private ImageButton mRightButton;
+    private ImageButton mExitButton;
+
     @Before
     public void setUp() {
         mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager);
-        mScrollPanel = new AutoclickScrollPanel(mTestableContext, mMockWindowManager);
+        mScrollPanel = new AutoclickScrollPanel(mTestableContext, mMockWindowManager,
+                mMockScrollPanelController);
+
+        View contentView = mScrollPanel.getContentViewForTesting();
+
+        // Initialize buttons.
+        mUpButton = contentView.findViewById(R.id.scroll_up);
+        mDownButton = contentView.findViewById(R.id.scroll_down);
+        mLeftButton = contentView.findViewById(R.id.scroll_left);
+        mRightButton = contentView.findViewById(R.id.scroll_right);
+        mExitButton = contentView.findViewById(R.id.scroll_exit);
     }
 
     @Test
@@ -89,4 +113,55 @@
         // Verify scroll panel is hidden.
         assertThat(mScrollPanel.isVisible()).isFalse();
     }
+
+    @Test
+    public void initialState_correctButtonVisibility() {
+        // Verify all expected buttons exist in the view.
+        assertThat(mUpButton.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mDownButton.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mLeftButton.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mRightButton.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mExitButton.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void directionButtons_onHover_callsHandleScroll() {
+        // Test up button.
+        triggerHoverEvent(mUpButton);
+        verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_UP);
+
+        // Test down button.
+        triggerHoverEvent(mDownButton);
+        verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_DOWN);
+
+        // Test left button.
+        triggerHoverEvent(mLeftButton);
+        verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_LEFT);
+
+        // Test right button.
+        triggerHoverEvent(mRightButton);
+        verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_RIGHT);
+    }
+
+    @Test
+    public void exitButton_onHover_callsExitScrollMode() {
+        // Test exit button.
+        triggerHoverEvent(mExitButton);
+        verify(mMockScrollPanelController).exitScrollMode();
+    }
+
+    // Helper method to simulate a hover event on a view.
+    private void triggerHoverEvent(View view) {
+        MotionEvent event = MotionEvent.obtain(
+                /* downTime= */ 0,
+                /* eventTime= */ 0,
+                /* action= */ MotionEvent.ACTION_HOVER_ENTER,
+                /* x= */ 0,
+                /* y= */ 0,
+                /* metaState= */ 0);
+
+        // Dispatch the event to the view's OnHoverListener.
+        view.dispatchGenericMotionEvent(event);
+        event.recycle();
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
index 1af59da..5922b12 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
@@ -37,6 +37,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -46,6 +47,7 @@
 import android.graphics.PointF;
 import android.os.Looper;
 import android.os.SystemClock;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.DexmakerShareClassLoaderRule;
@@ -504,6 +506,36 @@
         assertThat(sentRawEvent.getDisplayId()).isEqualTo(rawDisplayId);
     }
 
+    @Test
+    @DisableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+    public void handleMotionEventStateTouchExploring_pointerUp_doesNotSendToManager() {
+        mTouchExplorer.getState().setServiceDetectsGestures(true);
+        mTouchExplorer.getState().clear();
+
+        mLastEvent = pointerDownEvent();
+        mTouchExplorer.getState().startTouchExploring();
+        MotionEvent event = fromTouchscreen(pointerUpEvent());
+
+        mTouchExplorer.onMotionEvent(event, event, /*policyFlags=*/0);
+
+        verify(mMockAms, never()).sendMotionEventToListeningServices(event);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+    public void handleMotionEventStateTouchExploring_pointerUp_sendsToManager() {
+        mTouchExplorer.getState().setServiceDetectsGestures(true);
+        mTouchExplorer.getState().clear();
+
+        mLastEvent = pointerDownEvent();
+        mTouchExplorer.getState().startTouchExploring();
+        MotionEvent event = fromTouchscreen(pointerUpEvent());
+
+        mTouchExplorer.onMotionEvent(event, event, /*policyFlags=*/0);
+
+        verify(mMockAms).sendMotionEventToListeningServices(event);
+    }
+
     /**
      * Used to play back event data of a gesture by parsing the log into MotionEvents and sending
      * them to TouchExplorer.
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java
new file mode 100644
index 0000000..3e7d9fd
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.gestures;
+
+import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_INTERACTION_END;
+
+import static com.android.server.accessibility.gestures.TouchState.STATE_CLEAR;
+import static com.android.server.accessibility.gestures.TouchState.STATE_TOUCH_EXPLORING;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.Display;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.accessibility.AccessibilityManagerService;
+import com.android.server.accessibility.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class TouchStateTest {
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private TouchState mTouchState;
+    @Mock private AccessibilityManagerService mMockAms;
+
+    @Before
+    public void setup() {
+        mTouchState = new TouchState(Display.DEFAULT_DISPLAY, mMockAms);
+    }
+
+    @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+    @Test
+    public void injectedEvent_interactionEnd_pointerDown_startsTouchExploring() {
+        mTouchState.mReceivedPointerTracker.mReceivedPointersDown = 1;
+        mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END);
+        assertThat(mTouchState.getState()).isEqualTo(STATE_TOUCH_EXPLORING);
+    }
+
+    @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+    @Test
+    public void injectedEvent_interactionEnd_pointerUp_clears() {
+        mTouchState.mReceivedPointerTracker.mReceivedPointersDown = 0;
+        mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END);
+        assertThat(mTouchState.getState()).isEqualTo(STATE_CLEAR);
+    }
+
+    @DisableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+    @Test
+    public void injectedEvent_interactionEnd_clears() {
+        mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END);
+        assertThat(mTouchState.getState()).isEqualTo(STATE_CLEAR);
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
index 3ca0197..fcdf88f 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
@@ -605,29 +605,6 @@
     }
 
     @Test
-    public void testKeyGestureAccessibilityShortcutChord() {
-        Assert.assertTrue(
-                sendKeyGestureEventStart(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
-        mPhoneWindowManager.moveTimeForward(5000);
-        Assert.assertTrue(
-                sendKeyGestureEventCancel(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
-        mPhoneWindowManager.assertAccessibilityKeychordCalled();
-    }
-
-    @Test
-    public void testKeyGestureAccessibilityShortcutChordCancelled() {
-        Assert.assertTrue(
-                sendKeyGestureEventStart(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
-        Assert.assertTrue(
-                sendKeyGestureEventCancel(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
-        mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
-    }
-
-    @Test
     public void testKeyGestureRingerToggleChord() {
         mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
         Assert.assertTrue(
@@ -670,29 +647,6 @@
     }
 
     @Test
-    public void testKeyGestureAccessibilityTvShortcutChord() {
-        Assert.assertTrue(
-                sendKeyGestureEventStart(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
-        mPhoneWindowManager.moveTimeForward(5000);
-        Assert.assertTrue(
-                sendKeyGestureEventCancel(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
-        mPhoneWindowManager.assertAccessibilityKeychordCalled();
-    }
-
-    @Test
-    public void testKeyGestureAccessibilityTvShortcutChordCancelled() {
-        Assert.assertTrue(
-                sendKeyGestureEventStart(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
-        Assert.assertTrue(
-                sendKeyGestureEventCancel(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
-        mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
-    }
-
-    @Test
     public void testKeyGestureTvTriggerBugReport() {
         Assert.assertTrue(
                 sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index f884924..e56fd3c 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -750,11 +750,6 @@
         verify(mAccessibilityShortcutController).performAccessibilityShortcut();
     }
 
-    void assertAccessibilityKeychordNotCalled() {
-        mTestLooper.dispatchAll();
-        verify(mAccessibilityShortcutController, never()).performAccessibilityShortcut();
-    }
-
     void assertCloseAllDialogs() {
         verify(mContext).closeSystemDialogs();
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
index 7d59f48..eb6d5cf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
@@ -21,10 +21,18 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import android.graphics.PixelFormat;
+import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.view.WindowInsets;
 import android.view.inputmethod.Flags;
 import android.view.inputmethod.ImeTracker;
 
@@ -211,4 +219,29 @@
         mImeProvider.setFrozen(false);
         assertFalse(mImeProvider.getSource().isVisible());
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
+    public void testUpdateControlForTarget_remoteInsetsControlTarget() throws RemoteException {
+        final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).build();
+        makeWindowVisibleAndDrawn(ime);
+        mImeProvider.setWindowContainer(ime, null, null);
+        mImeProvider.setServerVisible(true);
+        mImeProvider.setClientVisible(true);
+        final WindowState inputTarget = newWindowBuilder("app", TYPE_APPLICATION).build();
+        final var displayWindowInsetsController = spy(createDisplayWindowInsetsController());
+        mDisplayContent.setRemoteInsetsController(displayWindowInsetsController);
+        final var controlTarget = mDisplayContent.mRemoteInsetsControlTarget;
+
+        inputTarget.setRequestedVisibleTypes(
+                WindowInsets.Type.defaultVisible() | WindowInsets.Type.ime());
+        mDisplayContent.setImeInputTarget(inputTarget);
+        mDisplayContent.setImeControlTarget(controlTarget);
+
+        assertTrue(inputTarget.isRequestedVisible(WindowInsets.Type.ime()));
+        assertFalse(controlTarget.isRequestedVisible(WindowInsets.Type.ime()));
+        mImeProvider.updateControlForTarget(controlTarget, true /* force */, null /* statsToken */);
+        verify(displayWindowInsetsController, times(1)).setImeInputTargetRequestedVisibility(
+                eq(true), any());
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index ae61447..5401a44 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -910,7 +910,7 @@
         final RunningTaskInfo info2 = task2.getTaskInfo();
 
         WindowContainerTransaction wct = new WindowContainerTransaction();
-        wct.setAdjacentRoots(info1.token, info2.token);
+        wct.setAdjacentRootSet(info1.token, info2.token);
         mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
         assertTrue(task1.isAdjacentTo(task2));
         assertTrue(task2.isAdjacentTo(task1));
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index fbba999..14d567d 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -3371,14 +3371,13 @@
                 return telephony.getDataNetworkTypeForSubscriber(subId, getOpPackageName(),
                         getAttributionTag());
             } else {
-                // This can happen when the ITelephony interface is not up yet.
+                Log.e(TAG, "getDataNetworkType: ITelephony interface is not up yet");
                 return NETWORK_TYPE_UNKNOWN;
             }
-        } catch(RemoteException ex) {
-            // This shouldn't happen in the normal case
-            return NETWORK_TYPE_UNKNOWN;
-        } catch (NullPointerException ex) {
-            // This could happen before phone restarts due to crashing
+        } catch (RemoteException // Shouldn't happen in the normal case
+                | NullPointerException ex // Could happen before phone restarts due to crashing
+        ) {
+            Log.e(TAG, "getDataNetworkType: " + ex.getMessage());
             return NETWORK_TYPE_UNKNOWN;
         }
     }
diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
index e99c814..794fd02 100644
--- a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
+++ b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
@@ -214,9 +214,5 @@
         ): Boolean {
             return handler(event, focusedToken)
         }
-
-        override fun isKeyGestureSupported(gestureType: Int): Boolean {
-            return true
-        }
     }
 }
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 2799f6c..4f1fb64 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -32,6 +32,7 @@
 import android.hardware.input.InputManager
 import android.hardware.input.InputManagerGlobal
 import android.hardware.input.KeyGestureEvent
+import android.os.Handler
 import android.os.IBinder
 import android.os.Process
 import android.os.SystemClock
@@ -48,9 +49,11 @@
 import androidx.test.core.app.ApplicationProvider
 import com.android.dx.mockito.inline.extended.ExtendedMockito
 import com.android.internal.R
+import com.android.internal.accessibility.AccessibilityShortcutController
 import com.android.internal.annotations.Keep
 import com.android.internal.util.FrameworkStatsLog
 import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.server.input.InputManagerService.WindowManagerCallbacks
 import java.io.File
 import java.io.FileOutputStream
 import java.io.InputStream
@@ -67,6 +70,8 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
 
 /**
  * Tests for {@link KeyGestureController}.
@@ -102,6 +107,7 @@
         const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0
         const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1
         const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2
+        const val TEST_PID = 10
     }
 
     @JvmField
@@ -116,11 +122,10 @@
     @Rule
     val rule = SetFlagsRule()
 
-    @Mock
-    private lateinit var iInputManager: IInputManager
-
-    @Mock
-    private lateinit var packageManager: PackageManager
+    @Mock private lateinit var iInputManager: IInputManager
+    @Mock private lateinit var packageManager: PackageManager
+    @Mock private lateinit var wmCallbacks: WindowManagerCallbacks
+    @Mock private lateinit var accessibilityShortcutController: AccessibilityShortcutController
 
     private var currentPid = 0
     private lateinit var context: Context
@@ -207,8 +212,34 @@
 
     private fun setupKeyGestureController() {
         keyGestureController =
-            KeyGestureController(context, testLooper.looper, testLooper.looper, inputDataStore)
-        Mockito.`when`(iInputManager.getAppLaunchBookmarks())
+            KeyGestureController(
+                context,
+                testLooper.looper,
+                testLooper.looper,
+                inputDataStore,
+                object : KeyGestureController.Injector() {
+                    override fun getAccessibilityShortcutController(
+                        context: Context?,
+                        handler: Handler?
+                    ): AccessibilityShortcutController {
+                        return accessibilityShortcutController
+                    }
+                })
+        Mockito.`when`(iInputManager.registerKeyGestureHandler(Mockito.any()))
+            .thenAnswer {
+                val args = it.arguments
+                if (args[0] != null) {
+                    keyGestureController.registerKeyGestureHandler(
+                        args[0] as IKeyGestureHandler,
+                        TEST_PID
+                    )
+                }
+        }
+        keyGestureController.setWindowManagerCallbacks(wmCallbacks)
+        Mockito.`when`(wmCallbacks.isKeyguardLocked(Mockito.anyInt())).thenReturn(false)
+        Mockito.`when`(accessibilityShortcutController
+            .isAccessibilityShortcutAvailable(Mockito.anyBoolean())).thenReturn(true)
+        Mockito.`when`(iInputManager.appLaunchBookmarks)
             .thenReturn(keyGestureController.appLaunchBookmarks)
         keyGestureController.systemRunning()
         testLooper.dispatchAll()
@@ -1270,9 +1301,9 @@
                 )
             ),
             TestData(
-                "BACK + DPAD_DOWN -> TV Accessibility Chord",
+                "BACK + DPAD_DOWN -> Accessibility Chord(for TV)",
                 intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
-                KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
                 intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
                 0,
                 intArrayOf(
@@ -1622,6 +1653,52 @@
         )
     }
 
+    @Test
+    fun testAccessibilityShortcutChordPressed() {
+        setupKeyGestureController()
+
+        sendKeys(
+            intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN),
+            // Assuming this value is always greater than the accessibility shortcut timeout, which
+            // currently defaults to 3000ms
+            timeDelayMs = 10000
+        )
+        Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut()
+    }
+
+    @Test
+    fun testAccessibilityTvShortcutChordPressed() {
+        setupKeyGestureController()
+
+        sendKeys(
+            intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+            timeDelayMs = 10000
+        )
+        Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut()
+    }
+
+    @Test
+    fun testAccessibilityShortcutChordPressedForLessThanTimeout() {
+        setupKeyGestureController()
+
+        sendKeys(
+            intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN),
+            timeDelayMs = 0
+        )
+        Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut()
+    }
+
+    @Test
+    fun testAccessibilityTvShortcutChordPressedForLessThanTimeout() {
+        setupKeyGestureController()
+
+        sendKeys(
+            intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+            timeDelayMs = 0
+        )
+        Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut()
+    }
+
     private fun testKeyGestureInternal(test: TestData) {
         val handledEvents = mutableListOf<KeyGestureEvent>()
         val handler = KeyGestureHandler { event, _ ->
@@ -1683,7 +1760,11 @@
         assertEquals("Test: $testName should not produce Key gesture", 0, handledEvents.size)
     }
 
-    private fun sendKeys(testKeys: IntArray, assertNotSentToApps: Boolean = false) {
+    private fun sendKeys(
+        testKeys: IntArray,
+        assertNotSentToApps: Boolean = false,
+        timeDelayMs: Long = 0
+    ) {
         var metaState = 0
         val now = SystemClock.uptimeMillis()
         for (key in testKeys) {
@@ -1699,6 +1780,11 @@
             testLooper.dispatchAll()
         }
 
+        if (timeDelayMs > 0) {
+            testLooper.moveTimeForward(timeDelayMs)
+            testLooper.dispatchAll()
+        }
+
         for (key in testKeys.reversed()) {
             val upEvent = KeyEvent(
                 now, now, KeyEvent.ACTION_UP, key, 0 /*repeat*/, metaState,
@@ -1742,9 +1828,5 @@
         override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?): Boolean {
             return handler(event, token)
         }
-
-        override fun isKeyGestureSupported(gestureType: Int): Boolean {
-            return true
-        }
     }
 }
diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java
index 8cd89ce..649241a 100644
--- a/tests/testables/src/android/testing/TestableLooper.java
+++ b/tests/testables/src/android/testing/TestableLooper.java
@@ -75,16 +75,7 @@
      * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection.
      */
     private static boolean isAtLeastBaklava() {
-        Method[] methods = TestLooperManager.class.getMethods();
-        for (Method method : methods) {
-            if (method.getName().equals("peekWhen")) {
-                return true;
-            }
-        }
-        return false;
-        // TODO(shayba): delete the above, uncomment the below.
-        // SDK_INT has not yet ramped to Baklava in all 25Q2 builds.
-        // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
     }
 
     static {
diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java
index 4d379e4..bb54a26 100644
--- a/tests/utils/testutils/java/android/os/test/TestLooper.java
+++ b/tests/utils/testutils/java/android/os/test/TestLooper.java
@@ -68,16 +68,7 @@
      * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection.
      */
     private static boolean isAtLeastBaklava() {
-        Method[] methods = TestLooperManager.class.getMethods();
-        for (Method method : methods) {
-            if (method.getName().equals("peekWhen")) {
-                return true;
-            }
-        }
-        return false;
-        // TODO(shayba): delete the above, uncomment the below.
-        // SDK_INT has not yet ramped to Baklava in all 25Q2 builds.
-        // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
     }
 
     static {
diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h
index 0d261ab..e51477c 100644
--- a/tools/aapt2/Resource.h
+++ b/tools/aapt2/Resource.h
@@ -249,6 +249,8 @@
 
   // Flag
   std::optional<FeatureFlagAttribute> flag;
+
+  bool uses_readwrite_feature_flags = false;
 };
 
 /**
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index 5435cba2..db7dddc 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -664,6 +664,7 @@
     if (!config_value->value) {
       // Resource does not exist, add it now.
       config_value->value = std::move(res.value);
+      config_value->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags;
     } else {
       // When validation is enabled, ensure that a resource cannot have multiple values defined for
       // the same configuration unless protected by flags.
@@ -681,12 +682,14 @@
                                ConfigKey{&res.config, res.product}, lt_config_key_ref()),
               util::make_unique<ResourceConfigValue>(res.config, res.product));
           (*it)->value = std::move(res.value);
+          (*it)->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags;
           break;
         }
 
         case CollisionResult::kTakeNew:
           // Take the incoming value.
           config_value->value = std::move(res.value);
+          config_value->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags;
           break;
 
         case CollisionResult::kConflict:
@@ -843,6 +846,12 @@
   return *this;
 }
 
+NewResourceBuilder& NewResourceBuilder::SetUsesReadWriteFeatureFlags(
+    bool uses_readwrite_feature_flags) {
+  res_.uses_readwrite_feature_flags = uses_readwrite_feature_flags;
+  return *this;
+}
+
 NewResource NewResourceBuilder::Build() {
   return std::move(res_);
 }
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index b0e1855..778b43a 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -104,6 +104,9 @@
   // The actual Value.
   std::unique_ptr<Value> value;
 
+  // Whether the value uses read/write feature flags
+  bool uses_readwrite_feature_flags = false;
+
   ResourceConfigValue(const android::ConfigDescription& config, android::StringPiece product)
       : config(config), product(product) {
   }
@@ -284,6 +287,7 @@
   std::optional<AllowNew> allow_new;
   std::optional<StagedId> staged_id;
   bool allow_mangled = false;
+  bool uses_readwrite_feature_flags = false;
 };
 
 struct NewResourceBuilder {
@@ -297,6 +301,7 @@
   NewResourceBuilder& SetAllowNew(AllowNew allow_new);
   NewResourceBuilder& SetStagedId(StagedId id);
   NewResourceBuilder& SetAllowMangled(bool allow_mangled);
+  NewResourceBuilder& SetUsesReadWriteFeatureFlags(bool uses_feature_flags);
   NewResource Build();
 
  private:
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 2a79216..755dbb6 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -673,11 +673,13 @@
 
               // Update the output format of this XML file.
               file_ref->type = XmlFileTypeForOutputFormat(options_.output_format);
-              bool result = table->AddResource(NewResourceBuilder(file.name)
-                                                   .SetValue(std::move(file_ref), file.config)
-                                                   .SetAllowMangled(true)
-                                                   .Build(),
-                                               context_->GetDiagnostics());
+              bool result = table->AddResource(
+                  NewResourceBuilder(file.name)
+                      .SetValue(std::move(file_ref), file.config)
+                      .SetAllowMangled(true)
+                      .SetUsesReadWriteFeatureFlags(doc->file.uses_readwrite_feature_flags)
+                      .Build(),
+                  context_->GetDiagnostics());
               if (!result) {
                 return false;
               }
diff --git a/tools/aapt2/format/binary/BinaryResourceParser.cpp b/tools/aapt2/format/binary/BinaryResourceParser.cpp
index 2e20e81..bac871b 100644
--- a/tools/aapt2/format/binary/BinaryResourceParser.cpp
+++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp
@@ -414,6 +414,8 @@
         .SetId(res_id, OnIdConflict::CREATE_ENTRY)
         .SetAllowMangled(true);
 
+    res_builder.SetUsesReadWriteFeatureFlags(entry->uses_feature_flags());
+
     if (entry->flags() & ResTable_entry::FLAG_PUBLIC) {
       Visibility visibility{Visibility::Level::kPublic};
 
diff --git a/tools/aapt2/format/binary/ResEntryWriter.cpp b/tools/aapt2/format/binary/ResEntryWriter.cpp
index 9dc205f..0be3921 100644
--- a/tools/aapt2/format/binary/ResEntryWriter.cpp
+++ b/tools/aapt2/format/binary/ResEntryWriter.cpp
@@ -199,6 +199,10 @@
     flags |= ResTable_entry::FLAG_WEAK;
   }
 
+  if (entry->uses_readwrite_feature_flags) {
+    flags |= ResTable_entry::FLAG_USES_FEATURE_FLAGS;
+  }
+
   if constexpr (std::is_same_v<ResTable_entry_ext, T>) {
     flags |= ResTable_entry::FLAG_COMPLEX;
   }
diff --git a/tools/aapt2/format/binary/ResEntryWriter.h b/tools/aapt2/format/binary/ResEntryWriter.h
index c11598e..f54b29a 100644
--- a/tools/aapt2/format/binary/ResEntryWriter.h
+++ b/tools/aapt2/format/binary/ResEntryWriter.h
@@ -38,6 +38,8 @@
 
   // The entry string pool index to the entry's name.
   uint32_t entry_key;
+
+  bool uses_readwrite_feature_flags;
 };
 
 // Pair of ResTable_entry and Res_value. These pairs are stored sequentially in values buffer.
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 1a82021..50144ae 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -502,7 +502,8 @@
         // Group values by configuration.
         for (auto& config_value : entry.values) {
           config_to_entry_list_map[config_value->config].push_back(
-              FlatEntry{&entry, config_value->value.get(), local_key_index});
+              FlatEntry{&entry, config_value->value.get(), local_key_index,
+                        config_value->uses_readwrite_feature_flags});
         }
       }
 
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index 0f11685..9156b96 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -1069,4 +1069,23 @@
               testing::IsTrue());
 }
 
+TEST_F(TableFlattenerTest, UsesReadWriteFeatureFlagSerializesCorrectly) {
+  std::unique_ptr<ResourceTable> table =
+      test::ResourceTableBuilder()
+          .Add(NewResourceBuilder("com.app.a:color/foo")
+                   .SetValue(util::make_unique<BinaryPrimitive>(
+                       uint8_t(Res_value::TYPE_INT_COLOR_ARGB8), 0xffaabbcc))
+                   .SetUsesReadWriteFeatureFlags(true)
+                   .SetId(0x7f020000)
+                   .Build())
+          .Build();
+  ResTable res_table;
+  TableFlattenerOptions options;
+  ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table));
+
+  uint32_t flags;
+  ASSERT_TRUE(res_table.getResourceEntryFlags(0x7f020000, &flags));
+  ASSERT_EQ(flags, ResTable_entry::FLAG_USES_FEATURE_FLAGS);
+}
+
 }  // namespace aapt
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
index dbef776..47a71fe 100644
--- a/tools/aapt2/link/FlaggedResources_test.cpp
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+#include <regex>
+#include <string>
+
 #include "LoadedApk.h"
 #include "cmd/Dump.h"
 #include "io/StringStream.h"
@@ -183,4 +186,49 @@
       "Only read only flags may be used with resources: test.package.rwFlag"));
 }
 
+TEST_F(FlaggedResourcesTest, ReadWriteFlagInXmlGetsFlagged) {
+  auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"});
+  auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
+
+  std::string output;
+  DumpChunksToString(loaded_apk.get(), &output);
+
+  // The actual line looks something like:
+  // [ResTable_entry] id: 0x0000 name: layout1 keyIndex: 14 size: 8 flags: 0x0010
+  //
+  // This regex matches that line and captures the name and the flag value for checking.
+  std::regex regex("[0-9a-zA-Z:_\\]\\[ ]+name: ([0-9a-zA-Z]+)[0-9a-zA-Z: ]+flags: (0x\\d{4})");
+  std::smatch match;
+
+  std::stringstream ss(output);
+  std::string line;
+  bool found = false;
+  int fields_flagged = 0;
+  while (std::getline(ss, line)) {
+    bool first_line = false;
+    if (line.contains("config: v36")) {
+      std::getline(ss, line);
+      first_line = true;
+    }
+    if (!line.contains("flags")) {
+      continue;
+    }
+    if (std::regex_search(line, match, regex) && (match.size() == 3)) {
+      unsigned int hex_value;
+      std::stringstream hex_ss;
+      hex_ss << std::hex << match[2];
+      hex_ss >> hex_value;
+      if (hex_value & android::ResTable_entry::FLAG_USES_FEATURE_FLAGS) {
+        fields_flagged++;
+        if (first_line && match[1] == "layout1") {
+          found = true;
+        }
+      }
+    }
+  }
+  ASSERT_TRUE(found) << "No entry for layout1 at v36 with FLAG_USES_FEATURE_FLAGS bit set";
+  // There should only be 1 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1
+  ASSERT_EQ(fields_flagged, 1);
+}
+
 }  // namespace aapt
diff --git a/tools/aapt2/link/FlaggedXmlVersioner.cpp b/tools/aapt2/link/FlaggedXmlVersioner.cpp
index 75c6f17..8a3337c 100644
--- a/tools/aapt2/link/FlaggedXmlVersioner.cpp
+++ b/tools/aapt2/link/FlaggedXmlVersioner.cpp
@@ -66,6 +66,28 @@
   bool had_flags_ = false;
 };
 
+// An xml visitor that goes through the a doc and determines if any elements are behind a flag.
+class FindFlagsVisitor : public xml::Visitor {
+ public:
+  void Visit(xml::Element* node) override {
+    if (had_flags_) {
+      return;
+    }
+    auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag);
+    if (attr != nullptr) {
+      had_flags_ = true;
+      return;
+    }
+    VisitChildren(node);
+  }
+
+  bool HadFlags() const {
+    return had_flags_;
+  }
+
+  bool had_flags_ = false;
+};
+
 std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAaptContext* context,
                                                                             xml::XmlResource* doc) {
   std::vector<std::unique_ptr<xml::XmlResource>> docs;
@@ -74,15 +96,20 @@
     // Support for read/write flags was added in baklava so if the doc will only get used on
     // baklava or later we can just return the original doc.
     docs.push_back(doc->Clone());
+    FindFlagsVisitor visitor;
+    doc->root->Accept(&visitor);
+    docs.back()->file.uses_readwrite_feature_flags = visitor.HadFlags();
   } else {
     auto preBaklavaVersion = doc->Clone();
     AllDisabledFlagsVisitor visitor;
     preBaklavaVersion->root->Accept(&visitor);
+    preBaklavaVersion->file.uses_readwrite_feature_flags = false;
     docs.push_back(std::move(preBaklavaVersion));
 
     if (visitor.HadFlags()) {
       auto baklavaVersion = doc->Clone();
       baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA;
+      baklavaVersion->file.uses_readwrite_feature_flags = true;
       docs.push_back(std::move(baklavaVersion));
     }
   }