Merge "fix: show overrides in device_config list" into main
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index aca9bb4..531537c 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -30,6 +30,7 @@
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.net.Uri;
 import android.os.Build;
 import android.os.IBinder;
 import android.os.Parcel;
@@ -303,6 +304,19 @@
     public boolean isTopActivityStyleFloating;
 
     /**
+     * The URI of the intent that generated the top-most activity opened using a URL.
+     * @hide
+     */
+    @Nullable
+    public Uri capturedLink;
+
+    /**
+     * The time of the last launch of the activity opened using the {@link #capturedLink}.
+     * @hide
+     */
+    public long capturedLinkTimestamp;
+
+    /**
      * Encapsulate specific App Compat information.
      * @hide
      */
@@ -436,6 +450,8 @@
                 && Objects.equals(topActivity, that.topActivity)
                 && isTopActivityTransparent == that.isTopActivityTransparent
                 && isTopActivityStyleFloating == that.isTopActivityStyleFloating
+                && Objects.equals(capturedLink, that.capturedLink)
+                && capturedLinkTimestamp == that.capturedLinkTimestamp
                 && appCompatTaskInfo.equalsForTaskOrganizer(that.appCompatTaskInfo);
     }
 
@@ -506,6 +522,8 @@
         displayAreaFeatureId = source.readInt();
         isTopActivityTransparent = source.readBoolean();
         isTopActivityStyleFloating = source.readBoolean();
+        capturedLink = source.readTypedObject(Uri.CREATOR);
+        capturedLinkTimestamp = source.readLong();
         appCompatTaskInfo = source.readTypedObject(AppCompatTaskInfo.CREATOR);
     }
 
@@ -554,6 +572,8 @@
         dest.writeInt(displayAreaFeatureId);
         dest.writeBoolean(isTopActivityTransparent);
         dest.writeBoolean(isTopActivityStyleFloating);
+        dest.writeTypedObject(capturedLink, flags);
+        dest.writeLong(capturedLinkTimestamp);
         dest.writeTypedObject(appCompatTaskInfo, flags);
     }
 
@@ -592,6 +612,8 @@
                 + " displayAreaFeatureId=" + displayAreaFeatureId
                 + " isTopActivityTransparent=" + isTopActivityTransparent
                 + " isTopActivityStyleFloating=" + isTopActivityStyleFloating
+                + " capturedLink=" + capturedLink
+                + " capturedLinkTimestamp=" + capturedLinkTimestamp
                 + " appCompatTaskInfo=" + appCompatTaskInfo
                 + "}";
     }
diff --git a/core/java/android/hardware/OWNERS b/core/java/android/hardware/OWNERS
index 51ad151..43d3f54 100644
--- a/core/java/android/hardware/OWNERS
+++ b/core/java/android/hardware/OWNERS
@@ -5,7 +5,7 @@
 sumir@google.com
 
 # Camera
-per-file *Camera*=cychen@google.com,epeev@google.com,etalvala@google.com,shuzhenwang@google.com,zhijunhe@google.com,jchowdhary@google.com
+per-file *Camera*=file:platform/frameworks/av:/camera/OWNERS
 
 # Sensor Privacy
 per-file *SensorPrivacy* = file:platform/frameworks/native:/libs/sensorprivacy/OWNERS
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index acd0d00f..16d9ef2 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -61,3 +61,10 @@
     description: "Allows system to provide keyboard specific key drawables and shortcuts via config files"
     bug: "345440920"
 }
+
+flag {
+    namespace: "input_native"
+    name: "keyboard_a11y_mouse_keys"
+    description: "Controls if the mouse keys accessibility feature for physical keyboard is available to the user"
+    bug: "341799888"
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index c954cdb..2562c8e 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -12324,6 +12324,18 @@
                 "accessibility_force_invert_color_enabled";
 
         /**
+         * Whether to enable mouse keys for Physical Keyboard accessibility.
+         *
+         * If set to true, key presses (of the mouse keys) on
+         * physical keyboard will control mouse pointer on the display.
+         *
+         * @hide
+         */
+        @Readable
+        public static final String ACCESSIBILITY_MOUSE_KEYS_ENABLED =
+                "accessibility_mouse_keys_enabled";
+
+        /**
          * Whether the Adaptive connectivity option is enabled.
          *
          * @hide
diff --git a/core/res/res/xml/power_profile.xml b/core/res/res/xml/power_profile.xml
index fc63657..f67ad3f 100644
--- a/core/res/res/xml/power_profile.xml
+++ b/core/res/res/xml/power_profile.xml
@@ -33,14 +33,14 @@
        There must be one of these for each display, labeled:
        ambient.on.display0, ambient.on.display1, etc...
 
-       Each display suffix number should match it's ordinal in its display device config.
+       Each display suffix number should match its ordinal in its display device config.
   -->
   <item name="ambient.on.display0">0.1</item>  <!-- ~100mA -->
   <!-- Average battery current draw of display0 while on without backlight.
        There must be one of these for each display, labeled:
        screen.on.display0, screen.on.display1, etc...
 
-       Each display suffix number should match it's ordinal in its display device config.
+       Each display suffix number should match its ordinal in its display device config.
   -->
   <item name="screen.on.display0">0.1</item>  <!-- ~100mA -->
   <!-- Average battery current draw of the backlight at full brightness.
@@ -50,7 +50,7 @@
        There must be one of these for each display, labeled:
        screen.full.display0, screen.full.display1, etc...
 
-       Each display suffix number should match it's ordinal in its display device config.
+       Each display suffix number should match its ordinal in its display device config.
   -->
   <item name="screen.full.display0">0.1</item>  <!-- ~100mA -->
 
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_open_in_browser.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_open_in_browser.xml
new file mode 100644
index 0000000..7d912a2
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_open_in_browser.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal">
+    <path android:fillColor="@android:color/black" android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,440Q80,407 103.5,383.5Q127,360 160,360L240,360L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,520Q880,553 856.5,576.5Q833,600 800,600L720,600L720,800Q720,833 696.5,856.5Q673,880 640,880L160,880ZM160,800L640,800Q640,800 640,800Q640,800 640,800L640,520L160,520L160,800Q160,800 160,800Q160,800 160,800ZM720,520L800,520Q800,520 800,520Q800,520 800,520L800,240L320,240L320,360L640,360Q673,360 696.5,383.5Q720,407 720,440L720,520Z"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index d5724cc..419d5c0 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -135,5 +135,24 @@
             android:drawableTint="?androidprv:attr/materialColorOnSurface"
             style="@style/DesktopModeHandleMenuActionButton"/>
     </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/open_in_browser_pill"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/desktop_mode_handle_menu_open_in_browser_pill_height"
+        android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin"
+        android:layout_marginStart="1dp"
+        android:orientation="vertical"
+        android:elevation="1dp"
+        android:background="@drawable/desktop_mode_decor_handle_menu_background">
+
+        <Button
+            android:id="@+id/open_in_browser_button"
+            android:contentDescription="@string/open_in_browser_text"
+            android:text="@string/open_in_browser_text"
+            android:drawableStart="@drawable/desktop_mode_ic_handle_menu_open_in_browser"
+            android:drawableTint="?androidprv:attr/materialColorOnSurface"
+            style="@style/DesktopModeHandleMenuActionButton"/>
+    </LinearLayout>
 </LinearLayout>
 
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 595d346..d143263 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -507,8 +507,11 @@
     <!-- The height of the handle menu's "More Actions" pill in desktop mode. -->
     <dimen name="desktop_mode_handle_menu_more_actions_pill_height">52dp</dimen>
 
+    <!-- The height of the handle menu's "Open in browser" pill in desktop mode. -->
+    <dimen name="desktop_mode_handle_menu_open_in_browser_pill_height">52dp</dimen>
+
     <!-- The height of the handle menu in desktop mode. -->
-    <dimen name="desktop_mode_handle_menu_height">328dp</dimen>
+    <dimen name="desktop_mode_handle_menu_height">380dp</dimen>
 
     <!-- The top margin of the handle menu in desktop mode. -->
     <dimen name="desktop_mode_handle_menu_margin_top">4dp</dimen>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 4784674..4e7cfb6 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -280,6 +280,8 @@
     <string name="select_text">Select</string>
     <!-- Accessibility text for the handle menu screenshot button [CHAR LIMIT=NONE] -->
     <string name="screenshot_text">Screenshot</string>
+    <!-- Accessibility text for the handle menu open in browser button [CHAR LIMIT=NONE] -->
+    <string name="open_in_browser_text">Open in browser</string>
     <!-- Accessibility text for the handle menu close button [CHAR LIMIT=NONE] -->
     <string name="close_text">Close</string>
     <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 1be33e5..faf6a62 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -22,6 +22,7 @@
 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;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_HOVER_ENTER;
@@ -41,12 +42,17 @@
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.ActivityTaskManager;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.hardware.input.InputManager;
+import android.net.Uri;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.RemoteException;
@@ -410,6 +416,26 @@
         decoration.closeMaximizeMenu();
     }
 
+    private void onOpenInBrowser(@NonNull DesktopModeWindowDecoration decor, @NonNull Uri uri) {
+        openInBrowser(uri);
+        decor.closeHandleMenu();
+        decor.closeMaximizeMenu();
+    }
+
+    private void openInBrowser(Uri uri) {
+        final Intent intent = new Intent(Intent.ACTION_VIEW, uri)
+                .setComponent(getDefaultBrowser())
+                .addFlags(FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+    }
+
+    private ComponentName getDefaultBrowser() {
+        final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://"));
+        final ResolveInfo info = mContext.getPackageManager()
+                .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
+        return info.getComponentInfo().getComponentName();
+    }
+
     private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
             implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
             View.OnGenericMotionListener, DragDetector.MotionEventHandler {
@@ -489,6 +515,10 @@
             } else if (id == R.id.split_screen_button) {
                 decoration.closeHandleMenu();
                 mDesktopTasksController.requestSplit(decoration.mTaskInfo);
+            } else if (id == R.id.open_in_browser_button) {
+                // TODO(b/346441962): let the decoration handle the click gesture and only call back
+                //  to the ViewModel via #setOpenInBrowserClickListener
+                decoration.onOpenInBrowserClick();
             } else if (id == R.id.collapse_menu_button) {
                 decoration.closeHandleMenu();
             } else if (id == R.id.maximize_window) {
@@ -1091,6 +1121,7 @@
         windowDecoration.setOnRightSnapClickListener((taskId, tag) -> {
             onSnapResize(taskId, false /* isLeft */);
         });
+        windowDecoration.setOpenInBrowserClickListener(this::onOpenInBrowser);
         windowDecoration.setCaptionListeners(
                 touchEventListener, touchEventListener, touchEventListener, touchEventListener);
         windowDecoration.setExclusionRegionListener(mExclusionRegionListener);
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 b62194c..5ffd883 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
@@ -44,6 +44,7 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.graphics.drawable.Drawable;
+import android.net.Uri;
 import android.os.Handler;
 import android.os.Trace;
 import android.util.Log;
@@ -88,6 +89,7 @@
  */
 public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
     private static final String TAG = "DesktopModeWindowDecoration";
+    private static final int CAPTURED_LINK_TIMEOUT_MS = 7000;
 
     @VisibleForTesting
     static final long CLOSE_MAXIMIZE_MENU_DELAY_MS = 150L;
@@ -124,6 +126,8 @@
     private Bitmap mResizeVeilBitmap;
 
     private CharSequence mAppName;
+    private CapturedLink mCapturedLink;
+    private OpenInBrowserClickListener mOpenInBrowserClickListener;
 
     private ExclusionRegionListener mExclusionRegionListener;
 
@@ -138,6 +142,7 @@
     // being hovered. There's a small delay after stopping the hover, to allow a quick reentry
     // to cancel the close.
     private final Runnable mCloseMaximizeWindowRunnable = this::closeMaximizeMenu;
+    private final Runnable mCapturedLinkExpiredRunnable = this::onCapturedLinkExpired;
 
     DesktopModeWindowDecoration(
             Context context,
@@ -153,8 +158,7 @@
                 handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer,
                 SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
                 WindowContainerTransaction::new, SurfaceControl::new,
-                new SurfaceControlViewHostFactory() {},
-                DefaultMaximizeMenuFactory.INSTANCE);
+                new SurfaceControlViewHostFactory() {}, DefaultMaximizeMenuFactory.INSTANCE);
     }
 
     DesktopModeWindowDecoration(
@@ -232,6 +236,10 @@
         mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop());
     }
 
+    void setOpenInBrowserClickListener(OpenInBrowserClickListener listener) {
+        mOpenInBrowserClickListener = listener;
+    }
+
     @Override
     void relayout(ActivityManager.RunningTaskInfo taskInfo) {
         final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
@@ -323,6 +331,11 @@
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
         Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces");
+
+        if (Flags.enableDesktopWindowingAppToWeb()) {
+            setCapturedLink(taskInfo.capturedLink, taskInfo.capturedLinkTimestamp);
+        }
+
         if (isHandleMenuActive()) {
             mHandleMenu.relayout(startT);
         }
@@ -367,6 +380,28 @@
         Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces
     }
 
+    private void setCapturedLink(Uri capturedLink, long timeStamp) {
+        if (capturedLink == null
+                || (mCapturedLink != null && mCapturedLink.mTimeStamp == timeStamp)) {
+            return;
+        }
+        mCapturedLink = new CapturedLink(capturedLink, timeStamp);
+        mHandler.postDelayed(mCapturedLinkExpiredRunnable, CAPTURED_LINK_TIMEOUT_MS);
+    }
+
+    private void onCapturedLinkExpired() {
+        mHandler.removeCallbacks(mCapturedLinkExpiredRunnable);
+        if (mCapturedLink != null) {
+            mCapturedLink.setExpired();
+        }
+    }
+
+    void onOpenInBrowserClick() {
+        if (mOpenInBrowserClickListener == null || mCapturedLink == null) return;
+        mOpenInBrowserClickListener.onClick(this, mCapturedLink.mUri);
+        onCapturedLinkExpired();
+    }
+
     private void updateDragResizeListener(SurfaceControl oldDecorationSurface) {
         if (!isDragResizable(mTaskInfo)) {
             if (!mTaskInfo.positionInParent.equals(mPositionInParent)) {
@@ -827,11 +862,17 @@
                 .setCaptionHeight(mResult.mCaptionHeight)
                 .setDisplayController(mDisplayController)
                 .setSplitScreenController(splitScreenController)
+                .setBrowserLinkAvailable(browserLinkAvailable())
                 .build();
         mWindowDecorViewHolder.onHandleMenuOpened();
         mHandleMenu.show();
     }
 
+    @VisibleForTesting
+    boolean browserLinkAvailable() {
+        return mCapturedLink != null && !mCapturedLink.mExpired;
+    }
+
     /**
      * Close the handle menu window.
      */
@@ -1121,6 +1162,31 @@
         }
     }
 
+    @VisibleForTesting
+    static class CapturedLink {
+        private final long mTimeStamp;
+        private final Uri mUri;
+        private boolean mExpired;
+
+        CapturedLink(@NonNull Uri uri, long timeStamp) {
+            mUri = uri;
+            mTimeStamp = timeStamp;
+            mExpired = false;
+        }
+
+        void setExpired() {
+            mExpired = true;
+        }
+    }
+
+
+    /** Listener for the handle menu's "Open in browser" button */
+    interface OpenInBrowserClickListener {
+
+        /** Inform the implementing class that the "Open in browser" button has been clicked */
+        void onClick(DesktopModeWindowDecoration decoration, Uri uri);
+    }
+
     interface ExclusionRegionListener {
         /** Inform the implementing class of this task's change in region resize handles */
         void onExclusionRegionChanged(int taskId, Region region);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
index df0836c..7e44f32 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
@@ -42,6 +42,7 @@
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.View;
+import android.widget.Button;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.TextView;
@@ -81,6 +82,7 @@
     // those as well.
     final Point mGlobalMenuPosition = new Point();
     private final boolean mShouldShowWindowingPill;
+    private final boolean mShouldShowBrowserPill;
     private final Bitmap mAppIconBitmap;
     private final CharSequence mAppName;
     private final View.OnClickListener mOnClickListener;
@@ -101,7 +103,7 @@
             View.OnClickListener onClickListener, View.OnTouchListener onTouchListener,
             Bitmap appIcon, CharSequence appName, DisplayController displayController,
             SplitScreenController splitScreenController, boolean shouldShowWindowingPill,
-            int captionHeight) {
+            boolean shouldShowBrowserPill, int captionHeight) {
         mParentDecor = parentDecor;
         mContext = mParentDecor.mDecorWindowContext;
         mTaskInfo = mParentDecor.mTaskInfo;
@@ -113,6 +115,7 @@
         mAppIconBitmap = appIcon;
         mAppName = appName;
         mShouldShowWindowingPill = shouldShowWindowingPill;
+        mShouldShowBrowserPill = shouldShowBrowserPill;
         mCaptionHeight = captionHeight;
         loadHandleMenuDimensions();
         updateHandleMenuPillPositions();
@@ -170,6 +173,7 @@
             setupWindowingPill(handleMenu);
         }
         setupMoreActionsPill(handleMenu);
+        setupOpenInBrowserPill(handleMenu);
     }
 
     /**
@@ -228,6 +232,15 @@
         }
     }
 
+    private void setupOpenInBrowserPill(View handleMenu) {
+        if (!mShouldShowBrowserPill) {
+            handleMenu.findViewById(R.id.open_in_browser_pill).setVisibility(View.GONE);
+            return;
+        }
+        final Button browserButton = handleMenu.findViewById(R.id.open_in_browser_button);
+        browserButton.setOnClickListener(mOnClickListener);
+    }
+
     /**
      * Returns array of windowing icon color based on current UI theme. First element of the
      * array is for inactive icons and the second is for active icons.
@@ -423,6 +436,10 @@
             menuHeight -= loadDimensionPixelSize(resources,
                     R.dimen.desktop_mode_handle_menu_more_actions_pill_height);
         }
+        if (!mShouldShowBrowserPill) {
+            menuHeight -= loadDimensionPixelSize(resources,
+                    R.dimen.desktop_mode_handle_menu_open_in_browser_pill_height);
+        }
         return menuHeight;
     }
 
@@ -457,6 +474,7 @@
         private int mCaptionHeight;
         private DisplayController mDisplayController;
         private SplitScreenController mSplitScreenController;
+        private boolean mShowBrowserPill;
 
         Builder(@NonNull DesktopModeWindowDecoration parent) {
             mParent = parent;
@@ -507,10 +525,15 @@
             return this;
         }
 
+        Builder setBrowserLinkAvailable(Boolean showBrowserPill) {
+            mShowBrowserPill = showBrowserPill;
+            return this;
+        }
+
         HandleMenu build() {
             return new HandleMenu(mParent, mLayoutId, mOnClickListener,
                     mOnTouchListener, mAppIcon, mName, mDisplayController, mSplitScreenController,
-                    mShowWindowingPill, mCaptionHeight);
+                    mShowWindowingPill, mShowBrowserPill, mCaptionHeight);
         }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
index 8c5d4a2..25a829b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
@@ -26,6 +26,7 @@
 import android.view.View.TRANSLATION_Y
 import android.view.View.TRANSLATION_Z
 import android.view.ViewGroup
+import android.widget.Button
 import androidx.core.animation.doOnEnd
 import androidx.core.view.children
 import com.android.wm.shell.R
@@ -72,6 +73,7 @@
     private val appInfoPill: ViewGroup = handleMenu.requireViewById(R.id.app_info_pill)
     private val windowingPill: ViewGroup = handleMenu.requireViewById(R.id.windowing_pill)
     private val moreActionsPill: ViewGroup = handleMenu.requireViewById(R.id.more_actions_pill)
+    private val openInBrowserPill: ViewGroup = handleMenu.requireViewById(R.id.open_in_browser_pill)
 
     /** Animates the opening of the handle menu. */
     fun animateOpen() {
@@ -80,6 +82,7 @@
         animateAppInfoPillOpen()
         animateWindowingPillOpen()
         animateMoreActionsPillOpen()
+        animateOpenInBrowserPill()
         runAnimations()
     }
 
@@ -94,6 +97,7 @@
         animateAppInfoPillOpen()
         animateWindowingPillOpen()
         animateMoreActionsPillOpen()
+        animateOpenInBrowserPill()
         runAnimations()
     }
 
@@ -109,6 +113,7 @@
         animateAppInfoPillFadeOut()
         windowingPillClose()
         moreActionsPillClose()
+        openInBrowserPillClose()
         runAnimations(after)
     }
 
@@ -125,6 +130,7 @@
         animateAppInfoPillFadeOut()
         windowingPillClose()
         moreActionsPillClose()
+        openInBrowserPillClose()
         runAnimations(after)
     }
 
@@ -137,6 +143,7 @@
         appInfoPill.children.forEach { it.alpha = 0f }
         windowingPill.alpha = 0f
         moreActionsPill.alpha = 0f
+        openInBrowserPill.alpha = 0f
 
         // Setup pivots.
         handleMenu.pivotX = menuWidth / 2f
@@ -147,6 +154,9 @@
 
         moreActionsPill.pivotX = menuWidth / 2f
         moreActionsPill.pivotY = appInfoPill.measuredHeight.toFloat()
+
+        openInBrowserPill.pivotX = menuWidth / 2f
+        openInBrowserPill.pivotY = appInfoPill.measuredHeight.toFloat()
     }
 
     private fun animateAppInfoPillOpen() {
@@ -268,12 +278,50 @@
         // More Actions Content Opacity Animation
         moreActionsPill.children.forEach {
             animators +=
-                ObjectAnimator.ofFloat(it, ALPHA, 1f).apply {
+                    ObjectAnimator.ofFloat(it, ALPHA, 1f).apply {
+                        startDelay = BODY_ALPHA_OPEN_DELAY
+                        duration = BODY_CONTENT_ALPHA_OPEN_DURATION
+                        interpolator = Interpolators.FAST_OUT_SLOW_IN
+                    }
+        }
+    }
+
+    private fun animateOpenInBrowserPill() {
+        // Open in Browser X & Y Scaling Animation
+        animators +=
+                ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
+                    startDelay = BODY_SCALE_OPEN_DELAY
+                    duration = BODY_SCALE_OPEN_DURATION
+                }
+
+        animators +=
+                ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
+                    startDelay = BODY_SCALE_OPEN_DELAY
+                    duration = BODY_SCALE_OPEN_DURATION
+                }
+
+        // Open in Browser Opacity Animation
+        animators +=
+                ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 1f).apply {
+                    startDelay = BODY_ALPHA_OPEN_DELAY
+                    duration = BODY_ALPHA_OPEN_DURATION
+                }
+
+        // Open in Browser Elevation Animation
+        animators +=
+                ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Z, 1f).apply {
+                    startDelay = ELEVATION_OPEN_DELAY
+                    duration = BODY_ELEVATION_OPEN_DURATION
+                }
+
+        // Open in Browser Button Opacity Animation
+        val button = openInBrowserPill.requireViewById<Button>(R.id.open_in_browser_button)
+        animators +=
+                ObjectAnimator.ofFloat(button, ALPHA, 1f).apply {
                     startDelay = BODY_ALPHA_OPEN_DELAY
                     duration = BODY_CONTENT_ALPHA_OPEN_DURATION
                     interpolator = Interpolators.FAST_OUT_SLOW_IN
                 }
-        }
     }
 
     private fun appInfoPillCollapse() {
@@ -379,6 +427,37 @@
             }
     }
 
+    private fun openInBrowserPillClose() {
+        // Open in Browser X & Y Scaling Animation
+        animators +=
+                ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE).apply {
+                    duration = BODY_CLOSE_DURATION
+                }
+
+        animators +=
+                ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE).apply {
+                    duration = BODY_CLOSE_DURATION
+                }
+
+        // Open in Browser Opacity Animation
+        animators +=
+                ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply {
+                    duration = BODY_CLOSE_DURATION
+                }
+
+        animators +=
+                ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply {
+                    duration = BODY_CLOSE_DURATION
+                }
+
+        // Upward Open in Browser y-translation Animation
+        val yStart: Float = -captionHeight / 2
+        animators +=
+                ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Y, yStart).apply {
+                    duration = BODY_CLOSE_DURATION
+                }
+    }
+
     /**
      * Runs the list of hide animators concurrently.
      *
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 b355137..d860609 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
@@ -31,6 +31,7 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
@@ -50,6 +51,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.PointF;
+import android.net.Uri;
 import android.os.Handler;
 import android.os.SystemProperties;
 import android.platform.test.annotations.DisableFlags;
@@ -118,6 +120,8 @@
     private static final String USE_ROUNDED_CORNERS_SYSPROP_KEY =
             "persist.wm.debug.desktop_use_rounded_corners";
 
+    private static final Uri TEST_URI = Uri.parse("www.google.com");
+
     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
 
     @Mock
@@ -150,6 +154,8 @@
     private PackageManager mMockPackageManager;
     @Mock
     private Handler mMockHandler;
+    @Mock
+    private DesktopModeWindowDecoration.OpenInBrowserClickListener mMockOpenInBrowserClickListener;
     @Captor
     private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener;
     @Captor
@@ -555,6 +561,65 @@
         verify(mMockHandler).removeCallbacks(any());
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
+    public void capturedLink_postsOnCapturedLinkExpiredRunnable() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
+        final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo);
+
+        decor.relayout(taskInfo);
+        // Assert captured link is set
+        assertTrue(decor.browserLinkAvailable());
+        // Asset runnable posted to set captured link to expired
+        verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong());
+        runnableArgument.getValue().run();
+        assertFalse(decor.browserLinkAvailable());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
+    public void capturedLink_capturedLinkNotResetToSameLink() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo);
+        final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
+
+        // Set captured link and run on captured link expired runnable
+        decor.relayout(taskInfo);
+        verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong());
+        runnableArgument.getValue().run();
+
+        decor.relayout(taskInfo);
+        // Assert captured link not set to same value twice
+        assertFalse(decor.browserLinkAvailable());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
+    public void capturedLink_capturedLinkExpiresAfterClick() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo);
+
+        decor.relayout(taskInfo);
+        // Assert captured link is set
+        assertTrue(decor.browserLinkAvailable());
+        decor.onOpenInBrowserClick();
+        //Assert Captured link expires after button is clicked
+        assertFalse(decor.browserLinkAvailable());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
+    public void capturedLink_openInBrowserListenerCalledOnClick() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo);
+
+        decor.relayout(taskInfo);
+        decor.onOpenInBrowserClick();
+
+        verify(mMockOpenInBrowserClickListener).onClick(any(), any());
+    }
+
     private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) {
         final OnTaskActionClickListener l = (taskId, tag) -> {};
         decoration.setOnMaximizeOrRestoreClickListener(l);
@@ -595,11 +660,11 @@
                 mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
                 SurfaceControl.Builder::new, mMockTransactionSupplier,
                 WindowContainerTransaction::new, SurfaceControl::new,
-                mMockSurfaceControlViewHostFactory,
-                maximizeMenuFactory);
+                mMockSurfaceControlViewHostFactory, maximizeMenuFactory);
         windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener,
                 mMockTouchEventListener, mMockTouchEventListener);
         windowDecor.setExclusionRegionListener(mMockExclusionRegionListener);
+        windowDecor.setOpenInBrowserClickListener(mMockOpenInBrowserClickListener);
         return windowDecor;
     }
 
@@ -615,6 +680,8 @@
                 "DesktopModeWindowDecorationTests");
         taskInfo.baseActivity = new ComponentName("com.android.wm.shell.windowdecor",
                 "DesktopModeWindowDecorationTests");
+        taskInfo.capturedLink = TEST_URI;
+        taskInfo.capturedLinkTimestamp = System.currentTimeMillis();
         return taskInfo;
 
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
index 5582e0f..0c50ab6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
@@ -196,9 +196,9 @@
             R.layout.desktop_mode_app_header
         }
         val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId,
-            onClickListener, onTouchListener, appIcon, appName, displayController,
-            splitScreenController, true /* shouldShowWindowingPill */,
-            50 /* captionHeight */ )
+                onClickListener, onTouchListener, appIcon, appName, displayController,
+                splitScreenController, true /* shouldShowWindowingPill */,
+                true /* shouldShowBrowserPill */, 50 /* captionHeight */)
         handleMenu.show()
         return handleMenu
     }
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index d4a4703..5f23651 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -228,6 +228,7 @@
         Settings.Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED,
         Settings.Secure.ACCESSIBILITY_MAGNIFICATION_JOYSTICK_ENABLED,
         Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED,
+        Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED,
         Settings.Secure.ACCESSIBILITY_PINCH_TO_ZOOM_ANYWHERE_ENABLED,
         Settings.Secure.ACCESSIBILITY_SINGLE_FINGER_PANNING_ENABLED,
         Settings.Secure.ODI_CAPTIONS_VOLUME_UI_ENABLED,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 6df1c45..c8da8af 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -439,6 +439,7 @@
         VALIDATORS.put(Secure.ON_DEVICE_INFERENCE_UNBIND_TIMEOUT_MS, ANY_LONG_VALIDATOR);
         VALIDATORS.put(Secure.ON_DEVICE_INTELLIGENCE_UNBIND_TIMEOUT_MS, ANY_LONG_VALIDATOR);
         VALIDATORS.put(Secure.ON_DEVICE_INTELLIGENCE_IDLE_TIMEOUT_MS, NONE_NEGATIVE_LONG_VALIDATOR);
+        VALIDATORS.put(Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.MANDATORY_BIOMETRICS, new InclusiveIntegerRangeValidator(0, 1));
     }
 }
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index a1f1a08..e2ecda3 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -591,13 +591,6 @@
 }
 
 flag {
-    name: "screenshot_shelf_ui2"
-    namespace: "systemui"
-    description: "Use new shelf UI flow for screenshots"
-    bug: "329659738"
-}
-
-flag {
    name: "run_fingerprint_detect_on_dismissible_keyguard"
    namespace: "systemui"
    description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible."
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index d046631..43d51c3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -28,7 +28,6 @@
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.scene.Back
-import com.android.compose.animation.scene.CommunalSwipeDetector
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index e2d693e..f6535ec0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -661,10 +661,7 @@
         val addWidgetText = stringResource(R.string.hub_mode_add_widget_button_text)
         ToolbarButton(
             isPrimary = !removeEnabled,
-            modifier =
-                Modifier.align(Alignment.CenterStart).semantics {
-                    contentDescription = addWidgetText
-                },
+            modifier = Modifier.align(Alignment.CenterStart),
             onClick = onOpenWidgetPicker,
         ) {
             Icon(Icons.Default.Add, null)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/systemui/communal/ui/compose/CommunalSwipeDetector.kt
similarity index 84%
rename from packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt
rename to packages/SystemUI/compose/scene/src/com/android/systemui/communal/ui/compose/CommunalSwipeDetector.kt
index 7be34ca..3fda9b8 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/systemui/communal/ui/compose/CommunalSwipeDetector.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.compose.animation.scene
+package com.android.systemui.communal.ui.compose
 
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.ui.input.pointer.PointerInputChange
@@ -22,10 +22,12 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.SwipeDetector
+import com.android.compose.animation.scene.SwipeSource
+import com.android.compose.animation.scene.SwipeSourceDetector
 import kotlin.math.abs
 
-private const val TRAVEL_RATIO_THRESHOLD = .5f
-
 /**
  * {@link CommunalSwipeDetector} provides an implementation of {@link SwipeDetector} and {@link
  * SwipeSourceDetector} to enable fullscreen swipe handling to transition to and from the glanceable
@@ -33,6 +35,10 @@
  */
 class CommunalSwipeDetector(private var lastDirection: SwipeSource? = null) :
     SwipeSourceDetector, SwipeDetector {
+    companion object {
+        private const val TRAVEL_RATIO_THRESHOLD = .5f
+    }
+
     override fun source(
         layoutSize: IntSize,
         position: IntOffset,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt
new file mode 100644
index 0000000..e4916b1
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.domain.interactor
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.devicePolicyManager
+import android.content.Intent
+import android.content.pm.UserInfo
+import android.os.UserManager
+import android.os.userManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.settings.fakeUserTracker
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalSettingsInteractorTest : SysuiTestCase() {
+
+    private lateinit var userManager: UserManager
+    private lateinit var userRepository: FakeUserRepository
+    private lateinit var userTracker: FakeUserTracker
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private lateinit var underTest: CommunalSettingsInteractor
+
+    @Before
+    fun setUp() {
+        userManager = kosmos.userManager
+        userRepository = kosmos.fakeUserRepository
+        userTracker = kosmos.fakeUserTracker
+
+        val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK)
+        userRepository.setUserInfos(userInfos)
+        userTracker.set(
+            userInfos = userInfos,
+            selectedUserIndex = 0,
+        )
+
+        underTest = kosmos.communalSettingsInteractor
+    }
+
+    @Test
+    fun filterUsers_dontFilteredUsersWhenAllAreAllowed() =
+        testScope.runTest {
+            // If no users have any keyguard features disabled...
+            val disallowedUser by
+                collectLastValue(underTest.workProfileUserDisallowedByDevicePolicy)
+            // ...then the disallowed user should be null
+            assertNull(disallowedUser)
+        }
+
+    @Test
+    fun filterUsers_filterWorkProfileUserWhenDisallowed() =
+        testScope.runTest {
+            // If the work profile user has keyguard widgets disabled...
+            setKeyguardFeaturesDisabled(
+                USER_INFO_WORK,
+                DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL
+            )
+            // ...then the disallowed user match the work profile
+            val disallowedUser by
+                collectLastValue(underTest.workProfileUserDisallowedByDevicePolicy)
+            assertNotNull(disallowedUser)
+            assertEquals(USER_INFO_WORK.id, disallowedUser!!.id)
+        }
+
+    private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) {
+        whenever(
+                kosmos.devicePolicyManager.getKeyguardDisabledFeatures(
+                    anyOrNull(),
+                    ArgumentMatchers.eq(user.id)
+                )
+            )
+            .thenReturn(disabledFlags)
+        kosmos.broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+            context,
+            Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED),
+        )
+    }
+
+    private companion object {
+        val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+        val USER_INFO_WORK =
+            UserInfo(
+                10,
+                "work",
+                /* iconPath= */ "",
+                /* flags= */ 0,
+                UserManager.USER_TYPE_PROFILE_MANAGED,
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.kt
new file mode 100644
index 0000000..1b704b4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.util.service
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.testKosmos
+import com.android.systemui.util.service.ObservableServiceConnection.DISCONNECT_REASON_DISCONNECTED
+import com.android.systemui.util.time.fakeSystemClock
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PersistentConnectionManagerTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+
+    private val fakeClock = kosmos.fakeSystemClock
+    private val fakeExecutor = kosmos.fakeExecutor
+
+    private class Proxy {
+        // Fake proxy class
+    }
+
+    private val connection: ObservableServiceConnection<Proxy> = mock()
+    private val observer: Observer = mock()
+
+    private val underTest: PersistentConnectionManager<Proxy> by lazy {
+        PersistentConnectionManager(
+            /* clock = */ fakeClock,
+            /* bgExecutor = */ fakeExecutor,
+            /* dumpManager = */ kosmos.dumpManager,
+            /* dumpsysName = */ DUMPSYS_NAME,
+            /* serviceConnection = */ connection,
+            /* maxReconnectAttempts = */ MAX_RETRIES,
+            /* baseReconnectDelayMs = */ RETRY_DELAY_MS,
+            /* minConnectionDurationMs = */ CONNECTION_MIN_DURATION_MS,
+            /* observer = */ observer
+        )
+    }
+
+    /** Validates initial connection. */
+    @Test
+    fun testConnect() {
+        underTest.start()
+        captureCallbackAndVerifyBind(connection).onConnected(connection, mock<Proxy>())
+    }
+
+    /** Ensures reconnection on disconnect. */
+    @Test
+    fun testExponentialRetryOnDisconnect() {
+        underTest.start()
+
+        // IF service is connected...
+        val captor = argumentCaptor<ObservableServiceConnection.Callback<Proxy>>()
+        verify(connection, times(1)).bind()
+        verify(connection).addCallback(captor.capture())
+        val callback = captor.lastValue
+        callback.onConnected(connection, mock<Proxy>())
+
+        // ...AND service becomes disconnected within CONNECTION_MIN_DURATION_MS
+        callback.onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)
+
+        // THEN verify we retry to bind after the retry delay. (RETRY #1)
+        verify(connection, times(1)).bind()
+        fakeClock.advanceTime(RETRY_DELAY_MS.toLong())
+        verify(connection, times(2)).bind()
+
+        // IF service becomes disconnected for a second time after first retry...
+        callback.onConnected(connection, mock<Proxy>())
+        callback.onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)
+
+        // THEN verify we retry after a longer delay of 2 * RETRY_DELAY_MS (RETRY #2)
+        fakeClock.advanceTime(RETRY_DELAY_MS.toLong())
+        verify(connection, times(2)).bind()
+        fakeClock.advanceTime(RETRY_DELAY_MS.toLong())
+        verify(connection, times(3)).bind()
+
+        // IF service becomes disconnected for a third time after the second retry...
+        callback.onConnected(connection, mock<Proxy>())
+        callback.onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)
+
+        // THEN verify we retry after a longer delay of 4 * RETRY_DELAY_MS (RETRY #3)
+        fakeClock.advanceTime(3 * RETRY_DELAY_MS.toLong())
+        verify(connection, times(3)).bind()
+        fakeClock.advanceTime(RETRY_DELAY_MS.toLong())
+        verify(connection, times(4)).bind()
+    }
+
+    @Test
+    fun testDoesNotRetryAfterMaxRetries() {
+        underTest.start()
+
+        val captor = argumentCaptor<ObservableServiceConnection.Callback<Proxy>>()
+        verify(connection).addCallback(captor.capture())
+        val callback = captor.lastValue
+
+        // IF we retry MAX_TRIES times...
+        for (attemptCount in 0 until MAX_RETRIES + 1) {
+            verify(connection, times(attemptCount + 1)).bind()
+            callback.onConnected(connection, mock<Proxy>())
+            callback.onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)
+            fakeClock.advanceTime(Math.scalb(RETRY_DELAY_MS.toDouble(), attemptCount).toLong())
+        }
+
+        // THEN we should not retry again after the last attempt.
+        fakeExecutor.advanceClockToLast()
+        verify(connection, times(MAX_RETRIES + 1)).bind()
+    }
+
+    @Test
+    fun testEnsureNoRetryIfServiceNeverConnectsAfterRetry() {
+        underTest.start()
+
+        with(captureCallbackAndVerifyBind(connection)) {
+            // IF service initially connects and then disconnects...
+            onConnected(connection, mock<Proxy>())
+            onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)
+            fakeExecutor.advanceClockToLast()
+            fakeExecutor.runAllReady()
+
+            // ...AND we retry once.
+            verify(connection, times(1)).bind()
+
+            // ...AND service disconnects after initial retry without ever connecting again.
+            onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)
+            fakeExecutor.advanceClockToLast()
+            fakeExecutor.runAllReady()
+
+            // THEN verify another retry is not triggered.
+            verify(connection, times(1)).bind()
+        }
+    }
+
+    @Test
+    fun testEnsureNoRetryIfServiceNeverInitiallyConnects() {
+        underTest.start()
+
+        with(captureCallbackAndVerifyBind(connection)) {
+            // IF service never connects and we just receive the disconnect signal...
+            onDisconnected(connection, DISCONNECT_REASON_DISCONNECTED)
+            fakeExecutor.advanceClockToLast()
+            fakeExecutor.runAllReady()
+
+            // THEN do not retry
+            verify(connection, never()).bind()
+        }
+    }
+
+    /** Ensures manual unbind does not reconnect. */
+    @Test
+    fun testStopDoesNotReconnect() {
+        underTest.start()
+
+        val connectionCallbackCaptor = argumentCaptor<ObservableServiceConnection.Callback<Proxy>>()
+        verify(connection).addCallback(connectionCallbackCaptor.capture())
+        verify(connection).bind()
+        clearInvocations(connection)
+
+        underTest.stop()
+        fakeExecutor.advanceClockToNext()
+        fakeExecutor.runAllReady()
+        verify(connection, never()).bind()
+    }
+
+    /** Ensures rebind on package change. */
+    @Test
+    fun testAttemptOnPackageChange() {
+        underTest.start()
+
+        verify(connection).bind()
+
+        val callbackCaptor = argumentCaptor<Observer.Callback>()
+        captureCallbackAndVerifyBind(connection).onConnected(connection, mock<Proxy>())
+
+        verify(observer).addCallback(callbackCaptor.capture())
+        callbackCaptor.lastValue.onSourceChanged()
+        verify(connection).bind()
+    }
+
+    @Test
+    fun testAddConnectionCallback() {
+        val connectionCallback: ObservableServiceConnection.Callback<Proxy> = mock()
+        underTest.addConnectionCallback(connectionCallback)
+        verify(connection).addCallback(connectionCallback)
+    }
+
+    @Test
+    fun testRemoveConnectionCallback() {
+        val connectionCallback: ObservableServiceConnection.Callback<Proxy> = mock()
+        underTest.removeConnectionCallback(connectionCallback)
+        verify(connection).removeCallback(connectionCallback)
+    }
+
+    /** Helper method to capture the [ObservableServiceConnection.Callback] */
+    private fun captureCallbackAndVerifyBind(
+        mConnection: ObservableServiceConnection<Proxy>,
+    ): ObservableServiceConnection.Callback<Proxy> {
+
+        val connectionCallbackCaptor = argumentCaptor<ObservableServiceConnection.Callback<Proxy>>()
+        verify(mConnection).addCallback(connectionCallbackCaptor.capture())
+        verify(mConnection).bind()
+        clearInvocations(mConnection)
+
+        return connectionCallbackCaptor.lastValue
+    }
+
+    companion object {
+        private const val MAX_RETRIES = 3
+        private const val RETRY_DELAY_MS = 1000
+        private const val CONNECTION_MIN_DURATION_MS = 5000
+        private const val DUMPSYS_NAME = "dumpsys_name"
+    }
+}
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index 2500769..65005f8 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -24,18 +24,29 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:contentDescription="@string/clipboard_overlay_window_name">
-    <ImageView
+    <!-- Min edge spacing guideline off of which the preview and actions can be anchored (without
+         this we'd need to express margins as the sum of two different dimens). -->
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/min_edge_guideline"
+        app:layout_constraintGuide_begin="@dimen/overlay_action_container_minimum_edge_spacing"
+        android:orientation="vertical"/>
+    <!-- Negative horizontal margin because this container background must render beyond the thing
+         it's constrained by (the actions themselves). -->
+    <FrameLayout
         android:id="@+id/actions_container_background"
         android:visibility="gone"
         android:layout_height="0dp"
         android:layout_width="0dp"
         android:elevation="4dp"
-        android:background="@drawable/action_chip_container_background"
-        android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
+        android:background="@drawable/shelf_action_chip_container_background"
+        android:layout_marginStart="@dimen/negative_overlay_action_container_minimum_edge_spacing"
+        android:layout_marginEnd="@dimen/negative_overlay_action_container_minimum_edge_spacing"
         android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="@+id/actions_container"
-        app:layout_constraintEnd_toEndOf="@+id/actions_container"
+        app:layout_constraintStart_toStartOf="@id/min_edge_guideline"
+        app:layout_constraintTop_toTopOf="@id/actions_container"
+        app:layout_constraintEnd_toEndOf="@id/actions_container"
         app:layout_constraintBottom_toBottomOf="parent"/>
     <HorizontalScrollView
         android:id="@+id/actions_container"
@@ -56,10 +67,13 @@
             android:id="@+id/actions"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:paddingStart="@dimen/shelf_action_chip_margin_start"
+            android:showDividers="middle"
+            android:divider="@drawable/shelf_action_chip_divider"
             android:animateLayoutChanges="true">
-            <include layout="@layout/overlay_action_chip"
+            <include layout="@layout/shelf_action_chip"
                      android:id="@+id/share_chip"/>
-            <include layout="@layout/overlay_action_chip"
+            <include layout="@layout/shelf_action_chip"
                      android:id="@+id/remote_copy_chip"/>
         </LinearLayout>
     </HorizontalScrollView>
@@ -73,7 +87,7 @@
         android:layout_marginBottom="@dimen/overlay_preview_container_margin"
         android:elevation="7dp"
         android:background="@drawable/overlay_border"
-        app:layout_constraintStart_toStartOf="@id/actions_container_background"
+        app:layout_constraintStart_toStartOf="@id/min_edge_guideline"
         app:layout_constraintTop_toTopOf="@id/clipboard_preview"
         app:layout_constraintEnd_toEndOf="@id/clipboard_preview"
         app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/>
diff --git a/packages/SystemUI/res/layout/clipboard_overlay2.xml b/packages/SystemUI/res/layout/clipboard_overlay2.xml
deleted file mode 100644
index 65005f8..0000000
--- a/packages/SystemUI/res/layout/clipboard_overlay2.xml
+++ /dev/null
@@ -1,202 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2021 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.
-  -->
-<com.android.systemui.clipboardoverlay.ClipboardOverlayView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/clipboard_ui"
-    android:theme="@style/FloatingOverlay"
-    android:alpha="0"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:contentDescription="@string/clipboard_overlay_window_name">
-    <!-- Min edge spacing guideline off of which the preview and actions can be anchored (without
-         this we'd need to express margins as the sum of two different dimens). -->
-    <androidx.constraintlayout.widget.Guideline
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:id="@+id/min_edge_guideline"
-        app:layout_constraintGuide_begin="@dimen/overlay_action_container_minimum_edge_spacing"
-        android:orientation="vertical"/>
-    <!-- Negative horizontal margin because this container background must render beyond the thing
-         it's constrained by (the actions themselves). -->
-    <FrameLayout
-        android:id="@+id/actions_container_background"
-        android:visibility="gone"
-        android:layout_height="0dp"
-        android:layout_width="0dp"
-        android:elevation="4dp"
-        android:background="@drawable/shelf_action_chip_container_background"
-        android:layout_marginStart="@dimen/negative_overlay_action_container_minimum_edge_spacing"
-        android:layout_marginEnd="@dimen/negative_overlay_action_container_minimum_edge_spacing"
-        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
-        app:layout_constraintStart_toStartOf="@id/min_edge_guideline"
-        app:layout_constraintTop_toTopOf="@id/actions_container"
-        app:layout_constraintEnd_toEndOf="@id/actions_container"
-        app:layout_constraintBottom_toBottomOf="parent"/>
-    <HorizontalScrollView
-        android:id="@+id/actions_container"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
-        android:paddingEnd="@dimen/overlay_action_container_padding_end"
-        android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
-        android:elevation="4dp"
-        android:scrollbars="none"
-        app:layout_constraintHorizontal_bias="0"
-        app:layout_constraintWidth_percent="1.0"
-        app:layout_constraintWidth_max="wrap"
-        app:layout_constraintStart_toEndOf="@+id/preview_border"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintBottom_toBottomOf="@id/actions_container_background">
-        <LinearLayout
-            android:id="@+id/actions"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:paddingStart="@dimen/shelf_action_chip_margin_start"
-            android:showDividers="middle"
-            android:divider="@drawable/shelf_action_chip_divider"
-            android:animateLayoutChanges="true">
-            <include layout="@layout/shelf_action_chip"
-                     android:id="@+id/share_chip"/>
-            <include layout="@layout/shelf_action_chip"
-                     android:id="@+id/remote_copy_chip"/>
-        </LinearLayout>
-    </HorizontalScrollView>
-    <View
-        android:id="@+id/preview_border"
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:layout_marginStart="@dimen/overlay_preview_container_margin"
-        android:layout_marginTop="@dimen/overlay_border_width_neg"
-        android:layout_marginEnd="@dimen/overlay_border_width_neg"
-        android:layout_marginBottom="@dimen/overlay_preview_container_margin"
-        android:elevation="7dp"
-        android:background="@drawable/overlay_border"
-        app:layout_constraintStart_toStartOf="@id/min_edge_guideline"
-        app:layout_constraintTop_toTopOf="@id/clipboard_preview"
-        app:layout_constraintEnd_toEndOf="@id/clipboard_preview"
-        app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/>
-    <FrameLayout
-        android:id="@+id/clipboard_preview"
-        android:layout_width="@dimen/clipboard_preview_size"
-        android:layout_height="wrap_content"
-        android:layout_marginStart="@dimen/overlay_border_width"
-        android:layout_marginBottom="@dimen/overlay_border_width"
-        android:layout_gravity="center"
-        android:elevation="7dp"
-        android:background="@drawable/overlay_preview_background"
-        android:clipChildren="true"
-        android:clipToOutline="true"
-        android:clipToPadding="true"
-        app:layout_constraintStart_toStartOf="@id/preview_border"
-        app:layout_constraintBottom_toBottomOf="@id/preview_border">
-        <TextView android:id="@+id/text_preview"
-                  android:textFontWeight="500"
-                  android:padding="8dp"
-                  android:gravity="center|start"
-                  android:ellipsize="end"
-                  android:autoSizeTextType="uniform"
-                  android:autoSizeMinTextSize="@dimen/clipboard_overlay_min_font"
-                  android:autoSizeMaxTextSize="@dimen/clipboard_overlay_max_font"
-                  android:textColor="?attr/overlayButtonTextColor"
-                  android:textColorLink="?attr/overlayButtonTextColor"
-                  android:background="?androidprv:attr/colorAccentSecondary"
-                  android:layout_width="@dimen/clipboard_preview_size"
-                  android:layout_height="@dimen/clipboard_preview_size"/>
-        <ImageView
-            android:id="@+id/image_preview"
-            android:scaleType="fitCenter"
-            android:adjustViewBounds="true"
-            android:contentDescription="@string/clipboard_image_preview"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"/>
-        <TextView
-            android:id="@+id/hidden_preview"
-            android:visibility="gone"
-            android:textFontWeight="500"
-            android:padding="8dp"
-            android:gravity="center"
-            android:textSize="14sp"
-            android:textColor="?attr/overlayButtonTextColor"
-            android:background="?androidprv:attr/colorAccentSecondary"
-            android:layout_width="@dimen/clipboard_preview_size"
-            android:layout_height="@dimen/clipboard_preview_size"/>
-    </FrameLayout>
-    <LinearLayout
-        android:id="@+id/minimized_preview"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:visibility="gone"
-        android:elevation="7dp"
-        android:padding="8dp"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
-        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
-        android:background="@drawable/clipboard_minimized_background">
-        <ImageView
-            android:src="@drawable/ic_content_paste"
-            android:tint="?attr/overlayButtonTextColor"
-            android:layout_width="24dp"
-            android:layout_height="24dp"/>
-        <ImageView
-            android:src="@*android:drawable/ic_chevron_end"
-            android:tint="?attr/overlayButtonTextColor"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
-            android:paddingEnd="-8dp"
-            android:paddingStart="-4dp"/>
-    </LinearLayout>
-    <androidx.constraintlayout.widget.Barrier
-        android:id="@+id/clipboard_content_top"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal"
-        app:barrierDirection="top"
-        app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
-    <androidx.constraintlayout.widget.Barrier
-        android:id="@+id/clipboard_content_end"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:orientation="vertical"
-        app:barrierDirection="end"
-        app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
-    <FrameLayout
-        android:id="@+id/dismiss_button"
-        android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
-        android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
-        android:elevation="10dp"
-        android:visibility="gone"
-        android:alpha="0"
-        app:layout_constraintStart_toEndOf="@id/clipboard_content_end"
-        app:layout_constraintEnd_toEndOf="@id/clipboard_content_end"
-        app:layout_constraintTop_toTopOf="@id/clipboard_content_top"
-        app:layout_constraintBottom_toTopOf="@id/clipboard_content_top"
-        android:contentDescription="@string/clipboard_dismiss_description">
-        <ImageView
-            android:id="@+id/dismiss_image"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_margin="@dimen/overlay_dismiss_button_margin"
-            android:background="@drawable/circular_background"
-            android:backgroundTint="?androidprv:attr/materialColorPrimaryFixedDim"
-            android:tint="?androidprv:attr/materialColorOnPrimaryFixed"
-            android:padding="4dp"
-            android:src="@drawable/ic_close"/>
-    </FrameLayout>
-</com.android.systemui.clipboardoverlay.ClipboardOverlayView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 0bc2c82..dee5528 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -321,11 +321,11 @@
     <!-- A toast message shown when the screen recording cannot be started due to a generic error [CHAR LIMIT=NONE] -->
     <string name="screenrecord_start_error">Error starting screen recording</string>
     <!-- Title for a dialog shown to the user that will let them stop recording their screen [CHAR LIMIT=50] -->
-    <string name="screenrecord_stop_dialog_title">Stop recording screen?</string>
-    <!-- Text telling a user that they will stop recording their screen if they click the "Stop recording" button [CHAR LIMIT=100] -->
-    <string name="screenrecord_stop_dialog_message">You will stop recording your screen</string>
-    <!-- Text telling a user that they will stop recording the contents of the specified [app_name] if they click the "Stop recording" button. Note that the app name will appear in bold. [CHAR LIMIT=100] -->
-    <string name="screenrecord_stop_dialog_message_specific_app">You will stop recording &lt;b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g>&lt;/b></string>
+    <string name="screenrecord_stop_dialog_title">Stop recording?</string>
+    <!-- Text telling a user that they're currently recording their screen [CHAR LIMIT=100] -->
+    <string name="screenrecord_stop_dialog_message">You\'re currently recording your entire screen</string>
+    <!-- Text telling a user that they're currently recording the contents of the specified [app_name]. [CHAR LIMIT=100] -->
+    <string name="screenrecord_stop_dialog_message_specific_app">You\'re currently recording <xliff:g id="app_name" example="Photos App">%1$s</xliff:g></string>
     <!-- Button to stop a screen recording [CHAR LIMIT=35] -->
     <string name="screenrecord_stop_dialog_button">Stop recording</string>
 
@@ -333,25 +333,33 @@
     <string name="share_to_app_chip_accessibility_label">Sharing screen</string>
     <!-- Title for a dialog shown to the user that will let them stop sharing their screen to another app on the device [CHAR LIMIT=50] -->
     <string name="share_to_app_stop_dialog_title">Stop sharing screen?</string>
-    <!-- Text telling a user that they will stop sharing their screen if they click the "Stop sharing" button [CHAR LIMIT=100] -->
-    <string name="share_to_app_stop_dialog_message">You will stop sharing your screen</string>
-    <!-- Text telling a user that they will stop sharing the contents of the specified [app_name] if they click the "Stop sharing" button. Note that the app name will appear in bold. [CHAR LIMIT=100] -->
-    <string name="share_to_app_stop_dialog_message_specific_app">You will stop sharing &lt;b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g>&lt;/b></string>
+    <!-- Text telling a user that they're currently sharing their entire screen to [host_app_name] (i.e. [host_app_name] can currently see all screen content) [CHAR LIMIT=150] -->
+    <string name="share_to_app_stop_dialog_message_entire_screen_with_host_app">You\'re currently sharing your entire screen with <xliff:g id="host_app_name" example="Screen Recorder App">%1$s</xliff:g></string>
+    <!-- Text telling a user that they're currently sharing their entire screen to an app (but we don't know what app) [CHAR LIMIT=150] -->
+    <string name="share_to_app_stop_dialog_message_entire_screen">You\'re currently sharing your entire screen with an app</string>
+    <!-- Text telling a user that they're currently sharing the contents of [app_being_shared_name]. (i.e. some app can currently see the content of [app_being_shared_name]). [CHAR LIMIT=150] -->
+    <string name="share_to_app_stop_dialog_message_single_app_specific">You\'re currently sharing <xliff:g id="app_being_shared_name" example="Photos App">%1$s</xliff:g></string>
+    <!-- Text telling a user that they're currently sharing their screen [CHAR LIMIT=150] -->
+    <string name="share_to_app_stop_dialog_message_single_app_generic">You\'re currently sharing an app</string>
     <!-- Button to stop screen sharing [CHAR LIMIT=35] -->
     <string name="share_to_app_stop_dialog_button">Stop sharing</string>
 
     <!-- Content description for the status bar chip shown to the user when they're casting their screen to a different device [CHAR LIMIT=NONE] -->
     <string name="cast_screen_to_other_device_chip_accessibility_label">Casting screen</string>
-    <!-- Title for a dialog shown to the user that will let them stop casting their screen to a different device [CHAR LIMIT=50] -->
-    <string name="cast_screen_to_other_device_stop_dialog_title">Stop casting screen?</string>
     <!-- Title for a dialog shown to the user that will let them stop casting to a different device [CHAR LIMIT=50] -->
     <string name="cast_to_other_device_stop_dialog_title">Stop casting?</string>
-    <!-- Text telling a user that they will stop casting their screen to a different device if they click the "Stop casting" button [CHAR LIMIT=100] -->
-    <string name="cast_screen_to_other_device_stop_dialog_message">You will stop casting your screen</string>
-    <!-- Text telling a user that they will stop casting the contents of the specified [app_name] to a different device if they click the "Stop casting" button. Note that the app name will appear in bold.  [CHAR LIMIT=100] -->
-    <string name="cast_screen_to_other_device_stop_dialog_message_specific_app">You will stop casting &lt;b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g>&lt;/b></string>
-    <!-- Text telling a user that they're currently casting to a different device [CHAR LIMIT=100] -->
-    <string name="cast_to_other_device_stop_dialog_message">You\'re currently casting</string>
+    <!-- Text telling a user that they're currently casting their screen to a different device. The device receiving the cast is named [device_name]. [CHAR LIMIT=150] -->
+    <string name="cast_to_other_device_stop_dialog_message_entire_screen_with_device">You\'re currently casting your entire screen to <xliff:g id="device_name" example="Living Room Device">%1$s</xliff:g></string>
+    <!-- Text telling a user that they're currently casting their screen to a nearby device. [CHAR LIMIT=150] -->
+    <string name="cast_to_other_device_stop_dialog_message_entire_screen">You\'re currently casting your entire screen to a nearby device</string>
+    <!-- Text telling a user that they're currently casting the contents of [app_being_shared_name] to a different device. The device receiving the cast is named [device_name]. [CHAR LIMIT=150] -->
+    <string name="cast_to_other_device_stop_dialog_message_specific_app_with_device">You\'re currently casting <xliff:g id="app_being_shared_name" example="Photos App">%1$s</xliff:g> to <xliff:g id="device_name" example="Living Room Device">%2$s</xliff:g></string>
+    <!-- Text telling a user that they're currently casting the contents of [app_being_shared_name] to a different device. [CHAR LIMIT=150] -->
+    <string name="cast_to_other_device_stop_dialog_message_specific_app">You\'re currently casting <xliff:g id="app_being_shared_name" example="Photos App">%1$s</xliff:g> to a nearby device</string>
+    <!-- Text telling a user that they're currently casting to a different device. The device receiving the cast is named [device_name]. [CHAR LIMIT=100] -->
+    <string name="cast_to_other_device_stop_dialog_message_generic_with_device">You\'re currently casting to <xliff:g id="device_name" example="Living Room Device">%1$s</xliff:g></string>
+    <!-- Text telling a user that they're currently casting to a nearby device [CHAR LIMIT=100] -->
+    <string name="cast_to_other_device_stop_dialog_message_generic">You\'re currently casting to a nearby device</string>
     <!-- Button to stop screen casting to a different device [CHAR LIMIT=35] -->
     <string name="cast_to_other_device_stop_dialog_button">Stop casting</string>
 
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
index ba236ba..1762d82 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
@@ -18,8 +18,6 @@
 
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
-import static com.android.systemui.Flags.screenshotShelfUi2;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
@@ -61,7 +59,6 @@
 import com.android.systemui.res.R;
 import com.android.systemui.screenshot.DraggableConstraintLayout;
 import com.android.systemui.screenshot.FloatingWindowUtil;
-import com.android.systemui.screenshot.OverlayActionChip;
 import com.android.systemui.screenshot.ui.binder.ActionButtonViewBinder;
 import com.android.systemui.screenshot.ui.viewmodel.ActionButtonAppearance;
 import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel;
@@ -152,66 +149,47 @@
     }
 
     private void bindDefaultActionChips() {
-        if (screenshotShelfUi2()) {
-            mActionButtonViewBinder.bind(mRemoteCopyChip,
-                    ActionButtonViewModel.Companion.withNextId(
-                            new ActionButtonAppearance(
-                                    Icon.createWithResource(mContext,
-                                            R.drawable.ic_baseline_devices_24).loadDrawable(
-                                            mContext),
-                                    null,
-                                    mContext.getString(R.string.clipboard_send_nearby_description),
-                                    true),
-                            new Function0<>() {
-                                @Override
-                                public Unit invoke() {
-                                    if (mClipboardCallbacks != null) {
-                                        mClipboardCallbacks.onRemoteCopyButtonTapped();
-                                    }
-                                    return null;
+        mActionButtonViewBinder.bind(mRemoteCopyChip,
+                ActionButtonViewModel.Companion.withNextId(
+                        new ActionButtonAppearance(
+                                Icon.createWithResource(mContext,
+                                        R.drawable.ic_baseline_devices_24).loadDrawable(
+                                        mContext),
+                                null,
+                                mContext.getString(R.string.clipboard_send_nearby_description),
+                                true),
+                        new Function0<>() {
+                            @Override
+                            public Unit invoke() {
+                                if (mClipboardCallbacks != null) {
+                                    mClipboardCallbacks.onRemoteCopyButtonTapped();
                                 }
-                            }));
-            mActionButtonViewBinder.bind(mShareChip,
-                    ActionButtonViewModel.Companion.withNextId(
-                            new ActionButtonAppearance(
-                                    Icon.createWithResource(mContext,
-                                            R.drawable.ic_screenshot_share).loadDrawable(mContext),
-                                    null,
-                                    mContext.getString(com.android.internal.R.string.share),
-                                    true),
-                            new Function0<>() {
-                                @Override
-                                public Unit invoke() {
-                                    if (mClipboardCallbacks != null) {
-                                        mClipboardCallbacks.onShareButtonTapped();
-                                    }
-                                    return null;
+                                return null;
+                            }
+                        }));
+        mActionButtonViewBinder.bind(mShareChip,
+                ActionButtonViewModel.Companion.withNextId(
+                        new ActionButtonAppearance(
+                                Icon.createWithResource(mContext,
+                                        R.drawable.ic_screenshot_share).loadDrawable(mContext),
+                                null,
+                                mContext.getString(com.android.internal.R.string.share),
+                                true),
+                        new Function0<>() {
+                            @Override
+                            public Unit invoke() {
+                                if (mClipboardCallbacks != null) {
+                                    mClipboardCallbacks.onShareButtonTapped();
                                 }
-                            }));
-        } else {
-            mShareChip.setAlpha(1);
-            mRemoteCopyChip.setAlpha(1);
-
-            ((ImageView) mRemoteCopyChip.findViewById(R.id.overlay_action_chip_icon)).setImageIcon(
-                    Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24));
-            ((ImageView) mShareChip.findViewById(R.id.overlay_action_chip_icon)).setImageIcon(
-                    Icon.createWithResource(mContext, R.drawable.ic_screenshot_share));
-
-            mShareChip.setContentDescription(
-                    mContext.getString(com.android.internal.R.string.share));
-            mRemoteCopyChip.setContentDescription(
-                    mContext.getString(R.string.clipboard_send_nearby_description));
-        }
+                                return null;
+                            }
+                        }));
     }
 
     @Override
     public void setCallbacks(SwipeDismissCallbacks callbacks) {
         super.setCallbacks(callbacks);
         ClipboardOverlayCallbacks clipboardCallbacks = (ClipboardOverlayCallbacks) callbacks;
-        if (!screenshotShelfUi2()) {
-            mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped());
-            mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped());
-        }
         mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped());
         mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped());
         mMinimizedPreview.setOnClickListener(v -> clipboardCallbacks.onMinimizedViewTapped());
@@ -495,12 +473,7 @@
 
     void setActionChip(RemoteAction action, Runnable onFinish) {
         mActionContainerBackground.setVisibility(View.VISIBLE);
-        View chip;
-        if (screenshotShelfUi2()) {
-            chip = constructShelfActionChip(action, onFinish);
-        } else {
-            chip = constructActionChip(action, onFinish);
-        }
+        View chip = constructShelfActionChip(action, onFinish);
         mActionContainer.addView(chip);
         mActionChips.add(chip);
     }
@@ -534,17 +507,6 @@
         return chip;
     }
 
-    private OverlayActionChip constructActionChip(RemoteAction action, Runnable onFinish) {
-        OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate(
-                R.layout.overlay_action_chip, mActionContainer, false);
-        chip.setText(action.getTitle());
-        chip.setContentDescription(action.getTitle());
-        chip.setIcon(action.getIcon(), false);
-        chip.setPendingIntent(action.getActionIntent(), onFinish);
-        chip.setAlpha(1);
-        return chip;
-    }
-
     private static void updateTextSize(CharSequence text, TextView textView) {
         Paint paint = new Paint(textView.getPaint());
         Resources res = textView.getResources();
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
index 740a93e..ff9fba4 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
@@ -18,8 +18,6 @@
 
 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
-import static com.android.systemui.Flags.screenshotShelfUi2;
-
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import android.content.Context;
@@ -59,13 +57,8 @@
      */
     @Provides
     static ClipboardOverlayView provideClipboardOverlayView(@OverlayWindowContext Context context) {
-        if (screenshotShelfUi2()) {
-            return (ClipboardOverlayView) LayoutInflater.from(context).inflate(
-                    R.layout.clipboard_overlay2, null);
-        } else {
-            return (ClipboardOverlayView) LayoutInflater.from(context).inflate(
-                    R.layout.clipboard_overlay, null);
-        }
+        return (ClipboardOverlayView) LayoutInflater.from(context).inflate(
+                R.layout.clipboard_overlay, null);
     }
 
     @Qualifier
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 f5255ac..86f5fe1 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
@@ -20,6 +20,7 @@
 import android.content.ComponentName
 import android.content.Intent
 import android.content.IntentFilter
+import android.content.pm.UserInfo
 import android.os.UserHandle
 import android.os.UserManager
 import android.provider.Settings
@@ -386,11 +387,11 @@
         combine(
             widgetRepository.communalWidgets
                 .map { filterWidgetsByExistingUsers(it) }
-                .combine(communalSettingsInteractor.allowedByDevicePolicyForWorkProfile) {
+                .combine(communalSettingsInteractor.workProfileUserDisallowedByDevicePolicy) {
                     // exclude widgets under work profile if not allowed by device policy
                     widgets,
-                    allowedForWorkProfile ->
-                    filterWidgetsAllowedByDevicePolicy(widgets, allowedForWorkProfile)
+                    disallowedByPolicyUser ->
+                    filterWidgetsAllowedByDevicePolicy(widgets, disallowedByPolicyUser)
                 },
             updateOnWorkProfileBroadcastReceived,
         ) { widgets, _ ->
@@ -418,13 +419,11 @@
     /** Filter widgets based on whether their associated profile is allowed by device policy. */
     private fun filterWidgetsAllowedByDevicePolicy(
         list: List<CommunalWidgetContentModel>,
-        allowedByDevicePolicyForWorkProfile: Boolean
+        disallowedByDevicePolicyUser: UserInfo?
     ): List<CommunalWidgetContentModel> =
-        if (allowedByDevicePolicyForWorkProfile) {
+        if (disallowedByDevicePolicyUser == null) {
             list
         } else {
-            // Get associated work profile for the currently selected user.
-            val workProfile = userTracker.userProfiles.find { it.isManagedProfile }
             list.filter { model ->
                 val uid =
                     when (model) {
@@ -432,7 +431,7 @@
                             model.providerInfo.profile.identifier
                         is CommunalWidgetContentModel.Pending -> model.user.identifier
                     }
-                uid != workProfile?.id
+                uid != disallowedByDevicePolicyUser.id
             }
         }
 
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 47b75c4..3b01aec 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
@@ -92,15 +92,22 @@
         awaitClose { userTracker.removeCallback(callback) }
     }
 
-    /** Whether or not keyguard widgets are allowed for work profile by device policy manager. */
-    val allowedByDevicePolicyForWorkProfile: StateFlow<Boolean> =
+    /**
+     * A user that device policy says shouldn't allow communal widgets, or null if there are no
+     * restrictions.
+     */
+    val workProfileUserDisallowedByDevicePolicy: StateFlow<UserInfo?> =
         workProfileUserInfoCallbackFlow
             .flatMapLatest { workProfile ->
-                workProfile?.let { repository.getAllowedByDevicePolicy(it) } ?: flowOf(false)
+                workProfile?.let {
+                    repository.getAllowedByDevicePolicy(it).map { allowed ->
+                        if (!allowed) it else null
+                    }
+                } ?: flowOf(null)
             }
             .stateIn(
                 scope = bgScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue = false
+                initialValue = null
             )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 4f54fee..18b343e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -186,6 +186,10 @@
                 AppWidgetManager.EXTRA_CATEGORY_FILTER,
                 CommunalWidgetCategories.defaultCategories
             )
+
+            communalSettingsInteractor.workProfileUserDisallowedByDevicePolicy.value?.let {
+                putExtra(EXTRA_USER_ID_FILTER, arrayListOf(it.id))
+            }
             putExtra(EXTRA_UI_SURFACE_KEY, EXTRA_UI_SURFACE_VALUE)
             putExtra(EXTRA_PICKER_TITLE, resources.getString(R.string.communal_widget_picker_title))
             putExtra(
@@ -223,6 +227,7 @@
         private const val EXTRA_PICKER_DESCRIPTION = "picker_description"
         private const val EXTRA_UI_SURFACE_KEY = "ui_surface"
         private const val EXTRA_UI_SURFACE_VALUE = "widgets_hub"
+        private const val EXTRA_USER_ID_FILTER = "filtered_user_ids"
         const val EXTRA_ADDED_APP_WIDGETS_KEY = "added_app_widgets"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
index 297ad84..befd822 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
@@ -161,5 +161,6 @@
             TASK_FRAGMENT_TRANSIT_CLOSE,
             false
         )
+        organizer.unregisterOrganizer()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 608e25a..33f9209 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.biometrics.ui.binder.DeviceEntryUnlockTrackerViewBinder
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
@@ -73,6 +74,7 @@
 import dagger.Lazy
 import java.util.Optional
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
@@ -108,6 +110,7 @@
     private val clockInteractor: KeyguardClockInteractor,
     private val keyguardViewMediator: KeyguardViewMediator,
     private val deviceEntryUnlockTrackerViewBinder: Optional<DeviceEntryUnlockTrackerViewBinder>,
+    @Main private val mainDispatcher: CoroutineDispatcher,
 ) : CoreStartable {
 
     private var rootViewHandle: DisposableHandle? = null
@@ -215,6 +218,7 @@
                 vibratorHelper,
                 falsingManager,
                 keyguardViewMediator,
+                mainDispatcher,
             )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 859326a..ec03a6d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -52,7 +52,6 @@
 import com.android.systemui.statusbar.notification.NotificationUtils.interpolate
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
-import com.android.systemui.util.kotlin.Utils.Companion.sampleFilter
 import com.android.systemui.util.kotlin.pairwise
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
@@ -251,13 +250,17 @@
 
     /** Keyguard can be clipped at the top as the shade is dragged */
     val topClippingBounds: Flow<Int?> by lazy {
-        repository.topClippingBounds
-            .sampleFilter(
+        combineTransform(
                 keyguardTransitionInteractor
                     .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE)
-                    .onStart { emit(0f) }
-            ) { goneValue ->
-                goneValue != 1f
+                    .map { it == 1f }
+                    .onStart { emit(false) }
+                    .distinctUntilChanged(),
+                repository.topClippingBounds
+            ) { isGone, topClippingBounds ->
+                if (!isGone) {
+                    emit(topClippingBounds)
+                }
             }
             .distinctUntilChanged()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 8f149fb..f96f053 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -37,6 +37,7 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.animation.Interpolators
+import com.android.app.tracing.coroutines.launch
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
 import com.android.systemui.Flags.newAodTransition
@@ -80,6 +81,7 @@
 import com.android.systemui.util.ui.stopAnimating
 import com.android.systemui.util.ui.value
 import kotlin.math.min
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.coroutineScope
@@ -110,6 +112,7 @@
         vibratorHelper: VibratorHelper?,
         falsingManager: FalsingManager?,
         keyguardViewMediator: KeyguardViewMediator?,
+        mainImmediateDispatcher: CoroutineDispatcher,
     ): DisposableHandle {
         val disposables = DisposableHandles()
         val childViews = mutableMapOf<Int, View>()
@@ -128,6 +131,30 @@
 
         val burnInParams = MutableStateFlow(BurnInParameters())
         val viewState = ViewStateAccessor(alpha = { view.alpha })
+
+        disposables +=
+            view.repeatWhenAttached(mainImmediateDispatcher) {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    if (MigrateClocksToBlueprint.isEnabled) {
+                        launch("$TAG#topClippingBounds") {
+                            val clipBounds = Rect()
+                            viewModel.topClippingBounds.collect { clipTop ->
+                                if (clipTop == null) {
+                                    view.setClipBounds(null)
+                                } else {
+                                    clipBounds.apply {
+                                        top = clipTop
+                                        left = view.getLeft()
+                                        right = view.getRight()
+                                        bottom = view.getBottom()
+                                    }
+                                    view.setClipBounds(clipBounds)
+                                }
+                            }
+                        }
+                    }
+                }
+            }
         disposables +=
             view.repeatWhenAttached {
                 repeatOnLifecycle(Lifecycle.State.CREATED) {
@@ -192,23 +219,6 @@
                         }
 
                         launch {
-                            val clipBounds = Rect()
-                            viewModel.topClippingBounds.collect { clipTop ->
-                                if (clipTop == null) {
-                                    view.setClipBounds(null)
-                                } else {
-                                    clipBounds.apply {
-                                        top = clipTop
-                                        left = view.getLeft()
-                                        right = view.getRight()
-                                        bottom = view.getBottom()
-                                    }
-                                    view.setClipBounds(clipBounds)
-                                }
-                            }
-                        }
-
-                        launch {
                             viewModel.lockscreenStateAlpha(viewState).collect { alpha ->
                                 childViews[statusViewId]?.alpha = alpha
                             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 4f0ac42..bc5b7b9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -394,6 +394,7 @@
                     null, // device entry haptics not required for preview mode
                     null, // falsing manager not required for preview mode
                     null, // keyguard view mediator is not required for preview mode
+                    mainDispatcher,
                 )
         }
         rootView.addView(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
index 0bb4cfa..127ef84 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
@@ -22,6 +22,7 @@
 import android.service.quicksettings.Tile.STATE_ACTIVE
 import android.service.quicksettings.Tile.STATE_INACTIVE
 import android.text.TextUtils
+import android.util.Log
 import androidx.appcompat.content.res.AppCompatResources
 import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
 import androidx.compose.animation.graphics.res.animatedVectorResource
@@ -81,7 +82,6 @@
 import com.android.systemui.common.ui.compose.load
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.toUiState
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
@@ -91,7 +91,6 @@
 import java.util.function.Supplier
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.mapLatest
 
 object TileType
 
@@ -103,29 +102,27 @@
     showLabels: Boolean = false,
     modifier: Modifier,
 ) {
-    val state: TileUiState by
-        tile.state
-            .mapLatest { it.toUiState() }
-            .collectAsStateWithLifecycle(tile.currentState.toUiState())
-    val colors = TileDefaults.getColorForState(state.state)
+    val state by tile.state.collectAsStateWithLifecycle(tile.currentState)
+    val uiState = remember(state) { state.toUiState() }
+    val colors = TileDefaults.getColorForState(uiState.state)
 
     TileContainer(
         colors = colors,
         showLabels = showLabels,
-        label = state.label.toString(),
+        label = uiState.label,
         iconOnly = iconOnly,
         clickEnabled = true,
         onClick = tile::onClick,
         onLongClick = tile::onLongClick,
         modifier = modifier,
     ) {
-        val icon = getTileIcon(icon = state.icon)
+        val icon = getTileIcon(icon = uiState.icon)
         if (iconOnly) {
             TileIcon(icon = icon, color = colors.icon, modifier = Modifier.align(Alignment.Center))
         } else {
             LargeTileContent(
-                label = state.label.toString(),
-                secondaryLabel = state.secondaryLabel.toString(),
+                label = uiState.label,
+                secondaryLabel = uiState.secondaryLabel,
                 icon = icon,
                 colors = colors,
                 clickEnabled = true,
@@ -234,19 +231,26 @@
             Text(
                 label,
                 color = colors.label,
-                modifier = Modifier.basicMarquee(),
+                modifier = Modifier.tileMarquee(),
             )
             if (!TextUtils.isEmpty(secondaryLabel)) {
                 Text(
                     secondaryLabel ?: "",
                     color = colors.secondaryLabel,
-                    modifier = Modifier.basicMarquee(),
+                    modifier = Modifier.tileMarquee(),
                 )
             }
         }
     }
 }
 
+private fun Modifier.tileMarquee(): Modifier {
+    return basicMarquee(
+        iterations = 1,
+        initialDelayMillis = 200,
+    )
+}
+
 @Composable
 fun TileLazyGrid(
     modifier: Modifier = Modifier,
@@ -452,6 +456,7 @@
     animateToEnd: Boolean = false,
     modifier: Modifier = Modifier,
 ) {
+    Log.d("Fabian", "Recomposing tile icon")
     val iconModifier = modifier.size(dimensionResource(id = R.dimen.qs_icon_size))
     val context = LocalContext.current
     val loadedDrawable =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
index b3acace..bb00494 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
@@ -26,8 +26,8 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.mapLatest
@@ -54,14 +54,24 @@
             quickQuickSettingsRowInteractor.defaultRows
         )
 
-    val tileViewModels: Flow<List<SizedTile<TileViewModel>>> =
-        columns.flatMapLatest { columns ->
-            tilesInteractor.currentTiles.combine(rows, ::Pair).mapLatest { (tiles, rows) ->
-                tiles
-                    .map { SizedTile(TileViewModel(it.tile, it.spec), it.spec.width) }
-                    .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() }
+    val tileViewModels: StateFlow<List<SizedTile<TileViewModel>>> =
+        columns
+            .flatMapLatest { columns ->
+                tilesInteractor.currentTiles.combine(rows, ::Pair).mapLatest { (tiles, rows) ->
+                    tiles
+                        .map { SizedTile(TileViewModel(it.tile, it.spec), it.spec.width) }
+                        .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() }
+                }
             }
-        }
+            .stateIn(
+                applicationScope,
+                SharingStarted.WhileSubscribed(),
+                tilesInteractor.currentTiles.value
+                    .map { SizedTile(TileViewModel(it.tile, it.spec), it.spec.width) }
+                    .let {
+                        splitInRowsSequence(it, columns.value).take(rows.value).toList().flatten()
+                    }
+            )
 
     private val TileSpec.width: Int
         get() = if (iconTilesViewModel.isIconTile(this)) 1 else 2
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
index 578a292..4ec59c9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
@@ -16,20 +16,22 @@
 
 package com.android.systemui.qs.panels.ui.viewmodel
 
+import androidx.compose.runtime.Immutable
 import com.android.systemui.plugins.qs.QSTile
 import java.util.function.Supplier
 
+@Immutable
 data class TileUiState(
-    val label: CharSequence,
-    val secondaryLabel: CharSequence,
+    val label: String,
+    val secondaryLabel: String,
     val state: Int,
     val icon: Supplier<QSTile.Icon>,
 )
 
 fun QSTile.State.toUiState(): TileUiState {
     return TileUiState(
-        label ?: "",
-        secondaryLabel ?: "",
+        label?.toString() ?: "",
+        secondaryLabel?.toString() ?: "",
         state,
         icon?.let { Supplier { icon } } ?: iconSupplier,
     )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt
index 7505b90..8578bb0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.panels.ui.viewmodel
 
+import androidx.compose.runtime.Immutable
 import com.android.systemui.animation.Expandable
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.qs.pipeline.shared.TileSpec
@@ -25,6 +26,7 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.onStart
 
+@Immutable
 class TileViewModel(private val tile: QSTile, val spec: TileSpec) {
     val state: Flow<QSTile.State> =
         conflatedCallbackFlow {
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
index bbf4e51..8a51ad4 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
@@ -87,7 +87,10 @@
             setNegativeButton(R.string.cancel) { _, _ -> }
             setPositiveButton(R.string.qs_record_issue_start) { _, _ -> onStarted.run() }
         }
-        bgExecutor.execute { traceurMessageSender.bindToTraceur(dialog.context) }
+        bgExecutor.execute {
+            traceurMessageSender.onBoundToTraceur.add { traceurMessageSender.getTags() }
+            traceurMessageSender.bindToTraceur(dialog.context)
+        }
     }
 
     override fun createDialog(): SystemUIDialog = factory.create(this)
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt
index 903d662..a31a9ef 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurMessageSender.kt
@@ -45,11 +45,15 @@
     private var binder: Messenger? = null
     private var isBound: Boolean = false
 
+    val onBoundToTraceur = mutableListOf<Runnable>()
+
     private val traceurConnection =
         object : ServiceConnection {
             override fun onServiceConnected(className: ComponentName, service: IBinder) {
                 binder = Messenger(service)
                 isBound = true
+                onBoundToTraceur.forEach(Runnable::run)
+                onBoundToTraceur.clear()
             }
 
             override fun onServiceDisconnected(className: ComponentName) {
@@ -103,11 +107,17 @@
 
     @WorkerThread
     fun shareTraces(context: Context, screenRecord: Uri?) {
-        val replyHandler = Messenger(TraceurMessageHandler(context, screenRecord, backgroundLooper))
+        val replyHandler = Messenger(ShareFilesHandler(context, screenRecord, backgroundLooper))
         notifyTraceur(MessageConstants.SHARE_WHAT, replyTo = replyHandler)
     }
 
     @WorkerThread
+    fun getTags() {
+        val replyHandler = Messenger(TagsHandler(backgroundLooper))
+        notifyTraceur(MessageConstants.TAGS_WHAT, replyTo = replyHandler)
+    }
+
+    @WorkerThread
     private fun notifyTraceur(what: Int, data: Bundle = Bundle(), replyTo: Messenger? = null) {
         try {
             binder!!.send(
@@ -122,7 +132,7 @@
         }
     }
 
-    private class TraceurMessageHandler(
+    private class ShareFilesHandler(
         private val context: Context,
         private val screenRecord: Uri?,
         looper: Looper,
@@ -154,4 +164,29 @@
             context.startActivity(fileSharingIntent)
         }
     }
+
+    private class TagsHandler(looper: Looper) : Handler(looper) {
+
+        override fun handleMessage(msg: Message) {
+            if (MessageConstants.TAGS_WHAT == msg.what) {
+                val keys = msg.data.getStringArrayList(MessageConstants.BUNDLE_KEY_TAGS)
+                val values =
+                    msg.data.getStringArrayList(MessageConstants.BUNDLE_KEY_TAG_DESCRIPTIONS)
+                if (keys == null || values == null) {
+                    throw IllegalArgumentException(
+                        "Neither keys: $keys, nor values: $values can " + "be null"
+                    )
+                }
+
+                val tags = keys.zip(values).map { "${it.first}: ${it.second}" }.toSet()
+                Log.e(
+                    TAG,
+                    "These tags: $tags will be saved and used for the Custom Trace" +
+                        " Config dialog in a future CL. This log will be removed."
+                )
+            } else {
+                throw IllegalArgumentException("received unknown msg.what: " + msg.what)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index c87b1f5..95ee2e0 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -20,7 +20,6 @@
 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
 import static com.android.systemui.Flags.screenshotPrivateProfileAccessibilityAnnouncementFix;
-import static com.android.systemui.Flags.screenshotShelfUi2;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT;
@@ -407,22 +406,16 @@
         }
 
         final UUID requestId;
-        if (screenshotShelfUi2()) {
-            requestId = mActionsController.setCurrentScreenshot(screenshot);
-            saveScreenshotInBackground(screenshot, requestId, finisher);
+        requestId = mActionsController.setCurrentScreenshot(screenshot);
+        saveScreenshotInBackground(screenshot, requestId, finisher);
 
-            if (screenshot.getTaskId() >= 0) {
-                mAssistContentRequester.requestAssistContent(
-                        screenshot.getTaskId(),
-                        assistContent ->
-                                mActionsController.onAssistContent(requestId, assistContent));
-            } else {
-                mActionsController.onAssistContent(requestId, null);
-            }
+        if (screenshot.getTaskId() >= 0) {
+            mAssistContentRequester.requestAssistContent(
+                    screenshot.getTaskId(),
+                    assistContent ->
+                            mActionsController.onAssistContent(requestId, assistContent));
         } else {
-            requestId = UUID.randomUUID(); // passed through but unused for legacy UI
-            saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher,
-                    this::showUiOnActionsReady, this::showUiOnQuickShareActionReady);
+            mActionsController.onAssistContent(requestId, null);
         }
 
         // The window is focusable by default
@@ -458,9 +451,6 @@
         // ignore system bar insets for the purpose of window layout
         mWindow.getDecorView().setOnApplyWindowInsetsListener(
                 (v, insets) -> WindowInsets.CONSUMED);
-        if (!screenshotShelfUi2()) {
-            mScreenshotHandler.cancelTimeout(); // restarted after animation
-        }
     }
 
     private boolean shouldShowUi() {
@@ -515,11 +505,7 @@
     }
 
     boolean isPendingSharedTransition() {
-        if (screenshotShelfUi2()) {
-            return mActionExecutor.isPendingSharedTransition();
-        } else {
-            return mViewProxy.isPendingSharedTransition();
-        }
+        return mActionExecutor.isPendingSharedTransition();
     }
 
     // Any cleanup needed when the service is being destroyed.
@@ -603,11 +589,7 @@
                             if (mConfigChanges.applyNewConfig(mContext.getResources())) {
                                 // Hide the scroll chip until we know it's available in this
                                 // orientation
-                                if (screenshotShelfUi2()) {
-                                    mActionsController.onScrollChipInvalidated();
-                                } else {
-                                    mViewProxy.hideScrollChip();
-                                }
+                                mActionsController.onScrollChipInvalidated();
                                 // Delay scroll capture eval a bit to allow the underlying activity
                                 // to set up in the new orientation.
                                 mScreenshotHandler.postDelayed(
@@ -640,13 +622,8 @@
                 (response) -> {
                     mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION,
                             0, response.getPackageName());
-                    if (screenshotShelfUi2()) {
-                        mActionsController.onScrollChipReady(requestId,
-                                () -> onScrollButtonClicked(owner, response));
-                    } else {
-                        mViewProxy.showScrollChip(response.getPackageName(),
-                                () -> onScrollButtonClicked(owner, response));
-                    }
+                    mActionsController.onScrollChipReady(requestId,
+                            () -> onScrollButtonClicked(owner, response));
                     return Unit.INSTANCE;
                 }
         );
@@ -715,11 +692,9 @@
         mWindowManager.addView(decorView, mWindowLayoutParams);
         decorView.requestApplyInsets();
 
-        if (screenshotShelfUi2()) {
-            ViewGroup layout = decorView.requireViewById(android.R.id.content);
-            layout.setClipChildren(false);
-            layout.setClipToPadding(false);
-        }
+        ViewGroup layout = decorView.requireViewById(android.R.id.content);
+        layout.setClipChildren(false);
+        layout.setClipToPadding(false);
     }
 
     void removeWindow() {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index 8235325..56ba1af4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -16,15 +16,12 @@
 
 package com.android.systemui.screenshot.dagger;
 
-import static com.android.systemui.Flags.screenshotShelfUi2;
-
 import android.app.Service;
 import android.view.accessibility.AccessibilityManager;
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.screenshot.ImageCapture;
 import com.android.systemui.screenshot.ImageCaptureImpl;
-import com.android.systemui.screenshot.LegacyScreenshotViewProxy;
 import com.android.systemui.screenshot.ScreenshotPolicy;
 import com.android.systemui.screenshot.ScreenshotPolicyImpl;
 import com.android.systemui.screenshot.ScreenshotShelfViewProxy;
@@ -96,14 +93,7 @@
         return new ScreenshotViewModel(accessibilityManager);
     }
 
-    @Provides
-    static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory(
-            ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory,
-            LegacyScreenshotViewProxy.Factory legacyScreenshotViewProxyFactory) {
-        if (screenshotShelfUi2()) {
-            return shelfScreenshotViewProxyFactory;
-        } else {
-            return legacyScreenshotViewProxyFactory;
-        }
-    }
+    @Binds
+    abstract ScreenshotViewProxy.Factory bindScreenshotViewProxyFactory(
+            ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt
index 21f301c..6917f46 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt
@@ -49,7 +49,7 @@
         activeCastDevice
             .map {
                 if (it != null) {
-                    MediaRouterCastModel.Casting
+                    MediaRouterCastModel.Casting(deviceName = it.name)
                 } else {
                     MediaRouterCastModel.DoingNothing
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/model/MediaRouterCastModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/model/MediaRouterCastModel.kt
index b228922..1f84d7c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/model/MediaRouterCastModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/model/MediaRouterCastModel.kt
@@ -21,6 +21,10 @@
     /** MediaRouter isn't aware of any active cast. */
     data object DoingNothing : MediaRouterCastModel
 
-    /** MediaRouter has an active cast. */
-    data object Casting : MediaRouterCastModel
+    /**
+     * MediaRouter has an active cast.
+     *
+     * @property deviceName the name of the device receiving the cast.
+     */
+    data class Casting(val deviceName: String?) : MediaRouterCastModel
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
index ffb20a7..cac3f25 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
@@ -16,7 +16,9 @@
 
 package com.android.systemui.statusbar.chips.casttootherdevice.ui.view
 
+import android.content.Context
 import android.os.Bundle
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel.Companion.CAST_TO_OTHER_DEVICE_ICON
 import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel
@@ -26,6 +28,7 @@
 /** A dialog that lets the user stop an ongoing cast-screen-to-other-device event. */
 class EndCastScreenToOtherDeviceDialogDelegate(
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
+    private val context: Context,
     private val stopAction: () -> Unit,
     private val state: ProjectionChipModel.Projecting,
 ) : SystemUIDialog.Delegate {
@@ -36,16 +39,8 @@
     override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
         with(dialog) {
             setIcon(CAST_TO_OTHER_DEVICE_ICON)
-            setTitle(R.string.cast_screen_to_other_device_stop_dialog_title)
-            // TODO(b/332662551): Include device name in this string.
-            setMessage(
-                endMediaProjectionDialogHelper.getDialogMessage(
-                    state.projectionState,
-                    genericMessageResId = R.string.cast_screen_to_other_device_stop_dialog_message,
-                    specificAppMessageResId =
-                        R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
-                )
-            )
+            setTitle(R.string.cast_to_other_device_stop_dialog_title)
+            setMessage(getMessage())
             // No custom on-click, because the dialog will automatically be dismissed when the
             // button is clicked anyway.
             setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
@@ -54,4 +49,41 @@
             }
         }
     }
+
+    private fun getMessage(): String {
+        return if (state.projectionState is MediaProjectionState.Projecting.SingleTask) {
+            val appBeingSharedName =
+                endMediaProjectionDialogHelper.getAppName(state.projectionState)
+            if (appBeingSharedName != null && state.deviceName != null) {
+                context.getString(
+                    R.string.cast_to_other_device_stop_dialog_message_specific_app_with_device,
+                    appBeingSharedName,
+                    state.deviceName,
+                )
+            } else if (appBeingSharedName != null) {
+                context.getString(
+                    R.string.cast_to_other_device_stop_dialog_message_specific_app,
+                    appBeingSharedName,
+                )
+            } else if (state.deviceName != null) {
+                context.getString(
+                    R.string.cast_to_other_device_stop_dialog_message_generic_with_device,
+                    state.deviceName
+                )
+            } else {
+                context.getString(R.string.cast_to_other_device_stop_dialog_message_generic)
+            }
+        } else {
+            if (state.deviceName != null) {
+                context.getString(
+                    R.string.cast_to_other_device_stop_dialog_message_entire_screen_with_device,
+                    state.deviceName
+                )
+            } else {
+                context.getString(
+                    R.string.cast_to_other_device_stop_dialog_message_entire_screen,
+                )
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
index afe67b4..7dc9b25 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.casttootherdevice.ui.view
 
+import android.content.Context
 import android.os.Bundle
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel.Companion.CAST_TO_OTHER_DEVICE_ICON
@@ -29,6 +30,8 @@
  */
 class EndGenericCastToOtherDeviceDialogDelegate(
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
+    private val context: Context,
+    private val deviceName: String?,
     private val stopAction: () -> Unit,
 ) : SystemUIDialog.Delegate {
     override fun createDialog(): SystemUIDialog {
@@ -36,11 +39,19 @@
     }
 
     override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+        val message =
+            if (deviceName != null) {
+                context.getString(
+                    R.string.cast_to_other_device_stop_dialog_message_generic_with_device,
+                    deviceName,
+                )
+            } else {
+                context.getString(R.string.cast_to_other_device_stop_dialog_message_generic)
+            }
         with(dialog) {
             setIcon(CAST_TO_OTHER_DEVICE_ICON)
             setTitle(R.string.cast_to_other_device_stop_dialog_title)
-            // TODO(b/332662551): Include device name in this string.
-            setMessage(R.string.cast_to_other_device_stop_dialog_message)
+            setMessage(message)
             // No custom on-click, because the dialog will automatically be dismissed when the
             // button is clicked anyway.
             setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
index 2eff336..4183cdd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel
 
+import android.content.Context
 import androidx.annotation.DrawableRes
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
@@ -53,6 +54,7 @@
 @Inject
 constructor(
     @Application private val scope: CoroutineScope,
+    private val context: Context,
     private val mediaProjectionChipInteractor: MediaProjectionChipInteractor,
     private val mediaRouterChipInteractor: MediaRouterChipInteractor,
     private val systemClock: SystemClock,
@@ -115,7 +117,7 @@
                         // This does mean that the audio-only casting chip will *never* show a
                         // timer, because audio-only casting never activates the MediaProjection
                         // APIs and those are the only cast APIs that show a timer.
-                        createIconOnlyCastChip()
+                        createIconOnlyCastChip(routerModel.deviceName)
                     }
                 }
             }
@@ -178,7 +180,7 @@
         )
     }
 
-    private fun createIconOnlyCastChip(): OngoingActivityChipModel.Shown {
+    private fun createIconOnlyCastChip(deviceName: String?): OngoingActivityChipModel.Shown {
         return OngoingActivityChipModel.Shown.IconOnly(
             icon =
                 Icon.Resource(
@@ -188,7 +190,7 @@
                 ),
             colors = ColorsModel.Red,
             createDialogLaunchOnClickListener(
-                createGenericCastToOtherDeviceDialogDelegate(),
+                createGenericCastToOtherDeviceDialogDelegate(deviceName),
                 dialogTransitionAnimator,
             ),
         )
@@ -199,13 +201,16 @@
     ) =
         EndCastScreenToOtherDeviceDialogDelegate(
             endMediaProjectionDialogHelper,
+            context,
             stopAction = this::stopProjecting,
             state,
         )
 
-    private fun createGenericCastToOtherDeviceDialogDelegate() =
+    private fun createGenericCastToOtherDeviceDialogDelegate(deviceName: String?) =
         EndGenericCastToOtherDeviceDialogDelegate(
             endMediaProjectionDialogHelper,
+            context,
+            deviceName,
             stopAction = this::stopMediaRouterCasting,
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
index cda17ce..ce60fab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
@@ -74,7 +74,8 @@
                             },
                             { "State: Projecting(type=$str1 hostPackage=$str2)" }
                         )
-                        ProjectionChipModel.Projecting(type, state)
+                        // TODO(b/351851835): Get the device name.
+                        ProjectionChipModel.Projecting(type, state, deviceName = null)
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
index 85682f5..a1a5e82 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
@@ -26,10 +26,16 @@
     /** There is no media being projected. */
     data object NotProjecting : ProjectionChipModel()
 
-    /** Media is currently being projected. */
+    /**
+     * Media is currently being projected.
+     *
+     * @property deviceName the name of the device receiving the projection, or null if the
+     *   projection is to this device (as opposed to a different device).
+     */
     data class Projecting(
         val type: Type,
         val projectionState: MediaProjectionState.Projecting,
+        val deviceName: String?,
     ) : ProjectionChipModel()
 
     enum class Type {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt
index 402306a..6004365 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt
@@ -16,13 +16,8 @@
 
 package com.android.systemui.statusbar.chips.mediaprojection.ui.view
 
-import android.annotation.StringRes
 import android.app.ActivityManager
-import android.content.Context
 import android.content.pm.PackageManager
-import android.text.Html
-import android.text.Html.FROM_HTML_MODE_LEGACY
-import android.text.TextUtils
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -35,63 +30,38 @@
 constructor(
     private val dialogFactory: SystemUIDialog.Factory,
     private val packageManager: PackageManager,
-    private val context: Context
 ) {
     /** Creates a new [SystemUIDialog] using the given delegate. */
     fun createDialog(delegate: SystemUIDialog.Delegate): SystemUIDialog {
         return dialogFactory.create(delegate)
     }
 
-    /** See other [getDialogMessage]. */
-    fun getDialogMessage(
-        state: MediaProjectionState.Projecting,
-        @StringRes genericMessageResId: Int,
-        @StringRes specificAppMessageResId: Int,
-    ): CharSequence {
+    fun getAppName(state: MediaProjectionState.Projecting): CharSequence? {
         val specificTaskInfo =
             if (state is MediaProjectionState.Projecting.SingleTask) {
                 state.task
             } else {
                 null
             }
-        return getDialogMessage(specificTaskInfo, genericMessageResId, specificAppMessageResId)
+        return getAppName(specificTaskInfo)
+    }
+
+    fun getAppName(specificTaskInfo: ActivityManager.RunningTaskInfo?): CharSequence? {
+        val packageName = specificTaskInfo?.baseIntent?.component?.packageName ?: return null
+        return getAppName(packageName)
     }
 
     /**
-     * Returns the message to show in the dialog based on the specific media projection state.
-     *
-     * @param genericMessageResId a res ID for a more generic "end projection" message
-     * @param specificAppMessageResId a res ID for an "end projection" message that also lets us
-     *   specify which app is currently being projected.
+     * Returns the human-readable application name for the given package, or null if it couldn't be
+     * found for any reason.
      */
-    fun getDialogMessage(
-        specificTaskInfo: ActivityManager.RunningTaskInfo?,
-        @StringRes genericMessageResId: Int,
-        @StringRes specificAppMessageResId: Int,
-    ): CharSequence {
-        if (specificTaskInfo == null) {
-            return context.getString(genericMessageResId)
-        }
-        val packageName =
-            specificTaskInfo.baseIntent.component?.packageName
-                ?: return context.getString(genericMessageResId)
+    fun getAppName(packageName: String): CharSequence? {
         return try {
             val appInfo = packageManager.getApplicationInfo(packageName, 0)
-            val appName = appInfo.loadLabel(packageManager)
-            getSpecificAppMessageText(specificAppMessageResId, appName)
+            appInfo.loadLabel(packageManager)
         } catch (e: PackageManager.NameNotFoundException) {
             // TODO(b/332662551): Log this error.
-            context.getString(genericMessageResId)
+            null
         }
     }
-
-    private fun getSpecificAppMessageText(
-        @StringRes specificAppMessageResId: Int,
-        appName: CharSequence,
-    ): CharSequence {
-        // https://developer.android.com/guide/topics/resources/string-resource#StylingWithHTML
-        val escapedAppName = TextUtils.htmlEncode(appName.toString())
-        val text = context.getString(specificAppMessageResId, escapedAppName)
-        return Html.fromHtml(text, FROM_HTML_MODE_LEGACY)
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
index 9adbff9..1eca827 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.chips.screenrecord.ui.view
 
 import android.app.ActivityManager
+import android.content.Context
 import android.os.Bundle
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
@@ -26,6 +27,7 @@
 /** A dialog that lets the user stop an ongoing screen recording. */
 class EndScreenRecordingDialogDelegate(
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
+    val context: Context,
     private val stopAction: () -> Unit,
     private val recordedTask: ActivityManager.RunningTaskInfo?,
 ) : SystemUIDialog.Delegate {
@@ -35,16 +37,18 @@
     }
 
     override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+        val appName = endMediaProjectionDialogHelper.getAppName(recordedTask)
+        val message =
+            if (appName != null) {
+                context.getString(R.string.screenrecord_stop_dialog_message_specific_app, appName)
+            } else {
+                context.getString(R.string.screenrecord_stop_dialog_message)
+            }
+
         with(dialog) {
             setIcon(ScreenRecordChipViewModel.ICON)
             setTitle(R.string.screenrecord_stop_dialog_title)
-            setMessage(
-                endMediaProjectionDialogHelper.getDialogMessage(
-                    recordedTask,
-                    genericMessageResId = R.string.screenrecord_stop_dialog_message,
-                    specificAppMessageResId = R.string.screenrecord_stop_dialog_message_specific_app
-                )
-            )
+            setMessage(message)
             // No custom on-click, because the dialog will automatically be dismissed when the
             // button is clicked anyway.
             setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
index 53679f1..df25d57 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel
 
 import android.app.ActivityManager
+import android.content.Context
 import androidx.annotation.DrawableRes
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
@@ -47,6 +48,7 @@
 @Inject
 constructor(
     @Application private val scope: CoroutineScope,
+    private val context: Context,
     private val interactor: ScreenRecordChipInteractor,
     private val systemClock: SystemClock,
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
@@ -90,6 +92,7 @@
     ): EndScreenRecordingDialogDelegate {
         return EndScreenRecordingDialogDelegate(
             endMediaProjectionDialogHelper,
+            context,
             stopAction = interactor::stopRecording,
             recordedTask,
         )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
index 7e7ef40..564f20e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
@@ -16,7 +16,9 @@
 
 package com.android.systemui.statusbar.chips.sharetoapp.ui.view
 
+import android.content.Context
 import android.os.Bundle
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel
 import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
@@ -26,6 +28,7 @@
 /** A dialog that lets the user stop an ongoing share-screen-to-app event. */
 class EndShareToAppDialogDelegate(
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
+    private val context: Context,
     private val stopAction: () -> Unit,
     private val state: ProjectionChipModel.Projecting,
 ) : SystemUIDialog.Delegate {
@@ -37,13 +40,7 @@
         with(dialog) {
             setIcon(SHARE_TO_APP_ICON)
             setTitle(R.string.share_to_app_stop_dialog_title)
-            setMessage(
-                endMediaProjectionDialogHelper.getDialogMessage(
-                    state.projectionState,
-                    genericMessageResId = R.string.share_to_app_stop_dialog_message,
-                    specificAppMessageResId = R.string.share_to_app_stop_dialog_message_specific_app
-                )
-            )
+            setMessage(getMessage())
             // No custom on-click, because the dialog will automatically be dismissed when the
             // button is clicked anyway.
             setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
@@ -52,4 +49,32 @@
             }
         }
     }
+
+    private fun getMessage(): String {
+        return if (state.projectionState is MediaProjectionState.Projecting.SingleTask) {
+            // If a single app is being shared, use the name of the app being shared in the dialog
+            val appBeingSharedName =
+                endMediaProjectionDialogHelper.getAppName(state.projectionState)
+            if (appBeingSharedName != null) {
+                context.getString(
+                    R.string.share_to_app_stop_dialog_message_single_app_specific,
+                    appBeingSharedName,
+                )
+            } else {
+                context.getString(R.string.share_to_app_stop_dialog_message_single_app_generic)
+            }
+        } else {
+            // Otherwise, use the name of the app *receiving* the share
+            val hostAppName =
+                endMediaProjectionDialogHelper.getAppName(state.projectionState.hostPackage)
+            if (hostAppName != null) {
+                context.getString(
+                    R.string.share_to_app_stop_dialog_message_entire_screen_with_host_app,
+                    hostAppName
+                )
+            } else {
+                context.getString(R.string.share_to_app_stop_dialog_message_entire_screen)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
index 8aef5a4..c097720 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel
 
+import android.content.Context
 import androidx.annotation.DrawableRes
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
@@ -48,6 +49,7 @@
 @Inject
 constructor(
     @Application private val scope: CoroutineScope,
+    private val context: Context,
     private val mediaProjectionChipInteractor: MediaProjectionChipInteractor,
     private val systemClock: SystemClock,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
@@ -97,6 +99,7 @@
     private fun createShareToAppDialogDelegate(state: ProjectionChipModel.Projecting) =
         EndShareToAppDialogDelegate(
             endMediaProjectionDialogHelper,
+            context,
             stopAction = this::stopProjecting,
             state,
         )
diff --git a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java
index 64f8246..a58a264 100644
--- a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java
@@ -27,6 +27,7 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.app.tracing.TraceStateLogger;
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dump.DumpManager;
@@ -41,11 +42,11 @@
 /**
  * The {@link PersistentConnectionManager} is responsible for maintaining a connection to a
  * {@link ObservableServiceConnection}.
+ *
  * @param <T> The transformed connection type handled by the service.
  */
 public class PersistentConnectionManager<T> implements Dumpable {
     private static final String TAG = "PersistentConnManager";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private final SystemClock mSystemClock;
     private final DelayableExecutor mBgExecutor;
@@ -55,6 +56,7 @@
     private final Observer mObserver;
     private final DumpManager mDumpManager;
     private final String mDumpsysName;
+    private final TraceStateLogger mConnectionReasonLogger;
 
     private int mReconnectAttempts = 0;
     private Runnable mCurrentReconnectCancelable;
@@ -64,36 +66,52 @@
     private final Runnable mConnectRunnable = new Runnable() {
         @Override
         public void run() {
+            mConnectionReasonLogger.log("ConnectionReasonRetry");
             mCurrentReconnectCancelable = null;
             mConnection.bind();
         }
     };
 
-    private final Observer.Callback mObserverCallback = () -> initiateConnectionAttempt();
+    private final Observer.Callback mObserverCallback = () -> initiateConnectionAttempt(
+            "ConnectionReasonObserver");
 
     private final ObservableServiceConnection.Callback<T> mConnectionCallback =
             new ObservableServiceConnection.Callback<>() {
-        private long mStartTime;
+                private long mStartTime = -1;
 
-        @Override
-        public void onConnected(ObservableServiceConnection connection, Object proxy) {
-            mStartTime = mSystemClock.currentTimeMillis();
-        }
+                @Override
+                public void onConnected(ObservableServiceConnection connection, Object proxy) {
+                    mStartTime = mSystemClock.currentTimeMillis();
+                }
 
-        @Override
-        public void onDisconnected(ObservableServiceConnection connection, int reason) {
-            // Do not attempt to reconnect if we were manually unbound
-            if (reason == ObservableServiceConnection.DISCONNECT_REASON_UNBIND) {
-                return;
-            }
+                @Override
+                public void onDisconnected(ObservableServiceConnection connection, int reason) {
+                    // Do not attempt to reconnect if we were manually unbound
+                    if (reason == ObservableServiceConnection.DISCONNECT_REASON_UNBIND) {
+                        return;
+                    }
 
-            if (mSystemClock.currentTimeMillis() - mStartTime > mMinConnectionDuration) {
-                initiateConnectionAttempt();
-            } else {
-                scheduleConnectionAttempt();
-            }
-        }
-    };
+                    if (mStartTime <= 0) {
+                        Log.e(TAG, "onDisconnected called with invalid connection start time: "
+                                + mStartTime);
+                        return;
+                    }
+
+                    final float connectionDuration = mSystemClock.currentTimeMillis() - mStartTime;
+                    // Reset the start time.
+                    mStartTime = -1;
+
+                    if (connectionDuration > mMinConnectionDuration) {
+                        Log.i(TAG, "immediately reconnecting since service was connected for "
+                                + connectionDuration
+                                + "ms which is longer than the min duration of "
+                                + mMinConnectionDuration + "ms");
+                        initiateConnectionAttempt("ConnectionReasonMinDurationMet");
+                    } else {
+                        scheduleConnectionAttempt();
+                    }
+                }
+            };
 
     @Inject
     public PersistentConnectionManager(
@@ -112,6 +130,7 @@
         mObserver = observer;
         mDumpManager = dumpManager;
         mDumpsysName = TAG + "#" + dumpsysName;
+        mConnectionReasonLogger = new TraceStateLogger(mDumpsysName);
 
         mMaxReconnectAttempts = maxReconnectAttempts;
         mBaseReconnectDelayMs = baseReconnectDelayMs;
@@ -125,7 +144,7 @@
         mDumpManager.registerCriticalDumpable(mDumpsysName, this);
         mConnection.addCallback(mConnectionCallback);
         mObserver.addCallback(mObserverCallback);
-        initiateConnectionAttempt();
+        initiateConnectionAttempt("ConnectionReasonStart");
     }
 
     /**
@@ -140,6 +159,7 @@
 
     /**
      * Add a callback to the {@link ObservableServiceConnection}.
+     *
      * @param callback The callback to add.
      */
     public void addConnectionCallback(ObservableServiceConnection.Callback<T> callback) {
@@ -148,6 +168,7 @@
 
     /**
      * Remove a callback from the {@link ObservableServiceConnection}.
+     *
      * @param callback The callback to remove.
      */
     public void removeConnectionCallback(ObservableServiceConnection.Callback<T> callback) {
@@ -163,10 +184,10 @@
         mConnection.dump(pw);
     }
 
-    private void initiateConnectionAttempt() {
+    private void initiateConnectionAttempt(String reason) {
+        mConnectionReasonLogger.log(reason);
         // Reset attempts
         mReconnectAttempts = 0;
-
         // The first attempt is always a direct invocation rather than delayed.
         mConnection.bind();
     }
@@ -179,20 +200,15 @@
         }
 
         if (mReconnectAttempts >= mMaxReconnectAttempts) {
-            if (DEBUG) {
-                Log.d(TAG, "exceeded max connection attempts.");
-            }
+            Log.d(TAG, "exceeded max connection attempts.");
             return;
         }
 
         final long reconnectDelayMs =
                 (long) Math.scalb(mBaseReconnectDelayMs, mReconnectAttempts);
 
-        if (DEBUG) {
-            Log.d(TAG,
-                    "scheduling connection attempt in " + reconnectDelayMs + "milliseconds");
-        }
-
+        Log.d(TAG,
+                "scheduling connection attempt in " + reconnectDelayMs + "milliseconds");
         mCurrentReconnectCancelable = mBgExecutor.executeDelayed(mConnectRunnable,
                 reconnectDelayMs);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
index 8a6a50d..ecb1a6d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
@@ -70,7 +70,7 @@
         }
 
     @Test
-    fun mediaRouterCastingState_connectingDevice_casting() =
+    fun mediaRouterCastingState_connectingDevice_casting_withName() =
         testScope.runTest {
             val latest by collectLastValue(underTest.mediaRouterCastingState)
 
@@ -79,17 +79,18 @@
                     CastDevice(
                         state = CastDevice.CastState.Connecting,
                         id = "id",
-                        name = "name",
+                        name = "My Favorite Device",
                         description = "desc",
                         origin = CastDevice.CastOrigin.MediaRouter,
                     )
                 )
 
-            assertThat(latest).isEqualTo(MediaRouterCastModel.Casting)
+            assertThat(latest)
+                .isEqualTo(MediaRouterCastModel.Casting(deviceName = "My Favorite Device"))
         }
 
     @Test
-    fun mediaRouterCastingState_connectedDevice_casting() =
+    fun mediaRouterCastingState_connectedDevice_casting_withName() =
         testScope.runTest {
             val latest by collectLastValue(underTest.mediaRouterCastingState)
 
@@ -98,13 +99,14 @@
                     CastDevice(
                         state = CastDevice.CastState.Connected,
                         id = "id",
-                        name = "name",
+                        name = "My Second Favorite Device",
                         description = "desc",
                         origin = CastDevice.CastOrigin.MediaRouter,
                     )
                 )
 
-            assertThat(latest).isEqualTo(MediaRouterCastModel.Casting)
+            assertThat(latest)
+                .isEqualTo(MediaRouterCastModel.Casting(deviceName = "My Second Favorite Device"))
         }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
index e9d6f0e..c8397bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
@@ -19,8 +19,10 @@
 import android.content.ComponentName
 import android.content.DialogInterface
 import android.content.Intent
+import android.content.applicationContext
 import android.content.packageManager
 import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.kosmos.Kosmos
@@ -68,21 +70,87 @@
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
-        verify(sysuiDialog).setTitle(R.string.cast_screen_to_other_device_stop_dialog_title)
+        verify(sysuiDialog).setTitle(R.string.cast_to_other_device_stop_dialog_title)
     }
 
     @Test
-    fun message_entireScreen() {
-        createAndSetDelegate(ENTIRE_SCREEN)
+    fun message_entireScreen_unknownDevice() {
+        createAndSetDelegate(ENTIRE_SCREEN, deviceName = null)
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
         verify(sysuiDialog)
-            .setMessage(context.getString(R.string.cast_screen_to_other_device_stop_dialog_message))
+            .setMessage(
+                context.getString(R.string.cast_to_other_device_stop_dialog_message_entire_screen)
+            )
     }
 
     @Test
-    fun message_singleTask() {
+    fun message_entireScreen_hasDevice() {
+        createAndSetDelegate(ENTIRE_SCREEN, deviceName = "My Favorite Device")
+
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog)
+            .setMessage(
+                context.getString(
+                    R.string.cast_to_other_device_stop_dialog_message_entire_screen_with_device,
+                    "My Favorite Device",
+                )
+            )
+    }
+
+    @Test
+    fun message_singleTask_unknownAppName_unknownDevice() {
+        val baseIntent =
+            Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+        whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+            .thenThrow(PackageManager.NameNotFoundException())
+
+        createAndSetDelegate(
+            MediaProjectionState.Projecting.SingleTask(
+                HOST_PACKAGE,
+                createTask(taskId = 1, baseIntent = baseIntent)
+            ),
+            deviceName = null,
+        )
+
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog)
+            .setMessage(
+                context.getString(R.string.cast_to_other_device_stop_dialog_message_generic)
+            )
+    }
+
+    @Test
+    fun message_singleTask_unknownAppName_hasDevice() {
+        val baseIntent =
+            Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+        whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+            .thenThrow(PackageManager.NameNotFoundException())
+
+        createAndSetDelegate(
+            MediaProjectionState.Projecting.SingleTask(
+                HOST_PACKAGE,
+                createTask(taskId = 1, baseIntent = baseIntent)
+            ),
+            deviceName = "My Favorite Device",
+        )
+
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog)
+            .setMessage(
+                context.getString(
+                    R.string.cast_to_other_device_stop_dialog_message_generic_with_device,
+                    "My Favorite Device",
+                )
+            )
+    }
+
+    @Test
+    fun message_singleTask_hasAppName_unknownDevice() {
         val baseIntent =
             Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
         val appInfo = mock<ApplicationInfo>()
@@ -94,16 +162,48 @@
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
                 createTask(taskId = 1, baseIntent = baseIntent)
-            )
+            ),
+            deviceName = null,
         )
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
-        // It'd be nice to use R.string.cast_screen_to_other_device_stop_dialog_message_specific_app
-        // directly, but it includes the <b> tags which aren't in the returned string.
-        val result = argumentCaptor<CharSequence>()
-        verify(sysuiDialog).setMessage(result.capture())
-        assertThat(result.firstValue.toString()).isEqualTo("You will stop casting Fake Package")
+        verify(sysuiDialog)
+            .setMessage(
+                context.getString(
+                    R.string.cast_to_other_device_stop_dialog_message_specific_app,
+                    "Fake Package",
+                )
+            )
+    }
+
+    @Test
+    fun message_singleTask_hasAppName_hasDevice() {
+        val baseIntent =
+            Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+        val appInfo = mock<ApplicationInfo>()
+        whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package")
+        whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+            .thenReturn(appInfo)
+
+        createAndSetDelegate(
+            MediaProjectionState.Projecting.SingleTask(
+                HOST_PACKAGE,
+                createTask(taskId = 1, baseIntent = baseIntent)
+            ),
+            deviceName = "My Favorite Device",
+        )
+
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog)
+            .setMessage(
+                context.getString(
+                    R.string.cast_to_other_device_stop_dialog_message_specific_app_with_device,
+                    "Fake Package",
+                    "My Favorite Device",
+                )
+            )
     }
 
     @Test
@@ -140,14 +240,19 @@
             assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
         }
 
-    private fun createAndSetDelegate(state: MediaProjectionState.Projecting) {
+    private fun createAndSetDelegate(
+        state: MediaProjectionState.Projecting,
+        deviceName: String? = null,
+    ) {
         underTest =
             EndCastScreenToOtherDeviceDialogDelegate(
                 kosmos.endMediaProjectionDialogHelper,
+                kosmos.applicationContext,
                 stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
                 ProjectionChipModel.Projecting(
                     ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE,
                     state,
+                    deviceName,
                 ),
             )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
index 0af423d..e6101f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.chips.casttootherdevice.ui.view
 
 import android.content.DialogInterface
+import android.content.applicationContext
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -65,12 +66,30 @@
     }
 
     @Test
-    fun message() {
-        createAndSetDelegate()
+    fun message_unknownDevice() {
+        createAndSetDelegate(deviceName = null)
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
-        verify(sysuiDialog).setMessage(R.string.cast_to_other_device_stop_dialog_message)
+        verify(sysuiDialog)
+            .setMessage(
+                context.getString(R.string.cast_to_other_device_stop_dialog_message_generic)
+            )
+    }
+
+    @Test
+    fun message_hasDevice() {
+        createAndSetDelegate(deviceName = "My Favorite Device")
+
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog)
+            .setMessage(
+                context.getString(
+                    R.string.cast_to_other_device_stop_dialog_message_generic_with_device,
+                    "My Favorite Device",
+                )
+            )
     }
 
     @Test
@@ -122,10 +141,12 @@
             assertThat(kosmos.fakeMediaRouterRepository.lastStoppedDevice).isEqualTo(device)
         }
 
-    private fun createAndSetDelegate() {
+    private fun createAndSetDelegate(deviceName: String? = null) {
         underTest =
             EndGenericCastToOtherDeviceDialogDelegate(
                 kosmos.endMediaProjectionDialogHelper,
+                kosmos.applicationContext,
+                deviceName,
                 stopAction = kosmos.mediaRouterChipInteractor::stopCasting,
             )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
index f9ad5ac..ab935fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
@@ -27,7 +27,6 @@
 import com.android.systemui.kosmos.testCase
 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
-import com.android.systemui.res.R
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.google.common.truth.Truth.assertThat
@@ -56,19 +55,15 @@
     }
 
     @Test
-    fun getDialogMessage_entireScreen_isGenericMessage() {
+    fun getAppName_stateVersion_entireScreen_returnsNull() {
         val result =
-            underTest.getDialogMessage(
-                MediaProjectionState.Projecting.EntireScreen("host.package"),
-                R.string.accessibility_home,
-                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
-            )
+            underTest.getAppName(MediaProjectionState.Projecting.EntireScreen("host.package"))
 
-        assertThat(result).isEqualTo(context.getString(R.string.accessibility_home))
+        assertThat(result).isNull()
     }
 
     @Test
-    fun getDialogMessage_singleTask_cannotFindPackage_isGenericMessage() {
+    fun getAppName_stateVersion_singleTask_cannotFindPackage_returnsNull() {
         val baseIntent =
             Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
         whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
@@ -77,21 +72,16 @@
         val projectionState =
             MediaProjectionState.Projecting.SingleTask(
                 "host.package",
-                createTask(taskId = 1, baseIntent = baseIntent)
+                createTask(taskId = 1, baseIntent = baseIntent),
             )
 
-        val result =
-            underTest.getDialogMessage(
-                projectionState,
-                R.string.accessibility_home,
-                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
-            )
+        val result = underTest.getAppName(projectionState)
 
-        assertThat(result).isEqualTo(context.getString(R.string.accessibility_home))
+        assertThat(result).isNull()
     }
 
     @Test
-    fun getDialogMessage_singleTask_findsPackage_isSpecificMessageWithAppLabel() {
+    fun getAppName_stateVersion_singleTask_findsPackage_returnsName() {
         val baseIntent =
             Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
         val appInfo = mock<ApplicationInfo>()
@@ -102,93 +92,66 @@
         val projectionState =
             MediaProjectionState.Projecting.SingleTask(
                 "host.package",
-                createTask(taskId = 1, baseIntent = baseIntent)
+                createTask(taskId = 1, baseIntent = baseIntent),
             )
 
-        val result =
-            underTest.getDialogMessage(
-                projectionState,
-                R.string.accessibility_home,
-                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
-            )
+        val result = underTest.getAppName(projectionState)
 
-        // It'd be nice to use the R.string resources directly, but they include the <b> tags which
-        // aren't in the returned string.
-        assertThat(result.toString()).isEqualTo("You will stop casting Fake Package")
+        assertThat(result).isEqualTo("Fake Package")
     }
 
     @Test
-    fun getDialogMessage_nullTask_isGenericMessage() {
-        val result =
-            underTest.getDialogMessage(
-                specificTaskInfo = null,
-                R.string.accessibility_home,
-                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
-            )
+    fun getAppName_taskInfoVersion_null_returnsNull() {
+        val result = underTest.getAppName(specificTaskInfo = null)
 
-        assertThat(result).isEqualTo(context.getString(R.string.accessibility_home))
+        assertThat(result).isNull()
     }
 
     @Test
-    fun getDialogMessage_withTask_cannotFindPackage_isGenericMessage() {
+    fun getAppName_taskInfoVersion_cannotFindPackage_returnsNull() {
         val baseIntent =
             Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
         whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
             .thenThrow(PackageManager.NameNotFoundException())
-        val task = createTask(taskId = 1, baseIntent = baseIntent)
 
-        val result =
-            underTest.getDialogMessage(
-                task,
-                R.string.accessibility_home,
-                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
-            )
+        val result = underTest.getAppName(createTask(taskId = 1, baseIntent = baseIntent))
 
-        assertThat(result).isEqualTo(context.getString(R.string.accessibility_home))
+        assertThat(result).isNull()
     }
 
     @Test
-    fun getDialogMessage_withTask_findsPackage_isSpecificMessageWithAppLabel() {
+    fun getAppName_taskInfoVersion_findsPackage_returnsName() {
         val baseIntent =
             Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
         val appInfo = mock<ApplicationInfo>()
         whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package")
         whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
             .thenReturn(appInfo)
-        val task = createTask(taskId = 1, baseIntent = baseIntent)
 
-        val result =
-            underTest.getDialogMessage(
-                task,
-                R.string.accessibility_home,
-                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
-            )
+        val result = underTest.getAppName(createTask(taskId = 1, baseIntent = baseIntent))
 
-        assertThat(result.toString()).isEqualTo("You will stop casting Fake Package")
+        assertThat(result).isEqualTo("Fake Package")
     }
 
     @Test
-    fun getDialogMessage_appLabelHasSpecialCharacters_isEscaped() {
-        val baseIntent =
-            Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+    fun getAppName_packageNameVersion_cannotFindPackage_returnsNull() {
+        whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+            .thenThrow(PackageManager.NameNotFoundException())
+
+        val result = underTest.getAppName("fake.task.package")
+
+        assertThat(result).isNull()
+    }
+
+    @Test
+    fun getAppName_packageNameVersion_findsPackage_returnsName() {
         val appInfo = mock<ApplicationInfo>()
-        whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake & Package <Here>")
+        whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package")
         whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
             .thenReturn(appInfo)
 
-        val projectionState =
-            MediaProjectionState.Projecting.SingleTask(
-                "host.package",
-                createTask(taskId = 1, baseIntent = baseIntent)
-            )
+        val result = underTest.getAppName("fake.task.package")
 
-        val result =
-            underTest.getDialogMessage(
-                projectionState,
-                R.string.accessibility_home,
-                R.string.cast_screen_to_other_device_stop_dialog_message_specific_app,
-            )
-
-        assertThat(result.toString()).isEqualTo("You will stop casting Fake & Package <Here>")
+        assertThat(result).isEqualTo("Fake Package")
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
index 7e667de..bfb63ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
@@ -20,6 +20,7 @@
 import android.content.ComponentName
 import android.content.DialogInterface
 import android.content.Intent
+import android.content.applicationContext
 import android.content.packageManager
 import android.content.pm.ApplicationInfo
 import androidx.test.filters.SmallTest
@@ -95,11 +96,13 @@
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
-        // It'd be nice to use R.string.screenrecord_stop_dialog_message_specific_app directly, but
-        // it includes the <b> tags which aren't in the returned string.
-        val result = argumentCaptor<CharSequence>()
-        verify(sysuiDialog).setMessage(result.capture())
-        assertThat(result.firstValue.toString()).isEqualTo("You will stop recording Fake Package")
+        verify(sysuiDialog)
+            .setMessage(
+                context.getString(
+                    R.string.screenrecord_stop_dialog_message_specific_app,
+                    "Fake Package",
+                )
+            )
     }
 
     @Test
@@ -140,6 +143,7 @@
         underTest =
             EndScreenRecordingDialogDelegate(
                 kosmos.endMediaProjectionDialogHelper,
+                kosmos.applicationContext,
                 stopAction = kosmos.screenRecordChipInteractor::stopRecording,
                 recordedTask,
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
index 63c29ac..bfb57c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
@@ -19,8 +19,10 @@
 import android.content.ComponentName
 import android.content.DialogInterface
 import android.content.Intent
+import android.content.applicationContext
 import android.content.packageManager
 import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.kosmos.Kosmos
@@ -65,6 +67,8 @@
     @Test
     fun title() {
         createAndSetDelegate(ENTIRE_SCREEN)
+        whenever(kosmos.packageManager.getApplicationInfo(eq(HOST_PACKAGE), any<Int>()))
+            .thenThrow(PackageManager.NameNotFoundException())
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
@@ -72,16 +76,60 @@
     }
 
     @Test
-    fun message_entireScreen() {
+    fun message_entireScreen_unknownHostPackage() {
         createAndSetDelegate(ENTIRE_SCREEN)
+        whenever(kosmos.packageManager.getApplicationInfo(eq(HOST_PACKAGE), any<Int>()))
+            .thenThrow(PackageManager.NameNotFoundException())
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
-        verify(sysuiDialog).setMessage(context.getString(R.string.share_to_app_stop_dialog_message))
+        verify(sysuiDialog)
+            .setMessage(context.getString(R.string.share_to_app_stop_dialog_message_entire_screen))
     }
 
     @Test
-    fun message_singleTask() {
+    fun message_entireScreen_hasHostPackage() {
+        createAndSetDelegate(ENTIRE_SCREEN)
+        val hostAppInfo = mock<ApplicationInfo>()
+        whenever(hostAppInfo.loadLabel(kosmos.packageManager)).thenReturn("Host Package")
+        whenever(kosmos.packageManager.getApplicationInfo(eq(HOST_PACKAGE), any<Int>()))
+            .thenReturn(hostAppInfo)
+
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog)
+            .setMessage(
+                context.getString(
+                    R.string.share_to_app_stop_dialog_message_entire_screen_with_host_app,
+                    "Host Package",
+                )
+            )
+    }
+
+    @Test
+    fun message_singleTask_unknownAppName() {
+        val baseIntent =
+            Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+        whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+            .thenThrow(PackageManager.NameNotFoundException())
+
+        createAndSetDelegate(
+            MediaProjectionState.Projecting.SingleTask(
+                HOST_PACKAGE,
+                createTask(taskId = 1, baseIntent = baseIntent)
+            )
+        )
+
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog)
+            .setMessage(
+                context.getString(R.string.share_to_app_stop_dialog_message_single_app_generic)
+            )
+    }
+
+    @Test
+    fun message_singleTask_hasAppName() {
         val baseIntent =
             Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
         val appInfo = mock<ApplicationInfo>()
@@ -98,11 +146,13 @@
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
-        // It'd be nice to use R.string.share_to_app_stop_dialog_message_specific_app directly, but
-        // it includes the <b> tags which aren't in the returned string.
-        val result = argumentCaptor<CharSequence>()
-        verify(sysuiDialog).setMessage(result.capture())
-        assertThat(result.firstValue.toString()).isEqualTo("You will stop sharing Fake Package")
+        verify(sysuiDialog)
+            .setMessage(
+                context.getString(
+                    R.string.share_to_app_stop_dialog_message_single_app_specific,
+                    "Fake Package",
+                )
+            )
     }
 
     @Test
@@ -118,6 +168,8 @@
     fun positiveButton() =
         kosmos.testScope.runTest {
             createAndSetDelegate(ENTIRE_SCREEN)
+            whenever(kosmos.packageManager.getApplicationInfo(eq(HOST_PACKAGE), any<Int>()))
+                .thenThrow(PackageManager.NameNotFoundException())
 
             underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
@@ -143,8 +195,13 @@
         underTest =
             EndShareToAppDialogDelegate(
                 kosmos.endMediaProjectionDialogHelper,
+                kosmos.applicationContext,
                 stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
-                ProjectionChipModel.Projecting(ProjectionChipModel.Type.SHARE_TO_APP, state),
+                ProjectionChipModel.Projecting(
+                    ProjectionChipModel.Type.SHARE_TO_APP,
+                    state,
+                    deviceName = null,
+                ),
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java
deleted file mode 100644
index ef10fdf..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.util.service;
-
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class PersistentConnectionManagerTest extends SysuiTestCase {
-    private static final int MAX_RETRIES = 5;
-    private static final int RETRY_DELAY_MS = 1000;
-    private static final int CONNECTION_MIN_DURATION_MS = 5000;
-    private static final String DUMPSYS_NAME = "dumpsys_name";
-
-    private FakeSystemClock mFakeClock = new FakeSystemClock();
-    private FakeExecutor mFakeExecutor = new FakeExecutor(mFakeClock);
-
-    @Mock
-    private ObservableServiceConnection<Proxy> mConnection;
-
-    @Mock
-    private ObservableServiceConnection.Callback<Proxy> mConnectionCallback;
-
-    @Mock
-    private Observer mObserver;
-
-    @Mock
-    private DumpManager mDumpManager;
-
-    private static class Proxy {
-    }
-
-    private PersistentConnectionManager<Proxy> mConnectionManager;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-
-        mConnectionManager = new PersistentConnectionManager<>(
-                mFakeClock,
-                mFakeExecutor,
-                mDumpManager,
-                DUMPSYS_NAME,
-                mConnection,
-                MAX_RETRIES,
-                RETRY_DELAY_MS,
-                CONNECTION_MIN_DURATION_MS,
-                mObserver);
-    }
-
-    private ObservableServiceConnection.Callback<Proxy> captureCallbackAndSend(
-            ObservableServiceConnection<Proxy> mConnection, Proxy proxy) {
-        ArgumentCaptor<ObservableServiceConnection.Callback<Proxy>> connectionCallbackCaptor =
-                ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class);
-
-        verify(mConnection).addCallback(connectionCallbackCaptor.capture());
-        verify(mConnection).bind();
-        Mockito.clearInvocations(mConnection);
-
-        final ObservableServiceConnection.Callback callback = connectionCallbackCaptor.getValue();
-        if (proxy != null) {
-            callback.onConnected(mConnection, proxy);
-        } else {
-            callback.onDisconnected(mConnection, 0);
-        }
-
-        return callback;
-    }
-
-    /**
-     * Validates initial connection.
-     */
-    @Test
-    public void testConnect() {
-        mConnectionManager.start();
-        captureCallbackAndSend(mConnection, Mockito.mock(Proxy.class));
-    }
-
-    /**
-     * Ensures reconnection on disconnect.
-     */
-    @Test
-    public void testRetryOnBindFailure() {
-        mConnectionManager.start();
-        ArgumentCaptor<ObservableServiceConnection.Callback<Proxy>> connectionCallbackCaptor =
-                ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class);
-
-        verify(mConnection).addCallback(connectionCallbackCaptor.capture());
-
-        // Verify attempts happen. Note that we account for the retries plus initial attempt, which
-        // is not scheduled.
-        for (int attemptCount = 0; attemptCount < MAX_RETRIES + 1; attemptCount++) {
-            verify(mConnection).bind();
-            Mockito.clearInvocations(mConnection);
-            connectionCallbackCaptor.getValue().onDisconnected(mConnection, 0);
-            mFakeExecutor.advanceClockToNext();
-            mFakeExecutor.runAllReady();
-        }
-    }
-
-    /**
-     * Ensures manual unbind does not reconnect.
-     */
-    @Test
-    public void testStopDoesNotReconnect() {
-        mConnectionManager.start();
-        ArgumentCaptor<ObservableServiceConnection.Callback<Proxy>> connectionCallbackCaptor =
-                ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class);
-
-        verify(mConnection).addCallback(connectionCallbackCaptor.capture());
-        verify(mConnection).bind();
-        Mockito.clearInvocations(mConnection);
-        mConnectionManager.stop();
-        mFakeExecutor.advanceClockToNext();
-        mFakeExecutor.runAllReady();
-        verify(mConnection, never()).bind();
-    }
-
-    /**
-     * Ensures rebind on package change.
-     */
-    @Test
-    public void testAttemptOnPackageChange() {
-        mConnectionManager.start();
-        verify(mConnection).bind();
-        ArgumentCaptor<Observer.Callback> callbackCaptor =
-                ArgumentCaptor.forClass(Observer.Callback.class);
-        captureCallbackAndSend(mConnection, Mockito.mock(Proxy.class));
-
-        verify(mObserver).addCallback(callbackCaptor.capture());
-
-        callbackCaptor.getValue().onSourceChanged();
-        verify(mConnection).bind();
-    }
-
-    @Test
-    public void testAddConnectionCallback() {
-        mConnectionManager.addConnectionCallback(mConnectionCallback);
-        verify(mConnection).addCallback(mConnectionCallback);
-    }
-
-    @Test
-    public void testRemoveConnectionCallback() {
-        mConnectionManager.removeConnectionCallback(mConnectionCallback);
-        verify(mConnection).removeCallback(mConnectionCallback);
-    }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
index a8de460..144fe26 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel
 
+import android.content.applicationContext
 import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
@@ -28,6 +29,7 @@
     Kosmos.Fixture {
         CastToOtherDeviceChipViewModel(
             scope = applicationCoroutineScope,
+            context = applicationContext,
             mediaProjectionChipInteractor = mediaProjectionChipInteractor,
             mediaRouterChipInteractor = mediaRouterChipInteractor,
             systemClock = fakeSystemClock,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt
index 4f82662..1ed7a47 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.chips.mediaprojection.ui.view
 
-import android.content.applicationContext
 import android.content.packageManager
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
@@ -26,6 +25,5 @@
         EndMediaProjectionDialogHelper(
             dialogFactory = mockSystemUIDialogFactory,
             packageManager = packageManager,
-            context = applicationContext,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt
index 99b7ec9..1d06947 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel
 
+import android.content.applicationContext
 import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
@@ -27,6 +28,7 @@
     Kosmos.Fixture {
         ScreenRecordChipViewModel(
             scope = applicationCoroutineScope,
+            context = applicationContext,
             interactor = screenRecordChipInteractor,
             endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
             dialogTransitionAnimator = mockDialogTransitionAnimator,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt
index 535f81a..2e475a3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel
 
+import android.content.applicationContext
 import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
@@ -27,6 +28,7 @@
     Kosmos.Fixture {
         ShareToAppChipViewModel(
             scope = applicationCoroutineScope,
+            context = applicationContext,
             mediaProjectionChipInteractor = mediaProjectionChipInteractor,
             systemClock = fakeSystemClock,
             endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index d3efa21..9fc64a9 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -26,6 +26,8 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.graphics.Region;
+import android.hardware.input.InputManager;
+import android.os.Looper;
 import android.os.PowerManager;
 import android.os.SystemClock;
 import android.provider.Settings;
@@ -54,6 +56,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Objects;
 import java.util.StringJoiner;
 
 /**
@@ -158,6 +161,13 @@
      */
     static final int FLAG_FEATURE_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP = 0x00001000;
 
+    /**
+     * Flag for enabling the Accessibility mouse key events feature.
+     *
+     * @see #setUserAndEnabledFeatures(int, int)
+     */
+    static final int FLAG_FEATURE_MOUSE_KEYS = 0x00002000;
+
     static final int FEATURES_AFFECTING_MOTION_EVENTS =
             FLAG_FEATURE_INJECT_MOTION_EVENTS
                     | FLAG_FEATURE_AUTOCLICK
@@ -189,6 +199,8 @@
 
     private KeyboardInterceptor mKeyboardInterceptor;
 
+    private MouseKeysInterceptor mMouseKeysInterceptor;
+
     private boolean mInstalled;
 
     private int mUserId;
@@ -733,6 +745,15 @@
             // default display.
             addFirstEventHandler(Display.DEFAULT_DISPLAY, mKeyboardInterceptor);
         }
+
+        if ((mEnabledFeatures & FLAG_FEATURE_MOUSE_KEYS) != 0) {
+            mMouseKeysInterceptor = new MouseKeysInterceptor(mAms,
+                    Objects.requireNonNull(mContext.getSystemService(
+                            InputManager.class)),
+                    Looper.myLooper(),
+                    Display.DEFAULT_DISPLAY);
+            addFirstEventHandler(Display.DEFAULT_DISPLAY, mMouseKeysInterceptor);
+        }
     }
 
     /**
@@ -816,6 +837,11 @@
             mKeyboardInterceptor.onDestroy();
             mKeyboardInterceptor = null;
         }
+
+        if (mMouseKeysInterceptor != null) {
+            mMouseKeysInterceptor.onDestroy();
+            mMouseKeysInterceptor = null;
+        }
     }
 
     private MagnificationGestureHandler createMagnificationGestureHandler(
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 32491b7..b918d80 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -57,6 +57,7 @@
 import static com.android.internal.util.FunctionalUtils.ignoreRemoteException;
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain;
+import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
 
 import android.accessibilityservice.AccessibilityGestureEvent;
@@ -2936,6 +2937,9 @@
             if (combinedGenericMotionEventSources != 0) {
                 flags |= AccessibilityInputFilter.FLAG_FEATURE_INTERCEPT_GENERIC_MOTION_EVENTS;
             }
+            if (userState.isMouseKeysEnabled()) {
+                flags |= AccessibilityInputFilter.FLAG_FEATURE_MOUSE_KEYS;
+            }
             if (flags != 0) {
                 if (!mHasInputFilter) {
                     mHasInputFilter = true;
@@ -3216,6 +3220,7 @@
         somethingChanged |= readMagnificationCapabilitiesLocked(userState);
         somethingChanged |= readMagnificationFollowTypingLocked(userState);
         somethingChanged |= readAlwaysOnMagnificationLocked(userState);
+        somethingChanged |= readMouseKeysEnabledLocked(userState);
         return somethingChanged;
     }
 
@@ -5476,6 +5481,9 @@
         private final Uri mAlwaysOnMagnificationUri = Settings.Secure.getUriFor(
                 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED);
 
+        private final Uri mMouseKeysUri = Settings.Secure.getUriFor(
+                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED);
+
         public AccessibilityContentObserver(Handler handler) {
             super(handler);
         }
@@ -5524,6 +5532,8 @@
                     mMagnificationFollowTypingUri, false, this, UserHandle.USER_ALL);
             contentResolver.registerContentObserver(
                     mAlwaysOnMagnificationUri, false, this, UserHandle.USER_ALL);
+            contentResolver.registerContentObserver(
+                    mMouseKeysUri, false, this, UserHandle.USER_ALL);
         }
 
         @Override
@@ -5604,6 +5614,10 @@
                     readMagnificationFollowTypingLocked(userState);
                 } else if (mAlwaysOnMagnificationUri.equals(uri)) {
                     readAlwaysOnMagnificationLocked(userState);
+                } else if (mMouseKeysUri.equals(uri)) {
+                    if (readMouseKeysEnabledLocked(userState)) {
+                        onUserStateChangedLocked(userState);
+                    }
                 }
             }
         }
@@ -5742,6 +5756,20 @@
         return false;
     }
 
+    boolean readMouseKeysEnabledLocked(AccessibilityUserState userState) {
+        if (!keyboardA11yMouseKeys()) {
+            return false;
+        }
+        final boolean isMouseKeysEnabled =
+                Settings.Secure.getIntForUser(mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED, 0, userState.mUserId) == 1;
+        if (isMouseKeysEnabled != userState.isMouseKeysEnabled()) {
+            userState.setMouseKeysEnabled(isMouseKeysEnabled);
+            return true;
+        }
+        return false;
+    }
+
     @Override
     public void setGestureDetectionPassthroughRegion(int displayId, Region region) {
         mMainHandler.sendMessage(
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index 7bcbc27..b061065 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -169,6 +169,8 @@
     private final int mFocusStrokeWidthDefaultValue;
     // The default value of the focus color.
     private final int mFocusColorDefaultValue;
+    /** Whether mouse keys feature is enabled. */
+    private boolean mMouseKeysEnabled = false;
     private final Map<ComponentName, ComponentName> mA11yServiceToTileService = new ArrayMap<>();
     private final Map<ComponentName, ComponentName> mA11yActivityToTileService = new ArrayMap<>();
 
@@ -674,6 +676,14 @@
         mIsFilterKeyEventsEnabled = enabled;
     }
 
+    public void setMouseKeysEnabled(boolean enabled) {
+        mMouseKeysEnabled = enabled;
+    }
+
+    public boolean isMouseKeysEnabled() {
+        return mMouseKeysEnabled;
+    }
+
     public int getInteractiveUiTimeoutLocked() {
         return mInteractiveUiTimeout;
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
new file mode 100644
index 0000000..3f0f23f
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
@@ -0,0 +1,498 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility;
+
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_INPUT_FILTER;
+import static android.util.MathUtils.sqrt;
+
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceParams;
+import android.hardware.input.InputManager;
+import android.hardware.input.VirtualMouse;
+import android.hardware.input.VirtualMouseButtonEvent;
+import android.hardware.input.VirtualMouseConfig;
+import android.hardware.input.VirtualMouseRelativeEvent;
+import android.hardware.input.VirtualMouseScrollEvent;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+
+import com.android.server.LocalServices;
+import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
+
+/**
+ * Implements the "mouse keys" accessibility feature for physical keyboards.
+ *
+ * If enabled, mouse keys will allow users to use a physical keyboard to
+ * control the mouse on the display.
+ * The following mouse functionality is supported by the mouse keys:
+ * <ul>
+ *   <li> Move the mouse pointer in different directions (up, down, left, right and diagonally).
+ *   <li> Click the mouse button (left, right and middle click).
+ *   <li> Press and hold the mouse button.
+ *   <li> Release the mouse button.
+ *   <li> Scroll (up and down).
+ * </ul>
+ *
+ * The keys that are mapped to mouse keys are consumed by {@link AccessibilityInputFilter}.
+ * Non-mouse key {@link KeyEvent} will be passed to the parent handler to be handled as usual.
+ * A new {@link VirtualMouse} is created whenever the mouse keys feature is turned on in Settings.
+ * In case multiple physical keyboard are connected to a device,
+ * mouse keys of each physical keyboard will control a single (global) mouse pointer.
+ */
+public class MouseKeysInterceptor extends BaseEventStreamTransformation implements Handler.Callback,
+        InputManager.InputDeviceListener {
+    private static final String LOG_TAG = "MouseKeysInterceptor";
+
+    // To enable these logs, run: 'adb shell setprop log.tag.MouseKeysInterceptor DEBUG'
+    // (requires restart)
+    private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
+
+    private static final int MESSAGE_MOVE_MOUSE_POINTER = 1;
+    private static final int MESSAGE_SCROLL_MOUSE_POINTER = 2;
+    private static final float MOUSE_POINTER_MOVEMENT_STEP = 1.8f;
+    private static final int KEY_NOT_SET = -1;
+
+    /** Time interval after which mouse action will be repeated */
+    private static final int INTERVAL_MILLIS = 10;
+
+    private final AccessibilityManagerService mAms;
+    private final InputManager mInputManager;
+    private final Handler mHandler;
+
+    private final int mDisplayId;
+
+    VirtualDeviceManager.VirtualDevice mVirtualDevice = null;
+
+    private VirtualMouse mVirtualMouse = null;
+
+    /**
+     * State of the active directional mouse key.
+     * Multiple mouse keys will not be allowed to be used simultaneously i.e.,
+     * once a mouse key is pressed, other mouse key presses will be disregarded
+     * (except for when the "HOLD" key is pressed).
+     */
+    private int mActiveMoveKey = KEY_NOT_SET;
+
+    /** State of the active scroll mouse key. */
+    private int mActiveScrollKey = KEY_NOT_SET;
+
+    /** Last time the key action was performed */
+    private long mLastTimeKeyActionPerformed = 0;
+
+    // TODO (b/346706749): This is currently using the numpad key bindings for mouse keys.
+    //  Decide the final mouse key bindings with UX input.
+    public enum MouseKeyEvent {
+        DIAGONAL_DOWN_LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_1),
+        DOWN_MOVE(KeyEvent.KEYCODE_NUMPAD_2),
+        DIAGONAL_DOWN_RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_3),
+        LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_4),
+        RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_6),
+        DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_7),
+        UP_MOVE(KeyEvent.KEYCODE_NUMPAD_8),
+        DIAGONAL_UP_RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_9),
+        LEFT_CLICK(KeyEvent.KEYCODE_NUMPAD_5),
+        RIGHT_CLICK(KeyEvent.KEYCODE_NUMPAD_DOT),
+        HOLD(KeyEvent.KEYCODE_NUMPAD_MULTIPLY),
+        RELEASE(KeyEvent.KEYCODE_NUMPAD_SUBTRACT),
+        SCROLL_UP(KeyEvent.KEYCODE_A),
+        SCROLL_DOWN(KeyEvent.KEYCODE_S);
+
+        private final int mKeyCode;
+        MouseKeyEvent(int enumValue) {
+            mKeyCode = enumValue;
+        }
+
+        private static final SparseArray<MouseKeyEvent> VALUE_TO_ENUM_MAP = new SparseArray<>();
+
+        static {
+            for (MouseKeyEvent type : MouseKeyEvent.values()) {
+                VALUE_TO_ENUM_MAP.put(type.mKeyCode, type);
+            }
+        }
+
+        public final int getKeyCodeValue() {
+            return mKeyCode;
+        }
+
+        /**
+         * Convert int value of the key code to corresponding MouseEvent enum. If no matching
+         * value is found, this will return {@code null}.
+         */
+        @Nullable
+        public static MouseKeyEvent from(int value) {
+            return VALUE_TO_ENUM_MAP.get(value);
+        }
+    }
+
+    /**
+     * Construct a new MouseKeysInterceptor.
+     *
+     * @param service The service to notify of key events
+     * @param inputManager InputManager to track changes to connected input devices
+     * @param looper Looper to use for callbacks and messages
+     * @param displayId Display ID to send mouse events to
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public MouseKeysInterceptor(AccessibilityManagerService service, InputManager inputManager,
+            Looper looper, int displayId) {
+        mAms = service;
+        mInputManager = inputManager;
+        mHandler = new Handler(looper, this);
+        mInputManager.registerInputDeviceListener(this, mHandler);
+        mDisplayId = displayId;
+        // Create the virtual mouse on a separate thread since virtual device creation
+        // should happen on an auxiliary thread, and not from the handler's thread.
+        new Thread(() -> {
+            mVirtualMouse = createVirtualMouse();
+        }).start();
+
+    }
+
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    private void sendVirtualMouseRelativeEvent(float x, float y) {
+        if (mVirtualMouse != null) {
+            mVirtualMouse.sendRelativeEvent(new VirtualMouseRelativeEvent.Builder()
+                    .setRelativeX(x)
+                    .setRelativeY(y)
+                    .build()
+            );
+        }
+    }
+
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    private void sendVirtualMouseButtonEvent(int buttonCode, int actionCode) {
+        if (mVirtualMouse != null) {
+            mVirtualMouse.sendButtonEvent(new VirtualMouseButtonEvent.Builder()
+                    .setAction(actionCode)
+                    .setButtonCode(buttonCode)
+                    .build()
+            );
+        }
+    }
+
+    /**
+     * Performs a mouse scroll action based on the provided key code.
+     * This method interprets the key code as a mouse scroll and sends
+     * the corresponding {@code VirtualMouseScrollEvent#mYAxisMovement}.
+
+     * @param keyCode The key code representing the mouse scroll action.
+     *                Supported keys are:
+     *                <ul>
+     *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent SCROLL_UP}
+     *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent SCROLL_DOWN}
+     *                </ul>
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    private void performMouseScrollAction(int keyCode) {
+        MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(keyCode);
+        float y = switch (mouseKeyEvent) {
+            case SCROLL_UP -> 1.0f;
+            case SCROLL_DOWN -> -1.0f;
+            default -> 0.0f;
+        };
+        if (mVirtualMouse != null) {
+            mVirtualMouse.sendScrollEvent(new VirtualMouseScrollEvent.Builder()
+                    .setYAxisMovement(y)
+                    .build()
+            );
+        }
+        if (DEBUG) {
+            Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name()
+                    + " for scroll action with axis movement (y=" + y + ")");
+        }
+    }
+
+    /**
+     * Performs a mouse button action based on the provided key code.
+     * This method interprets the key code as a mouse button press and sends
+     * the corresponding press and release events to the virtual mouse.
+
+     * @param keyCode The key code representing the mouse button action.
+     *                Supported keys are:
+     *                <ul>
+     *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent LEFT_CLICK} (Primary Button)
+     *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent RIGHT_CLICK} (Secondary
+     *                  Button)
+     *                </ul>
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    private void performMouseButtonAction(int keyCode) {
+        MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(keyCode);
+        int buttonCode = switch (mouseKeyEvent) {
+            case LEFT_CLICK -> VirtualMouseButtonEvent.BUTTON_PRIMARY;
+            case RIGHT_CLICK -> VirtualMouseButtonEvent.BUTTON_SECONDARY;
+            default -> VirtualMouseButtonEvent.BUTTON_UNKNOWN;
+        };
+        if (buttonCode != VirtualMouseButtonEvent.BUTTON_UNKNOWN) {
+            sendVirtualMouseButtonEvent(buttonCode, VirtualMouseButtonEvent.ACTION_BUTTON_PRESS);
+            sendVirtualMouseButtonEvent(buttonCode, VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE);
+        }
+        if (DEBUG) {
+            if (buttonCode == VirtualMouseButtonEvent.BUTTON_UNKNOWN) {
+                Slog.d(LOG_TAG, "Button code is unknown for mouse key event: "
+                        + mouseKeyEvent.name());
+            } else {
+                Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name()
+                        + " for button action");
+            }
+        }
+    }
+
+    /**
+     * Performs a mouse pointer action based on the provided key code.
+     * The method calculates the relative movement of the mouse pointer
+     * and sends the corresponding event to the virtual mouse.
+     *
+     * @param keyCode The key code representing the direction or button press.
+     *                Supported keys are:
+     *                <ul>
+     *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_DOWN_LEFT}
+     *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent DOWN}
+     *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_DOWN_RIGHT}
+     *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent LEFT}
+     *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent RIGHT}
+     *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_UP_LEFT}
+     *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent UP}
+     *                  <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_UP_RIGHT}
+     *                </ul>
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    private void performMousePointerAction(int keyCode) {
+        float x = 0f;
+        float y = 0f;
+        MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(keyCode);
+        switch (mouseKeyEvent) {
+            case DIAGONAL_DOWN_LEFT_MOVE -> {
+                x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
+                y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
+            }
+            case DOWN_MOVE -> {
+                y = MOUSE_POINTER_MOVEMENT_STEP;
+            }
+            case DIAGONAL_DOWN_RIGHT_MOVE -> {
+                x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
+                y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
+            }
+            case LEFT_MOVE -> {
+                x = -MOUSE_POINTER_MOVEMENT_STEP;
+            }
+            case RIGHT_MOVE -> {
+                x = MOUSE_POINTER_MOVEMENT_STEP;
+            }
+            case DIAGONAL_UP_LEFT_MOVE -> {
+                x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
+                y = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
+            }
+            case UP_MOVE -> {
+                y = -MOUSE_POINTER_MOVEMENT_STEP;
+            }
+            case DIAGONAL_UP_RIGHT_MOVE -> {
+                x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
+                y = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
+            }
+            default -> {
+                x = 0.0f;
+                y = 0.0f;
+            }
+        }
+        sendVirtualMouseRelativeEvent(x, y);
+        if (DEBUG) {
+            Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name()
+                    + " for relative pointer movement (x=" + x + ", y=" + y + ")");
+        }
+    }
+
+    private boolean isMouseKey(int keyCode) {
+        return MouseKeyEvent.VALUE_TO_ENUM_MAP.contains(keyCode);
+    }
+
+    private boolean isMouseButtonKey(int keyCode) {
+        return keyCode == MouseKeyEvent.LEFT_CLICK.getKeyCodeValue()
+                || keyCode == MouseKeyEvent.RIGHT_CLICK.getKeyCodeValue();
+    }
+
+    private boolean isMouseScrollKey(int keyCode) {
+        return keyCode == MouseKeyEvent.SCROLL_UP.getKeyCodeValue()
+                || keyCode == MouseKeyEvent.SCROLL_DOWN.getKeyCodeValue();
+    }
+
+    /**
+     * Create a virtual mouse using the VirtualDeviceManagerInternal.
+     *
+     * @return The created VirtualMouse.
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    private VirtualMouse createVirtualMouse() {
+        final VirtualDeviceManagerInternal localVdm =
+                LocalServices.getService(VirtualDeviceManagerInternal.class);
+        mVirtualDevice = localVdm.createVirtualDevice(
+                new VirtualDeviceParams.Builder().setName("Mouse Keys Virtual Device").build());
+        VirtualMouse virtualMouse = mVirtualDevice.createVirtualMouse(
+                new VirtualMouseConfig.Builder()
+                .setInputDeviceName("Mouse Keys Virtual Mouse")
+                .setAssociatedDisplayId(mDisplayId)
+                .build());
+        return virtualMouse;
+    }
+
+    /**
+     * Handles key events and forwards mouse key events to the virtual mouse.
+     *
+     * @param event The key event to handle.
+     * @param policyFlags The policy flags associated with the key event.
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    @Override
+    public void onKeyEvent(KeyEvent event, int policyFlags) {
+        if (mAms.getTraceManager().isA11yTracingEnabledForTypes(FLAGS_INPUT_FILTER)) {
+            mAms.getTraceManager().logTrace(LOG_TAG + ".onKeyEvent",
+                    FLAGS_INPUT_FILTER, "event=" + event + ";policyFlags=" + policyFlags);
+        }
+        boolean isDown = event.getAction() == KeyEvent.ACTION_DOWN;
+        int keyCode = event.getKeyCode();
+
+        if (!isMouseKey(keyCode)) {
+            // Pass non-mouse key events to the next handler
+            super.onKeyEvent(event, policyFlags);
+        } else if (keyCode == MouseKeyEvent.HOLD.getKeyCodeValue()) {
+            sendVirtualMouseButtonEvent(VirtualMouseButtonEvent.BUTTON_PRIMARY,
+                    VirtualMouseButtonEvent.ACTION_BUTTON_PRESS);
+        } else if (keyCode == MouseKeyEvent.RELEASE.getKeyCodeValue()) {
+            sendVirtualMouseButtonEvent(VirtualMouseButtonEvent.BUTTON_PRIMARY,
+                    VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE);
+        } else if (isDown && isMouseButtonKey(keyCode)) {
+            performMouseButtonAction(keyCode);
+        } else if (isDown && isMouseScrollKey(keyCode)) {
+            // If the scroll key is pressed down and no other key is active,
+            // set it as the active key and send a message to scroll the pointer
+            if (mActiveScrollKey == KEY_NOT_SET) {
+                mActiveScrollKey = keyCode;
+                mLastTimeKeyActionPerformed = event.getDownTime();
+                mHandler.sendEmptyMessage(MESSAGE_SCROLL_MOUSE_POINTER);
+            }
+        } else if (isDown) {
+            // This is a directional key.
+            // If the key is pressed down and no other key is active,
+            // set it as the active key and send a message to move the pointer
+            if (mActiveMoveKey == KEY_NOT_SET) {
+                mActiveMoveKey = keyCode;
+                mLastTimeKeyActionPerformed = event.getDownTime();
+                mHandler.sendEmptyMessage(MESSAGE_MOVE_MOUSE_POINTER);
+            }
+        } else if (mActiveMoveKey == keyCode) {
+            // If the key is released, and it is the active key, stop moving the pointer
+            mActiveMoveKey = KEY_NOT_SET;
+            mHandler.removeMessages(MESSAGE_MOVE_MOUSE_POINTER);
+        } else if (mActiveScrollKey == keyCode) {
+            // If the key is released, and it is the active key, stop scrolling the pointer
+            mActiveScrollKey = KEY_NOT_SET;
+            mHandler.removeMessages(MESSAGE_SCROLL_MOUSE_POINTER);
+        } else {
+            Slog.i(LOG_TAG, "Dropping event with key code: '" + keyCode
+                    + "', with no matching down event from deviceId = " + event.getDeviceId());
+        }
+    }
+
+    /**
+     * Handle messages for moving or scrolling the mouse pointer.
+     *
+     * @param msg The message to handle.
+     * @return True if the message was handled, false otherwise.
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    @Override
+    public boolean handleMessage(Message msg) {
+        switch (msg.what) {
+            case MESSAGE_MOVE_MOUSE_POINTER ->
+                    handleMouseMessage(msg.getWhen(), mActiveMoveKey, MESSAGE_MOVE_MOUSE_POINTER);
+            case MESSAGE_SCROLL_MOUSE_POINTER ->
+                    handleMouseMessage(msg.getWhen(), mActiveScrollKey,
+                            MESSAGE_SCROLL_MOUSE_POINTER);
+            default -> {
+                Slog.e(LOG_TAG, "Unexpected message type");
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Handles mouse-related messages for moving or scrolling the mouse pointer.
+     * This method checks if the specified time interval {@code INTERVAL_MILLIS} has passed since
+     * the last movement or scroll action and performs the corresponding action if necessary.
+     * If there is an active key, the message is rescheduled to be handled again
+     * after the specified {@code INTERVAL_MILLIS}.
+     *
+     * @param currentTime The current time when the message is being handled.
+     * @param activeKey The key code representing the active key. This determines
+     *                  the direction or type of action to be performed.
+     * @param messageType The type of message to be handled. It can be one of the
+     *                    following:
+     *                    <ul>
+     *                      <li>{@link #MESSAGE_MOVE_MOUSE_POINTER} - for moving the mouse pointer.
+     *                      <li>{@link #MESSAGE_SCROLL_MOUSE_POINTER} - for scrolling mouse pointer.
+     *                    </ul>
+     */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void handleMouseMessage(long currentTime, int activeKey, int messageType) {
+        if (currentTime - mLastTimeKeyActionPerformed >= INTERVAL_MILLIS) {
+            if (messageType == MESSAGE_MOVE_MOUSE_POINTER) {
+                performMousePointerAction(activeKey);
+            } else if (messageType == MESSAGE_SCROLL_MOUSE_POINTER) {
+                performMouseScrollAction(activeKey);
+            }
+            mLastTimeKeyActionPerformed = currentTime;
+        }
+        if (activeKey != KEY_NOT_SET) {
+            // Reschedule the message if the key is still active
+            mHandler.sendEmptyMessageDelayed(messageType, INTERVAL_MILLIS);
+        }
+    }
+
+    @Override
+    public void onInputDeviceAdded(int deviceId) {
+    }
+
+    @Override
+    public void onInputDeviceRemoved(int deviceId) {
+    }
+
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    @Override
+    public void onDestroy() {
+        // Clear mouse state
+        mActiveMoveKey = KEY_NOT_SET;
+        mActiveScrollKey = KEY_NOT_SET;
+        mLastTimeKeyActionPerformed = 0;
+        mHandler.removeCallbacksAndMessages(null);
+
+        mVirtualDevice.close();
+        mInputManager.unregisterInputDeviceListener(this);
+    }
+
+    @Override
+    public void onInputDeviceChanged(int deviceId) {
+    }
+
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
index eb78fe6..a118415 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
@@ -20,7 +20,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.hardware.biometrics.BiometricConstants;
-import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -28,11 +27,12 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
+import com.android.modules.expresslog.Counter;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.Arrays;
-import java.util.function.BooleanSupplier;
+
 
 /**
  * Contains all the necessary information for a HAL operation.
@@ -89,8 +89,6 @@
     private final BaseClientMonitor mClientMonitor;
     @Nullable
     private final ClientMonitorCallback mClientCallback;
-    @NonNull
-    private final BooleanSupplier mIsDebuggable;
     @Nullable
     private ClientMonitorCallback mOnStartCallback;
     @OperationState
@@ -99,6 +97,7 @@
     @NonNull
     final Runnable mCancelWatchdog;
 
+    @VisibleForTesting
     BiometricSchedulerOperation(
             @NonNull BaseClientMonitor clientMonitor,
             @Nullable ClientMonitorCallback callback
@@ -106,33 +105,14 @@
         this(clientMonitor, callback, STATE_WAITING_IN_QUEUE);
     }
 
-    @VisibleForTesting
-    BiometricSchedulerOperation(
-            @NonNull BaseClientMonitor clientMonitor,
-            @Nullable ClientMonitorCallback callback,
-            @NonNull BooleanSupplier isDebuggable
-    ) {
-        this(clientMonitor, callback, STATE_WAITING_IN_QUEUE, isDebuggable);
-    }
-
     protected BiometricSchedulerOperation(
             @NonNull BaseClientMonitor clientMonitor,
             @Nullable ClientMonitorCallback callback,
             @OperationState int state
     ) {
-        this(clientMonitor, callback, state, Build::isDebuggable);
-    }
-
-    private BiometricSchedulerOperation(
-            @NonNull BaseClientMonitor clientMonitor,
-            @Nullable ClientMonitorCallback callback,
-            @OperationState int state,
-            @NonNull BooleanSupplier isDebuggable
-    ) {
         mClientMonitor = clientMonitor;
         mClientCallback = callback;
         mState = state;
-        mIsDebuggable = isDebuggable;
         mCancelWatchdog = () -> {
             if (!isFinished()) {
                 Slog.e(TAG, "[Watchdog Triggered]: " + this);
@@ -187,9 +167,7 @@
 
         if (mClientMonitor.getCookie() != 0) {
             String err = "operation requires cookie";
-            if (mIsDebuggable.getAsBoolean()) {
-                throw new IllegalStateException(err);
-            }
+            Counter.logIncrement("biometric.value_biometric_scheduler_operation_state_error_count");
             Slog.e(TAG, err);
         }
 
@@ -456,10 +434,9 @@
     private boolean errorWhenOneOf(String op, @OperationState int... states) {
         final boolean isError = ArrayUtils.contains(states, mState);
         if (isError) {
-            String err = op + ": mState must not be " + mState;
-            if (mIsDebuggable.getAsBoolean()) {
-                throw new IllegalStateException(err);
-            }
+            Counter.logIncrement(
+                    "biometric.value_biometric_scheduler_operation_state_error_count");
+            final String err = op + ": mState must not be " + mState;
             Slog.e(TAG, err);
         }
         return isError;
@@ -468,10 +445,10 @@
     private boolean errorWhenNoneOf(String op, @OperationState int... states) {
         final boolean isError = !ArrayUtils.contains(states, mState);
         if (isError) {
-            String err = op + ": mState=" + mState + " must be one of " + Arrays.toString(states);
-            if (mIsDebuggable.getAsBoolean()) {
-                throw new IllegalStateException(err);
-            }
+            Counter.logIncrement(
+                    "biometric.value_biometric_scheduler_operation_state_error_count");
+            final String err = op + ": mState=" + mState + " must be one of "
+                    + Arrays.toString(states);
             Slog.e(TAG, err);
         }
         return isError;
diff --git a/services/core/java/com/android/server/location/altitude/AltitudeService.java b/services/core/java/com/android/server/location/altitude/AltitudeService.java
index 289d4a2..96540c2 100644
--- a/services/core/java/com/android/server/location/altitude/AltitudeService.java
+++ b/services/core/java/com/android/server/location/altitude/AltitudeService.java
@@ -25,6 +25,7 @@
 import android.location.Location;
 import android.location.altitude.AltitudeConverter;
 import android.os.RemoteException;
+import android.util.Log;
 
 import com.android.server.SystemService;
 
@@ -38,6 +39,8 @@
  */
 public class AltitudeService extends IAltitudeService.Stub {
 
+    private static final String TAG = "AltitudeService";
+
     private final AltitudeConverter mAltitudeConverter = new AltitudeConverter();
     private final Context mContext;
 
@@ -59,6 +62,7 @@
         try {
             mAltitudeConverter.addMslAltitudeToLocation(mContext, location);
         } catch (IOException e) {
+            Log.e(TAG, "", e);
             response.success = false;
             return response;
         }
@@ -74,6 +78,7 @@
         try {
             return mAltitudeConverter.getGeoidHeight(mContext, request);
         } catch (IOException e) {
+            Log.e(TAG, "", e);
             GetGeoidHeightResponse response = new GetGeoidHeightResponse();
             response.success = false;
             return response;
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index c9ba683..9d0c0e9 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3568,24 +3568,6 @@
                 Slog.wtf(TAG, "KEYCODE_VOICE_ASSIST should be handled in"
                         + " interceptKeyBeforeQueueing");
                 return true;
-            case KeyEvent.KEYCODE_VIDEO_APP_1:
-            case KeyEvent.KEYCODE_VIDEO_APP_2:
-            case KeyEvent.KEYCODE_VIDEO_APP_3:
-            case KeyEvent.KEYCODE_VIDEO_APP_4:
-            case KeyEvent.KEYCODE_VIDEO_APP_5:
-            case KeyEvent.KEYCODE_VIDEO_APP_6:
-            case KeyEvent.KEYCODE_VIDEO_APP_7:
-            case KeyEvent.KEYCODE_VIDEO_APP_8:
-            case KeyEvent.KEYCODE_FEATURED_APP_1:
-            case KeyEvent.KEYCODE_FEATURED_APP_2:
-            case KeyEvent.KEYCODE_FEATURED_APP_3:
-            case KeyEvent.KEYCODE_FEATURED_APP_4:
-            case KeyEvent.KEYCODE_DEMO_APP_1:
-            case KeyEvent.KEYCODE_DEMO_APP_2:
-            case KeyEvent.KEYCODE_DEMO_APP_3:
-            case KeyEvent.KEYCODE_DEMO_APP_4:
-                Slog.wtf(TAG, "KEYCODE_APP_X should be handled in interceptKeyBeforeQueueing");
-                return true;
             case KeyEvent.KEYCODE_BRIGHTNESS_UP:
             case KeyEvent.KEYCODE_BRIGHTNESS_DOWN:
                 if (down) {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index f5757dc..e81b440 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -38,6 +38,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.Intent.ACTION_VIEW;
 import static android.content.pm.PackageManager.NOTIFY_PACKAGE_USE_ACTIVITY;
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
@@ -121,6 +122,7 @@
 import android.graphics.Rect;
 import android.hardware.SensorPrivacyManager;
 import android.hardware.SensorPrivacyManagerInternal;
+import android.net.Uri;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
@@ -142,6 +144,7 @@
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.view.Display;
+import android.webkit.URLUtil;
 import android.window.ActivityWindowInfo;
 
 import com.android.internal.R;
@@ -158,6 +161,7 @@
 import com.android.server.pm.SaferIntentUtils;
 import com.android.server.utils.Slogf;
 import com.android.server.wm.ActivityMetricsLogger.LaunchingState;
+import com.android.window.flags.Flags;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -2900,6 +2904,9 @@
 
         @Override
         public void accept(ActivityRecord r) {
+            if (Flags.enableDesktopWindowingAppToWeb() && mInfo.capturedLink == null) {
+                setCapturedLink(r);
+            }
             if (r.mLaunchCookie != null) {
                 mInfo.addLaunchCookie(r.mLaunchCookie);
             }
@@ -2912,6 +2919,16 @@
                 mTopRunning = r;
             }
         }
+
+        private void setCapturedLink(ActivityRecord r) {
+            final Uri uri = r.intent.getData();
+            if (uri == null || !ACTION_VIEW.equals(r.intent.getAction())
+                    || !URLUtil.isNetworkUrl(uri.toString())) {
+                return;
+            }
+            mInfo.capturedLink = uri;
+            mInfo.capturedLinkTimestamp = r.lastLaunchTime;
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 9a5f84c..8033122 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9299,7 +9299,8 @@
                 isTrustedOverlay);
 
         final int sanitizedLpFlags =
-                (flags & (FLAG_NOT_TOUCHABLE | FLAG_SLIPPERY | LayoutParams.FLAG_NOT_FOCUSABLE))
+                (flags & (FLAG_NOT_TOUCHABLE | FLAG_SLIPPERY | LayoutParams.FLAG_NOT_FOCUSABLE
+                    | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH))
                 | LayoutParams.FLAG_NOT_TOUCH_MODAL;
         h.layoutParamsType = type;
         h.layoutParamsFlags = sanitizedLpFlags;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
new file mode 100644
index 0000000..dc8d239
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility
+
+import android.companion.virtual.VirtualDeviceManager
+import android.companion.virtual.VirtualDeviceParams
+import android.content.Context
+import android.hardware.input.IInputManager
+import android.hardware.input.InputManager
+import android.hardware.input.InputManagerGlobal
+import android.hardware.input.VirtualMouse
+import android.hardware.input.VirtualMouseButtonEvent
+import android.hardware.input.VirtualMouseConfig
+import android.hardware.input.VirtualMouseRelativeEvent
+import android.hardware.input.VirtualMouseScrollEvent
+import android.os.RemoteException
+import android.os.test.TestLooper
+import android.platform.test.annotations.Presubmit
+import android.view.KeyEvent
+import androidx.test.core.app.ApplicationProvider
+import com.android.server.companion.virtual.VirtualDeviceManagerInternal
+import com.android.server.LocalServices
+import com.android.server.testutils.OffsettableClock
+import junit.framework.Assert.assertEquals
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import java.util.concurrent.TimeUnit
+import java.util.LinkedList
+import java.util.Queue
+import android.util.ArraySet
+
+/**
+ * Tests for {@link MouseKeysInterceptor}
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:MouseKeysInterceptorTest
+ */
+@Presubmit
+class MouseKeysInterceptorTest {
+    companion object {
+        const val DISPLAY_ID = 1
+        const val DEVICE_ID = 123
+        // This delay is required for key events to be sent and handled correctly.
+        // The handler only performs a move/scroll event if it receives the key event
+        // at INTERVAL_MILLIS (which happens in practice). Hence, we need this delay in the tests.
+        const val KEYBOARD_POST_EVENT_DELAY_MILLIS = 20L
+    }
+
+    private lateinit var mouseKeysInterceptor: MouseKeysInterceptor
+    private val clock = OffsettableClock()
+    private val testLooper = TestLooper { clock.now() }
+    private val nextInterceptor = TrackingInterceptor()
+
+    @Mock
+    private lateinit var mockAms: AccessibilityManagerService
+
+    @Mock
+    private lateinit var iInputManager: IInputManager
+    private lateinit var testSession: InputManagerGlobal.TestSession
+    private lateinit var mockInputManager: InputManager
+
+    @Mock
+    private lateinit var mockVirtualDeviceManagerInternal: VirtualDeviceManagerInternal
+    @Mock
+    private lateinit var mockVirtualDevice: VirtualDeviceManager.VirtualDevice
+    @Mock
+    private lateinit var mockVirtualMouse: VirtualMouse
+
+    @Mock
+    private lateinit var mockTraceManager: AccessibilityTraceManager
+
+    @Before
+    @Throws(RemoteException::class)
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        testSession = InputManagerGlobal.createTestSession(iInputManager)
+        mockInputManager = InputManager(context)
+
+        Mockito.`when`(mockVirtualDeviceManagerInternal.getDeviceIdsForUid(Mockito.anyInt()))
+            .thenReturn(ArraySet(setOf(DEVICE_ID)))
+        LocalServices.removeServiceForTest(VirtualDeviceManagerInternal::class.java)
+        LocalServices.addService<VirtualDeviceManagerInternal>(
+            VirtualDeviceManagerInternal::class.java, mockVirtualDeviceManagerInternal
+        )
+
+        Mockito.`when`(mockVirtualDeviceManagerInternal.createVirtualDevice(
+            Mockito.any(VirtualDeviceParams::class.java)
+        )).thenReturn(mockVirtualDevice)
+        Mockito.`when`(mockVirtualDevice.createVirtualMouse(
+            Mockito.any(VirtualMouseConfig::class.java)
+        )).thenReturn(mockVirtualMouse)
+
+        Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID))
+        Mockito.`when`(mockAms.traceManager).thenReturn(mockTraceManager)
+
+        mouseKeysInterceptor = MouseKeysInterceptor(mockAms, mockInputManager,
+            testLooper.looper, DISPLAY_ID)
+        // VirtualMouse is created on a separate thread.
+        // Wait for VirtualMouse to be created before running tests
+        TimeUnit.MILLISECONDS.sleep(20L)
+        mouseKeysInterceptor.next = nextInterceptor
+    }
+
+    @After
+    fun tearDown() {
+        testLooper.dispatchAll()
+        if (this::testSession.isInitialized) {
+            testSession.close()
+        }
+    }
+
+    @Test
+    fun whenNonMouseKeyEventArrives_eventIsPassedToNextInterceptor() {
+        val downTime = clock.now()
+        val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+            KeyEvent.KEYCODE_Q, 0, 0, DEVICE_ID, 0)
+        mouseKeysInterceptor.onKeyEvent(downEvent, 0)
+        testLooper.dispatchAll()
+
+        assertEquals(1, nextInterceptor.events.size)
+        assertEquals(downEvent, nextInterceptor.events.poll())
+    }
+
+    @Test
+    fun whenMouseDirectionalKeyIsPressed_relativeEventIsSent() {
+        // There should be some delay between the downTime of the key event and calling onKeyEvent
+        val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
+        val keyCode = MouseKeysInterceptor.MouseKeyEvent.DOWN_MOVE.getKeyCodeValue()
+        val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+            keyCode, 0, 0, DEVICE_ID, 0)
+
+        mouseKeysInterceptor.onKeyEvent(downEvent, 0)
+        testLooper.dispatchAll()
+
+        // Verify the sendRelativeEvent method is called once and capture the arguments
+        verifyRelativeEvents(arrayOf<Float>(0f), arrayOf<Float>(1.8f))
+    }
+
+    @Test
+    fun whenClickKeyIsPressed_buttonEventIsSent() {
+        // There should be some delay between the downTime of the key event and calling onKeyEvent
+        val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
+        val keyCode = MouseKeysInterceptor.MouseKeyEvent.LEFT_CLICK.getKeyCodeValue()
+        val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+            keyCode, 0, 0, DEVICE_ID, 0)
+        mouseKeysInterceptor.onKeyEvent(downEvent, 0)
+        testLooper.dispatchAll()
+
+        val actions = arrayOf<Int>(
+            VirtualMouseButtonEvent.ACTION_BUTTON_PRESS,
+            VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE)
+        val buttons = arrayOf<Int>(
+            VirtualMouseButtonEvent.BUTTON_PRIMARY,
+            VirtualMouseButtonEvent.BUTTON_PRIMARY)
+        // Verify the sendButtonEvent method is called twice and capture the arguments
+        verifyButtonEvents(actions, buttons)
+    }
+
+    @Test
+    fun whenHoldKeyIsPressed_buttonEventIsSent() {
+        val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
+        val keyCode = MouseKeysInterceptor.MouseKeyEvent.HOLD.getKeyCodeValue()
+        val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+            keyCode, 0, 0, DEVICE_ID, 0)
+        mouseKeysInterceptor.onKeyEvent(downEvent, 0)
+        testLooper.dispatchAll()
+
+        // Verify the sendButtonEvent method is called once and capture the arguments
+        verifyButtonEvents(
+            arrayOf<Int>( VirtualMouseButtonEvent.ACTION_BUTTON_PRESS),
+            arrayOf<Int>( VirtualMouseButtonEvent.BUTTON_PRIMARY)
+        )
+    }
+
+    @Test
+    fun whenReleaseKeyIsPressed_buttonEventIsSent() {
+        val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
+        val keyCode = MouseKeysInterceptor.MouseKeyEvent.RELEASE.getKeyCodeValue()
+        val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+            keyCode, 0, 0, DEVICE_ID, 0)
+        mouseKeysInterceptor.onKeyEvent(downEvent, 0)
+        testLooper.dispatchAll()
+
+        // Verify the sendButtonEvent method is called once and capture the arguments
+        verifyButtonEvents(
+            arrayOf<Int>( VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE),
+            arrayOf<Int>( VirtualMouseButtonEvent.BUTTON_PRIMARY)
+        )
+    }
+
+    @Test
+    fun whenScrollUpKeyIsPressed_scrollEventIsSent() {
+        // There should be some delay between the downTime of the key event and calling onKeyEvent
+        val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
+        val keyCode = MouseKeysInterceptor.MouseKeyEvent.SCROLL_UP.getKeyCodeValue()
+        val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+            keyCode, 0, 0, DEVICE_ID, 0)
+
+        mouseKeysInterceptor.onKeyEvent(downEvent, 0)
+        testLooper.dispatchAll()
+
+        // Verify the sendScrollEvent method is called once and capture the arguments
+        verifyScrollEvents(arrayOf<Float>(0f), arrayOf<Float>(1.0f))
+    }
+
+    private fun verifyRelativeEvents(expectedX: Array<Float>, expectedY: Array<Float>) {
+        assertEquals(expectedX.size, expectedY.size)
+        val captor = ArgumentCaptor.forClass(VirtualMouseRelativeEvent::class.java)
+        Mockito.verify(mockVirtualMouse, Mockito.times(expectedX.size))
+            .sendRelativeEvent(captor.capture())
+
+        for (i in expectedX.indices) {
+            val captorEvent = captor.allValues[i]
+            assertEquals(expectedX[i], captorEvent.relativeX)
+            assertEquals(expectedY[i], captorEvent.relativeY)
+        }
+    }
+
+    private fun verifyButtonEvents(actions: Array<Int>, buttons: Array<Int>) {
+        assertEquals(actions.size, buttons.size)
+        val captor = ArgumentCaptor.forClass(VirtualMouseButtonEvent::class.java)
+        Mockito.verify(mockVirtualMouse, Mockito.times(actions.size))
+                .sendButtonEvent(captor.capture())
+
+        for (i in actions.indices) {
+            val captorEvent = captor.allValues[i]
+            assertEquals(actions[i], captorEvent.action)
+            assertEquals(buttons[i], captorEvent.buttonCode)
+        }
+    }
+
+    private fun verifyScrollEvents(xAxisMovements: Array<Float>, yAxisMovements: Array<Float>) {
+        assertEquals(xAxisMovements.size, yAxisMovements.size)
+        val captor = ArgumentCaptor.forClass(VirtualMouseScrollEvent::class.java)
+        Mockito.verify(mockVirtualMouse, Mockito.times(xAxisMovements.size))
+            .sendScrollEvent(captor.capture())
+
+        for (i in xAxisMovements.indices) {
+            val captorEvent = captor.allValues[i]
+            assertEquals(xAxisMovements[i], captorEvent.xAxisMovement)
+            assertEquals(yAxisMovements[i], captorEvent.yAxisMovement)
+        }
+    }
+
+    private class TrackingInterceptor : BaseEventStreamTransformation() {
+        val events: Queue<KeyEvent> = LinkedList()
+
+        override fun onKeyEvent(event: KeyEvent, policyFlags: Int) {
+            events.add(event)
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
index f6da411..ffc7811 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
@@ -28,7 +28,6 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
 
 import android.hardware.biometrics.BiometricConstants;
 import android.os.Handler;
@@ -88,16 +87,14 @@
     private Handler mHandler;
     private BiometricSchedulerOperation mInterruptableOperation;
     private BiometricSchedulerOperation mNonInterruptableOperation;
-    private boolean mIsDebuggable;
 
     @Before
     public void setUp() {
         mHandler = new Handler(TestableLooper.get(this).getLooper());
-        mIsDebuggable = false;
         mInterruptableOperation = new BiometricSchedulerOperation(mInterruptableClientMonitor,
-                mClientCallback, () -> mIsDebuggable);
+                mClientCallback);
         mNonInterruptableOperation = new BiometricSchedulerOperation(mNonInterruptableClientMonitor,
-                mClientCallback, () -> mIsDebuggable);
+                mClientCallback);
 
         when(mInterruptableClientMonitor.isInterruptable()).thenReturn(true);
         when(mNonInterruptableClientMonitor.isInterruptable()).thenReturn(false);
@@ -143,32 +140,13 @@
     }
 
     @Test
-    public void testSecondStartWithCookieCrashesWhenDebuggable() {
+    public void testSecondStartWithCookieFails() {
         final int cookie = 5;
-        mIsDebuggable = true;
         when(mInterruptableClientMonitor.getCookie()).thenReturn(cookie);
         when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, cookie);
-        assertThat(started).isTrue();
-
-        assertThrows(IllegalStateException.class,
-                () -> mInterruptableOperation.startWithCookie(mOnStartCallback, cookie));
-    }
-
-    @Test
-    public void testSecondStartWithCookieFailsNicelyWhenNotDebuggable() {
-        final int cookie = 5;
-        mIsDebuggable = false;
-        when(mInterruptableClientMonitor.getCookie()).thenReturn(cookie);
-        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
-
-        final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, cookie);
-        assertThat(started).isTrue();
-
-        final boolean startedAgain = mInterruptableOperation.startWithCookie(mOnStartCallback,
-                cookie);
-        assertThat(startedAgain).isFalse();
+        assertThat(mInterruptableOperation.startWithCookie(mOnStartCallback, cookie)).isTrue();
+        assertThat(mInterruptableOperation.startWithCookie(mOnStartCallback, cookie)).isFalse();
     }
 
     @Test
@@ -217,56 +195,23 @@
     }
 
     @Test
-    public void secondStartCrashesWhenDebuggable() {
-        mIsDebuggable = true;
+    public void secondStartFails() {
         when(mInterruptableClientMonitor.getCookie()).thenReturn(0);
         when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
-        final boolean started = mInterruptableOperation.start(mOnStartCallback);
-        assertThat(started).isTrue();
-
-        assertThrows(IllegalStateException.class, () -> mInterruptableOperation.start(
-                mOnStartCallback));
-    }
-
-    @Test
-    public void secondStartFailsNicelyWhenNotDebuggable() {
-        mIsDebuggable = false;
-        when(mInterruptableClientMonitor.getCookie()).thenReturn(0);
-        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
-
-        final boolean started = mInterruptableOperation.start(mOnStartCallback);
-        assertThat(started).isTrue();
-
-        final boolean startedAgain = mInterruptableOperation.start(mOnStartCallback);
-        assertThat(startedAgain).isFalse();
+        assertThat(mInterruptableOperation.start(mOnStartCallback)).isTrue();
+        assertThat(mInterruptableOperation.start(mOnStartCallback)).isFalse();
     }
 
     @Test
     public void doesNotStartWithCookie() {
-        // This class only throws exceptions when debuggable.
-        mIsDebuggable = true;
         when(mInterruptableClientMonitor.getCookie()).thenReturn(9);
-        assertThrows(IllegalStateException.class,
-                () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class)));
+
+        assertThat(mInterruptableOperation.start(mock(ClientMonitorCallback.class))).isFalse();
     }
 
     @Test
-    public void cannotRestart() {
-        // This class only throws exceptions when debuggable.
-        mIsDebuggable = true;
-        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
-
-        mInterruptableOperation.start(mOnStartCallback);
-
-        assertThrows(IllegalStateException.class,
-                () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class)));
-    }
-
-    @Test
-    public void abortsNotRunning() {
-        // This class only throws exceptions when debuggable.
-        mIsDebuggable = true;
+    public void abortSuccessfulIfOperationNotRunning() {
         when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
         mInterruptableOperation.abort();
@@ -274,28 +219,17 @@
         assertThat(mInterruptableOperation.isFinished()).isTrue();
         verify(mInterruptableClientMonitor).unableToStart();
         verify(mInterruptableClientMonitor).destroy();
-        assertThrows(IllegalStateException.class,
-                () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class)));
+        assertThat(mInterruptableOperation.start(mock(ClientMonitorCallback.class))).isFalse();
     }
 
     @Test
-    public void abortCrashesWhenDebuggableIfOperationIsRunning() {
-        mIsDebuggable = true;
+    public void abortFailsIfOperationIsRunning() {
         when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
         mInterruptableOperation.start(mOnStartCallback);
-
-        assertThrows(IllegalStateException.class, () -> mInterruptableOperation.abort());
-    }
-
-    @Test
-    public void abortFailsNicelyWhenNotDebuggableIfOperationIsRunning() {
-        mIsDebuggable = false;
-        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
-
-        mInterruptableOperation.start(mOnStartCallback);
-
         mInterruptableOperation.abort();
+
+        assertThat(mInterruptableOperation.isFinished()).isFalse();
     }
 
     @Test
@@ -344,21 +278,7 @@
     }
 
     @Test
-    public void cancelCrashesWhenDebuggableIfOperationIsFinished() {
-        mIsDebuggable = true;
-        when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
-
-        mInterruptableOperation.abort();
-        assertThat(mInterruptableOperation.isFinished()).isTrue();
-
-        final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class);
-        assertThrows(IllegalStateException.class, () -> mInterruptableOperation.cancel(mHandler,
-                cancelCb));
-    }
-
-    @Test
-    public void cancelFailsNicelyWhenNotDebuggableIfOperationIsFinished() {
-        mIsDebuggable = false;
+    public void cancelFailsIfOperationIsFinished() {
         when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
         mInterruptableOperation.abort();
@@ -366,6 +286,9 @@
 
         final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class);
         mInterruptableOperation.cancel(mHandler, cancelCb);
+
+        verify(mInterruptableClientMonitor, never()).cancel();
+        verify(mInterruptableClientMonitor, never()).cancelWithoutStarting(any());
     }
 
     @Test