Add generic links to app-to-web

This Cl does the following:
1. Creates a build-time and server side list of generic links
2. Creates a flag which determins which list will be used
3. Creates a parser to retrieve said list and parse it into a Hashmap,
   mapping package names to urls
4. When a user clicks on the "Open in browser" button in the handle
   menu, opens the generic link if captured link is unavailable or
   expired

Bug: 349695493
Test: Use open in browser button in header menu + atest
AppToWebGenericLinksParserTests
Flag: com.android.window.flags.enable_desktop_windowing_app_to_web

Change-Id: Ibbd8c21b95679125e3c480d0430b9d473110fdbb
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index c2ba064..39f6d8c 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -179,4 +179,8 @@
 
     <!-- Whether pointer pilfer is required to start back animation. -->
     <bool name="config_backAnimationRequiresPointerPilfer">true</bool>
+
+    <!-- This is to be overridden to define a list of packages mapped to web links which will be
+         parsed and utilized for desktop windowing's app-to-web feature. -->
+    <string name="generic_links_list" translatable="false"/>
 </resources>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index fc4710f..a1ba24c 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -70,6 +70,10 @@
     private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean(
             "persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
 
+    private static final boolean USE_APP_TO_WEB_BUILD_TIME_GENERIC_LINKS =
+            SystemProperties.getBoolean(
+                    "persist.wm.debug.use_app_to_web_build_time_generic_links", true);
+
     /** Whether the desktop density override is enabled. */
     public static final boolean DESKTOP_DENSITY_OVERRIDE_ENABLED =
             SystemProperties.getBoolean("persist.wm.debug.desktop_mode_density_enabled", false);
@@ -176,6 +180,13 @@
     }
 
     /**
+     * Returns {@code true} if the app-to-web feature is using the build-time generic links list.
+     */
+    public static boolean useAppToWebBuildTimeGenericLinks() {
+        return USE_APP_TO_WEB_BUILD_TIME_GENERIC_LINKS;
+    }
+
+    /**
      * Return {@code true} if the override desktop density is enabled.
      */
     private static boolean isDesktopDensityOverrideEnabled() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParser.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParser.kt
new file mode 100644
index 0000000..56447de
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParser.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.apptoweb
+
+import android.content.Context
+import android.provider.DeviceConfig
+import android.webkit.URLUtil
+import com.android.internal.annotations.VisibleForTesting
+import com.android.wm.shell.R
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useAppToWebBuildTimeGenericLinks
+
+/**
+ * Retrieves the build-time or server-side generic links list and parses and stores the
+ * package-to-url pairs.
+ */
+class AppToWebGenericLinksParser(
+    private val context: Context,
+    @ShellMainThread private val mainExecutor: ShellExecutor
+) {
+    private val genericLinksMap: MutableMap<String, String> = mutableMapOf()
+
+    init {
+        // If using the server-side generic links list, register a listener
+        if (!useAppToWebBuildTimeGenericLinks()) {
+            DeviceConfigListener()
+        }
+
+        updateGenericLinksMap()
+    }
+
+    /** Returns the generic link associated with the [packageName] or null if there is none. */
+    fun getGenericLink(packageName: String): String? = genericLinksMap[packageName]
+
+    private fun updateGenericLinksMap() {
+        val genericLinksList =
+            if (useAppToWebBuildTimeGenericLinks()) {
+                context.resources.getString(R.string.generic_links_list)
+            } else {
+                DeviceConfig.getString(NAMESPACE, FLAG_GENERIC_LINKS, /* defaultValue= */ "")
+            } ?: return
+
+        parseGenericLinkList(genericLinksList)
+    }
+
+    private fun parseGenericLinkList(genericLinksList: String) {
+        val newEntries =
+            genericLinksList
+                .split(" ")
+                .filter { it.contains(':') }
+                .map {
+                    val (packageName, url) = it.split(':', limit = 2)
+                    return@map packageName to url
+                }
+                .filter { URLUtil.isNetworkUrl(it.second) }
+
+        genericLinksMap.clear()
+        genericLinksMap.putAll(newEntries)
+    }
+
+    /**
+     * Listens for changes to the server-side generic links list and updates the package to url map
+     * if [DesktopModeStatus#useBuildTimeGenericLinkList()] is set to false.
+     */
+    inner class DeviceConfigListener : DeviceConfig.OnPropertiesChangedListener {
+        init {
+            DeviceConfig.addOnPropertiesChangedListener(NAMESPACE, mainExecutor, this)
+        }
+
+        override fun onPropertiesChanged(properties: DeviceConfig.Properties) {
+            if (properties.keyset.contains(FLAG_GENERIC_LINKS)) {
+                updateGenericLinksMap()
+            }
+        }
+    }
+
+    companion object {
+        private const val NAMESPACE = DeviceConfig.NAMESPACE_APP_COMPAT_OVERRIDES
+        @VisibleForTesting const val FLAG_GENERIC_LINKS = "generic_links_flag"
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 80f6a63..b8c22c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -35,6 +35,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.WindowManagerShellWrapper;
 import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
+import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
 import com.android.wm.shell.bubbles.BubbleController;
 import com.android.wm.shell.bubbles.BubbleData;
 import com.android.wm.shell.bubbles.BubbleDataRepository;
@@ -223,7 +224,8 @@
             Transitions transitions,
             Optional<DesktopTasksController> desktopTasksController,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
-            InteractionJankMonitor interactionJankMonitor) {
+            InteractionJankMonitor interactionJankMonitor,
+            AppToWebGenericLinksParser genericLinksParser) {
         if (DesktopModeStatus.canEnterDesktopMode(context)) {
             return new DesktopModeWindowDecorViewModel(
                     context,
@@ -242,7 +244,8 @@
                     transitions,
                     desktopTasksController,
                     rootTaskDisplayAreaOrganizer,
-                    interactionJankMonitor);
+                    interactionJankMonitor,
+                    genericLinksParser);
         }
         return new CaptionWindowDecorViewModel(
                 context,
@@ -259,6 +262,15 @@
                 transitions);
     }
 
+    @WMSingleton
+    @Provides
+    static AppToWebGenericLinksParser provideGenericLinksParser(
+            Context context,
+            @ShellMainThread ShellExecutor mainExecutor
+    ) {
+        return new AppToWebGenericLinksParser(context, mainExecutor);
+    }
+
     //
     // Freeform
     //
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 8312aef..fc63970 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
@@ -87,6 +87,7 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayLayout;
@@ -162,6 +163,7 @@
     private final DesktopModeKeyguardChangeListener mDesktopModeKeyguardChangeListener =
             new DesktopModeKeyguardChangeListener();
     private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
+    private final AppToWebGenericLinksParser mGenericLinksParser;
     private final DisplayInsetsController mDisplayInsetsController;
     private final Region mExclusionRegion = Region.obtain();
     private boolean mInImmersiveMode;
@@ -198,7 +200,8 @@
             Transitions transitions,
             Optional<DesktopTasksController> desktopTasksController,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
-            InteractionJankMonitor interactionJankMonitor
+            InteractionJankMonitor interactionJankMonitor,
+            AppToWebGenericLinksParser genericLinksParser
     ) {
         this(
                 context,
@@ -216,6 +219,7 @@
                 syncQueue,
                 transitions,
                 desktopTasksController,
+                genericLinksParser,
                 new DesktopModeWindowDecoration.Factory(),
                 new InputMonitorFactory(),
                 SurfaceControl.Transaction::new,
@@ -241,6 +245,7 @@
             SyncTransactionQueue syncQueue,
             Transitions transitions,
             Optional<DesktopTasksController> desktopTasksController,
+            AppToWebGenericLinksParser genericLinksParser,
             DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
             InputMonitorFactory inputMonitorFactory,
             Supplier<SurfaceControl.Transaction> transactionFactory,
@@ -266,6 +271,7 @@
         mInputMonitorFactory = inputMonitorFactory;
         mTransactionFactory = transactionFactory;
         mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
+        mGenericLinksParser = genericLinksParser;
         mInputManager = mContext.getSystemService(InputManager.class);
         mWindowDecorByTaskId = windowDecorByTaskId;
         mSysUIPackageName = mContext.getResources().getString(
@@ -1155,7 +1161,8 @@
                         mBgExecutor,
                         mMainChoreographer,
                         mSyncQueue,
-                        mRootTaskDisplayAreaOrganizer);
+                        mRootTaskDisplayAreaOrganizer,
+                        mGenericLinksParser);
         mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
 
         final DragPositioningCallback dragPositioningCallback;
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 529def7..3b62bcf 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
@@ -30,6 +30,7 @@
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
 import android.app.WindowConfiguration.WindowingMode;
@@ -49,8 +50,8 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.os.Trace;
-import android.util.Log;
 import android.util.Size;
+import android.util.Slog;
 import android.view.Choreographer;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
@@ -68,6 +69,7 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.ShellExecutor;
@@ -133,12 +135,15 @@
 
     private CharSequence mAppName;
     private CapturedLink mCapturedLink;
+    private Uri mGenericLink;
     private OpenInBrowserClickListener mOpenInBrowserClickListener;
 
     private ExclusionRegionListener mExclusionRegionListener;
 
     private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
     private final MaximizeMenuFactory mMaximizeMenuFactory;
+    private final HandleMenuFactory mHandleMenuFactory;
+    private final AppToWebGenericLinksParser mGenericLinksParser;
 
     // Hover state for the maximize menu and button. The menu will remain open as long as either of
     // these is true. See {@link #onMaximizeHoverStateChanged()}.
@@ -161,13 +166,14 @@
             @ShellBackgroundThread ShellExecutor bgExecutor,
             Choreographer choreographer,
             SyncTransactionQueue syncQueue,
-            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+            AppToWebGenericLinksParser genericLinksParser) {
         this (context, displayController, splitScreenController, taskOrganizer, taskInfo,
                 taskSurface, handler, bgExecutor, choreographer, syncQueue,
-                rootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new,
+                rootTaskDisplayAreaOrganizer, genericLinksParser, SurfaceControl.Builder::new,
                 SurfaceControl.Transaction::new,  WindowContainerTransaction::new,
                 SurfaceControl::new, new SurfaceControlViewHostFactory() {},
-                DefaultMaximizeMenuFactory.INSTANCE);
+                DefaultMaximizeMenuFactory.INSTANCE, DefaultHandleMenuFactory.INSTANCE);
     }
 
     DesktopModeWindowDecoration(
@@ -182,12 +188,14 @@
             Choreographer choreographer,
             SyncTransactionQueue syncQueue,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+            AppToWebGenericLinksParser genericLinksParser,
             Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
             Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
             Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
             Supplier<SurfaceControl> surfaceControlSupplier,
             SurfaceControlViewHostFactory surfaceControlViewHostFactory,
-            MaximizeMenuFactory maximizeMenuFactory) {
+            MaximizeMenuFactory maximizeMenuFactory,
+            HandleMenuFactory handleMenuFactory) {
         super(context, displayController, taskOrganizer, taskInfo, taskSurface,
                 surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
                 windowContainerTransactionSupplier, surfaceControlSupplier,
@@ -198,7 +206,9 @@
         mChoreographer = choreographer;
         mSyncQueue = syncQueue;
         mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
+        mGenericLinksParser = genericLinksParser;
         mMaximizeMenuFactory = maximizeMenuFactory;
+        mHandleMenuFactory = handleMenuFactory;
     }
 
     /**
@@ -425,11 +435,23 @@
     }
 
     void onOpenInBrowserClick() {
-        if (mOpenInBrowserClickListener == null || mCapturedLink == null) return;
-        mOpenInBrowserClickListener.onClick(this, mCapturedLink.mUri);
+        if (mOpenInBrowserClickListener == null || mHandleMenu == null) {
+            return;
+        }
+        mOpenInBrowserClickListener.onClick(this, mHandleMenu.getOpenInBrowserLink());
         onCapturedLinkExpired();
     }
 
+    @Nullable
+    private Uri getBrowserLink() {
+        // If the captured link is available and has not expired, return the captured link.
+        // Otherwise, return the generic link which is set to null if a generic link is unavailable.
+        if (mCapturedLink != null && !mCapturedLink.mExpired) {
+            return mCapturedLink.mUri;
+        }
+        return mGenericLink;
+    }
+
     private void updateDragResizeListener(SurfaceControl oldDecorationSurface) {
         if (!isDragResizable(mTaskInfo)) {
             if (!mTaskInfo.positionInParent.equals(mPositionInParent)) {
@@ -700,7 +722,7 @@
             }
             final ComponentName baseActivity = mTaskInfo.baseActivity;
             if (baseActivity == null) {
-                Log.e(TAG, "Base activity component not found in task");
+                Slog.e(TAG, "Base activity component not found in task");
                 return;
             }
             final PackageManager pm = mContext.getApplicationContext().getPackageManager();
@@ -719,7 +741,7 @@
             final ApplicationInfo applicationInfo = activityInfo.applicationInfo;
             mAppName = pm.getApplicationLabel(applicationInfo);
         } catch (PackageManager.NameNotFoundException e) {
-            Log.e(TAG, "Base activity's component name cannot be found on the system");
+            Slog.e(TAG, "Base activity's component name cannot be found on the system", e);
         } finally {
             Trace.endSection();
         }
@@ -914,7 +936,8 @@
      */
     void createHandleMenu(SplitScreenController splitScreenController) {
         loadAppInfoIfNeeded();
-        mHandleMenu = new HandleMenu(
+        updateGenericLink();
+        mHandleMenu = mHandleMenuFactory.create(
                 this,
                 mRelayoutParams.mLayoutResId,
                 mOnCaptionButtonClickListener,
@@ -924,7 +947,7 @@
                 mDisplayController,
                 splitScreenController,
                 DesktopModeStatus.canEnterDesktopMode(mContext),
-                browserLinkAvailable(),
+                getBrowserLink(),
                 mResult.mCaptionWidth,
                 mResult.mCaptionHeight,
                 mResult.mCaptionX
@@ -933,9 +956,15 @@
         mHandleMenu.show();
     }
 
-    @VisibleForTesting
-    boolean browserLinkAvailable() {
-        return mCapturedLink != null && !mCapturedLink.mExpired;
+    private void updateGenericLink() {
+        final ComponentName baseActivity = mTaskInfo.baseActivity;
+        if (baseActivity == null) {
+            return;
+        }
+
+        final String genericLink =
+                mGenericLinksParser.getGenericLink(baseActivity.getPackageName());
+        mGenericLink = genericLink == null ? null : Uri.parse(genericLink);
     }
 
     /**
@@ -1219,7 +1248,8 @@
                 @ShellBackgroundThread ShellExecutor bgExecutor,
                 Choreographer choreographer,
                 SyncTransactionQueue syncQueue,
-                RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+                RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+                AppToWebGenericLinksParser genericLinksParser) {
             return new DesktopModeWindowDecoration(
                     context,
                     displayController,
@@ -1231,7 +1261,8 @@
                     bgExecutor,
                     choreographer,
                     syncQueue,
-                    rootTaskDisplayAreaOrganizer);
+                    rootTaskDisplayAreaOrganizer,
+                    genericLinksParser);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index e174e83..32522c6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -27,6 +27,7 @@
 import android.graphics.Point
 import android.graphics.PointF
 import android.graphics.Rect
+import android.net.Uri
 import android.view.MotionEvent
 import android.view.SurfaceControl
 import android.view.View
@@ -68,7 +69,7 @@
     private val displayController: DisplayController,
     private val splitScreenController: SplitScreenController,
     private val shouldShowWindowingPill: Boolean,
-    private val shouldShowBrowserPill: Boolean,
+    val openInBrowserLink: Uri?,
     private val captionWidth: Int,
     private val captionHeight: Int,
     captionX: Int
@@ -106,6 +107,9 @@
     // those as well.
     private val globalMenuPosition: Point = Point()
 
+    private val shouldShowBrowserPill: Boolean
+        get() = openInBrowserLink != null
+
     init {
         updateHandleMenuPillPositions(captionX)
     }
@@ -497,3 +501,57 @@
         private const val SHOULD_SHOW_MORE_ACTIONS_PILL = false
     }
 }
+
+/** A factory interface to create a [HandleMenu]. */
+interface HandleMenuFactory {
+    fun create(
+        parentDecor: DesktopModeWindowDecoration,
+        layoutResId: Int,
+        onClickListener: View.OnClickListener?,
+        onTouchListener: View.OnTouchListener?,
+        appIconBitmap: Bitmap?,
+        appName: CharSequence?,
+        displayController: DisplayController,
+        splitScreenController: SplitScreenController,
+        shouldShowWindowingPill: Boolean,
+        openInBrowserLink: Uri?,
+        captionWidth: Int,
+        captionHeight: Int,
+        captionX: Int
+    ): HandleMenu
+}
+
+/** A [HandleMenuFactory] implementation that creates a [HandleMenu].  */
+object DefaultHandleMenuFactory : HandleMenuFactory {
+    override fun create(
+        parentDecor: DesktopModeWindowDecoration,
+        layoutResId: Int,
+        onClickListener: View.OnClickListener?,
+        onTouchListener: View.OnTouchListener?,
+        appIconBitmap: Bitmap?,
+        appName: CharSequence?,
+        displayController: DisplayController,
+        splitScreenController: SplitScreenController,
+        shouldShowWindowingPill: Boolean,
+        openInBrowserLink: Uri?,
+        captionWidth: Int,
+        captionHeight: Int,
+        captionX: Int
+    ): HandleMenu {
+        return HandleMenu(
+            parentDecor,
+            layoutResId,
+            onClickListener,
+            onTouchListener,
+            appIconBitmap,
+            appName,
+            displayController,
+            splitScreenController,
+            shouldShowWindowingPill,
+            openInBrowserLink,
+            captionWidth,
+            captionHeight,
+            captionX
+        )
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParserTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParserTests.kt
new file mode 100644
index 0000000..053027f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParserTests.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.apptoweb
+
+import android.provider.DeviceConfig
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableResources
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.wm.shell.R
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser.Companion.FLAG_GENERIC_LINKS
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.quality.Strictness
+
+/**
+ * Tests for [AppToWebGenericLinksParser].
+ *
+ * Build/Install/Run: atest WMShellUnitTests:AppToWebGenericLinksParserTests
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class AppToWebGenericLinksParserTests : ShellTestCase() {
+    @Mock private lateinit var mockExecutor: ShellExecutor
+
+    private lateinit var genericLinksParser: AppToWebGenericLinksParser
+    private lateinit var mockitoSession: StaticMockitoSession
+    private lateinit var resources: TestableResources
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        mockitoSession =
+            mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .spyStatic(DesktopModeStatus::class.java)
+                .startMocking()
+        resources = mContext.getOrCreateTestableResources()
+        resources.addOverride(R.string.generic_links_list, BUILD_TIME_LIST)
+        DeviceConfig.setProperty(
+            NAMESPACE,
+            FLAG_GENERIC_LINKS,
+            SERVER_SIDE_LIST,
+            false /* makeDefault */
+        )
+    }
+
+    @After
+    fun tearDown() {
+        mockitoSession.finishMocking()
+    }
+
+    @Test
+    fun init_usingBuildTimeList() {
+        doReturn(true).`when` { DesktopModeStatus.useAppToWebBuildTimeGenericLinks() }
+        genericLinksParser = AppToWebGenericLinksParser(mContext, mockExecutor)
+        // Assert build-time list correctly parsed
+        assertEquals(URL_B, genericLinksParser.getGenericLink(PACKAGE_NAME_1))
+    }
+
+    @Test
+    fun init_usingServerSideList() {
+        doReturn(false).`when` { DesktopModeStatus.useAppToWebBuildTimeGenericLinks() }
+        genericLinksParser = AppToWebGenericLinksParser(mContext, mockExecutor)
+        // Assert server side list correctly parsed
+        assertEquals(URL_S, genericLinksParser.getGenericLink(PACKAGE_NAME_1))
+    }
+
+    @Test
+    fun init_ignoresMalformedPair() {
+        doReturn(true).`when` { DesktopModeStatus.useAppToWebBuildTimeGenericLinks() }
+        val packageName2 = "com.google.android.slides"
+        val url2 = "https://docs.google.com"
+        resources.addOverride(R.string.generic_links_list,
+                "$PACKAGE_NAME_1:$URL_B error $packageName2:$url2")
+        genericLinksParser = AppToWebGenericLinksParser(mContext, mockExecutor)
+        // Assert generics links list correctly parsed
+        assertEquals(URL_B, genericLinksParser.getGenericLink(PACKAGE_NAME_1))
+        assertEquals(url2, genericLinksParser.getGenericLink(packageName2))
+    }
+
+
+    @Test
+    fun onlySavesValidPackageToUrlMaps() {
+        doReturn(true).`when` { DesktopModeStatus.useAppToWebBuildTimeGenericLinks() }
+        resources.addOverride(R.string.generic_links_list, "$PACKAGE_NAME_1:www.yout")
+        genericLinksParser = AppToWebGenericLinksParser(mContext, mockExecutor)
+        // Verify map with invalid url not saved
+        assertNull(genericLinksParser.getGenericLink(PACKAGE_NAME_1))
+    }
+
+    companion object {
+        private const val PACKAGE_NAME_1 = "com.google.android.youtube"
+
+        private const val URL_B = "http://www.youtube.com"
+        private const val URL_S = "http://www.google.com"
+
+        private const val SERVER_SIDE_LIST = "$PACKAGE_NAME_1:$URL_S"
+        private const val BUILD_TIME_LIST = "$PACKAGE_NAME_1:$URL_B"
+
+        private const val NAMESPACE = DeviceConfig.NAMESPACE_APP_COMPAT_OVERRIDES
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index aeae0be..01c4f3a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -64,6 +64,7 @@
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.TestRunningTaskInfoBuilder
 import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.DisplayInsetsController
 import com.android.wm.shell.common.DisplayLayout
@@ -140,6 +141,7 @@
     @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler
     @Mock private lateinit var mockWindowManager: IWindowManager
     @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
+    @Mock private lateinit var mockGenericLinksParser: AppToWebGenericLinksParser
     private val bgExecutor = TestShellExecutor()
 
     private val transactionFactory = Supplier<SurfaceControl.Transaction> {
@@ -171,11 +173,13 @@
                 mockSyncQueue,
                 mockTransitions,
                 Optional.of(mockDesktopTasksController),
+                mockGenericLinksParser,
                 mockDesktopModeWindowDecorFactory,
                 mockInputMonitorFactory,
                 transactionFactory,
                 mockRootTaskDisplayAreaOrganizer,
-            windowDecorByTaskIdSpy, mockInteractionJankMonitor
+                windowDecorByTaskIdSpy,
+                mockInteractionJankMonitor
         )
         desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
         whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
@@ -218,7 +222,8 @@
                 bgExecutor,
                 mockMainChoreographer,
                 mockSyncQueue,
-                mockRootTaskDisplayAreaOrganizer
+                mockRootTaskDisplayAreaOrganizer,
+                mockGenericLinksParser
         )
         verify(decoration).close()
     }
@@ -244,7 +249,8 @@
                 bgExecutor,
                 mockMainChoreographer,
                 mockSyncQueue,
-                mockRootTaskDisplayAreaOrganizer
+                mockRootTaskDisplayAreaOrganizer,
+                mockGenericLinksParser
         )
 
         task.setWindowingMode(WINDOWING_MODE_FREEFORM)
@@ -261,7 +267,8 @@
                 bgExecutor,
                 mockMainChoreographer,
                 mockSyncQueue,
-                mockRootTaskDisplayAreaOrganizer
+                mockRootTaskDisplayAreaOrganizer,
+                mockGenericLinksParser
         )
     }
 
@@ -359,7 +366,7 @@
 
         verify(mockDesktopModeWindowDecorFactory, never())
                 .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
-                    any())
+                    any(), any())
     }
 
     @Test
@@ -381,7 +388,7 @@
             onTaskOpening(task)
             verify(mockDesktopModeWindowDecorFactory)
                     .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
-                        any(), any())
+                        any(), any(), any())
         } finally {
             mockitoSession.finishMocking()
         }
@@ -399,7 +406,7 @@
 
         verify(mockDesktopModeWindowDecorFactory, never())
                 .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
-                    any())
+                    any(), any())
     }
 
     @Test
@@ -416,7 +423,7 @@
         onTaskOpening(task)
 
         verify(mockDesktopModeWindowDecorFactory, never())
-                .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
+                .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
                     any(), any())
     }
 
@@ -515,7 +522,7 @@
             onTaskOpening(task)
             verify(mockDesktopModeWindowDecorFactory, never())
                 .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
-                    any())
+                    any(), any())
         } finally {
             mockitoSession.finishMocking()
         }
@@ -540,7 +547,7 @@
             onTaskOpening(task)
             verify(mockDesktopModeWindowDecorFactory)
                 .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
-                    any())
+                    any(), any())
         } finally {
             mockitoSession.finishMocking()
         }
@@ -564,7 +571,7 @@
             onTaskOpening(task)
             verify(mockDesktopModeWindowDecorFactory)
                 .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
-                any())
+                    any(), any())
         } finally {
             mockitoSession.finishMocking()
         }
@@ -702,8 +709,8 @@
     private fun setUpMockDecorationForTask(task: RunningTaskInfo): DesktopModeWindowDecoration {
         val decoration = mock(DesktopModeWindowDecoration::class.java)
         whenever(
-            mockDesktopModeWindowDecorFactory.create(
-                any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
+            mockDesktopModeWindowDecorFactory.create(any(), any(), any(), any(), eq(task), any(),
+                any(), any(), any(), any(), any(), any())
         ).thenReturn(decoration)
         decoration.mTaskInfo = task
         whenever(decoration.isFocused).thenReturn(task.isFocused)
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 412fef3..4b069f9 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.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
@@ -59,6 +60,7 @@
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableContext;
+import android.testing.TestableLooper;
 import android.view.AttachedSurfaceControl;
 import android.view.Choreographer;
 import android.view.Display;
@@ -83,6 +85,7 @@
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
@@ -114,6 +117,7 @@
  * atest WMShellUnitTests:DesktopModeWindowDecorationTests
  */
 @SmallTest
+@TestableLooper.RunWithLooper
 @RunWith(AndroidTestingRunner.class)
 public class DesktopModeWindowDecorationTests extends ShellTestCase {
     private static final String USE_WINDOW_SHADOWS_SYSPROP_KEY =
@@ -123,7 +127,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");
+    private static final Uri TEST_URI1 = Uri.parse("https://www.google.com/");
+    private static final Uri TEST_URI2 = Uri.parse("https://docs.google.com/");
 
     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
 
@@ -161,6 +166,12 @@
     private Handler mMockHandler;
     @Mock
     private DesktopModeWindowDecoration.OpenInBrowserClickListener mMockOpenInBrowserClickListener;
+    @Mock
+    private AppToWebGenericLinksParser mMockGenericLinksParser;
+    @Mock
+    private HandleMenu mMockHandleMenu;
+    @Mock
+    private HandleMenuFactory mMockHandleMenuFactory;
     @Captor
     private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener;
     @Captor
@@ -204,6 +215,8 @@
         final Display defaultDisplay = mock(Display.class);
         doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY);
         doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt());
+        doReturn(mMockHandleMenu).when(mMockHandleMenuFactory).create(any(), anyInt(), any(), any(),
+                any(), any(), any(), any(), anyBoolean(), any(), anyInt(), anyInt(), anyInt());
     }
 
     @After
@@ -572,65 +585,123 @@
         verify(mMockHandler).removeCallbacks(any());
     }
 
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
+    public void capturedLink_handleMenuBrowserLinkSetToCapturedLinkIfValid() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final DesktopModeWindowDecoration decor = createWindowDecoration(
+                taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* generic link */);
+
+        // Verify handle menu's browser link set as captured link
+        decor.createHandleMenu(mMockSplitScreenController);
+        verifyHandleMenuCreated(TEST_URI1);
+    }
+
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
     public void capturedLink_postsOnCapturedLinkExpiredRunnable() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final DesktopModeWindowDecoration decor = createWindowDecoration(
+                taskInfo, TEST_URI1 /* captured link */, null /* generic link */);
         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
+        // Run runnable to set captured link to expired
         verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong());
         runnableArgument.getValue().run();
-        assertFalse(decor.browserLinkAvailable());
+
+        // Verify captured link is no longer valid by verifying link is not set as handle menu
+        // browser link.
+        decor.createHandleMenu(mMockSplitScreenController);
+        verifyHandleMenuCreated(null /* uri */);
     }
 
     @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 DesktopModeWindowDecoration decor = createWindowDecoration(
+                taskInfo, TEST_URI1 /* captured link */, null /* generic link */);
         final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
 
-        // Set captured link and run on captured link expired runnable
-        decor.relayout(taskInfo);
+        // Run runnable to set captured link to expired
         verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong());
         runnableArgument.getValue().run();
 
+        // Relayout decor with same captured link
         decor.relayout(taskInfo);
-        // Assert captured link not set to same value twice
-        assertFalse(decor.browserLinkAvailable());
+
+        // Verify handle menu's browser link not set to captured link since link is expired
+        decor.createHandleMenu(mMockSplitScreenController);
+        verifyHandleMenuCreated(null /* uri */);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
+    public void capturedLink_capturedLinkStillUsedIfExpiredAfterHandleMenuCreation() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final DesktopModeWindowDecoration decor = createWindowDecoration(
+                taskInfo, TEST_URI1 /* captured link */, null /* generic link */);
+        final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
+
+        // Create handle menu before link expires
+        decor.createHandleMenu(mMockSplitScreenController);
+
+        // Run runnable to set captured link to expired
+        verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong());
+        runnableArgument.getValue().run();
+
+        // Verify handle menu's browser link is set to captured link since menu was opened before
+        // captured link expired
+        decor.createHandleMenu(mMockSplitScreenController);
+        verifyHandleMenuCreated(TEST_URI1);
     }
 
     @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());
+        final DesktopModeWindowDecoration decor = createWindowDecoration(
+                taskInfo, TEST_URI1 /* captured link */, null /* generic link */);
+        // Simulate menu opening and clicking open in browser button
+        decor.createHandleMenu(mMockSplitScreenController);
         decor.onOpenInBrowserClick();
-        //Assert Captured link expires after button is clicked
-        assertFalse(decor.browserLinkAvailable());
+
+        // Verify handle menu's browser link not set to captured link since link not valid after
+        // open in browser clicked
+        decor.createHandleMenu(mMockSplitScreenController);
+        verifyHandleMenuCreated(null /* uri */);
     }
 
     @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);
+        final DesktopModeWindowDecoration decor = createWindowDecoration(
+                taskInfo, TEST_URI1 /* captured link */, null /* generic link */);
+        decor.createHandleMenu(mMockSplitScreenController);
         decor.onOpenInBrowserClick();
 
         verify(mMockOpenInBrowserClickListener).onClick(any(), any());
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
+    public void genericLink_genericLinkUsedWhenCapturedLinkUnavailable() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final DesktopModeWindowDecoration decor = createWindowDecoration(
+                taskInfo, null /* captured link */, TEST_URI2 /* generic link */);
+
+        // Verify handle menu's browser link set as generic link no captured link is available
+        decor.createHandleMenu(mMockSplitScreenController);
+        verifyHandleMenuCreated(TEST_URI2);
+    }
+
+    private void verifyHandleMenuCreated(@Nullable Uri uri) {
+        verify(mMockHandleMenuFactory).create(any(), anyInt(), any(), any(), any(), any(), any(),
+                any(), anyBoolean(), eq(uri), anyInt(), anyInt(), anyInt());
+    }
+
     private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) {
         final OnTaskActionClickListener l = (taskId, tag) -> {};
         decoration.setOnMaximizeOrRestoreClickListener(l);
@@ -657,6 +728,18 @@
                 R.dimen.rounded_corner_radius_bottom, fillValue);
     }
 
+    private DesktopModeWindowDecoration createWindowDecoration(
+            ActivityManager.RunningTaskInfo taskInfo, @Nullable Uri capturedLink,
+            @Nullable Uri genericLink) {
+        taskInfo.capturedLink = capturedLink;
+        taskInfo.capturedLinkTimestamp = System.currentTimeMillis();
+        final String genericLinkString = genericLink == null ? null : genericLink.toString();
+        doReturn(genericLinkString).when(mMockGenericLinksParser).getGenericLink(any());
+        final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo);
+        // Relayout to set captured link
+        decor.relayout(taskInfo);
+        return decor;
+    }
 
     private DesktopModeWindowDecoration createWindowDecoration(
             ActivityManager.RunningTaskInfo taskInfo) {
@@ -669,14 +752,15 @@
         final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
                 mMockDisplayController, mMockSplitScreenController, mMockShellTaskOrganizer,
                 taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor, mMockChoreographer,
-                mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
+                mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, mMockGenericLinksParser,
                 SurfaceControl.Builder::new, mMockTransactionSupplier,
                 WindowContainerTransaction::new, SurfaceControl::new,
-                mMockSurfaceControlViewHostFactory, maximizeMenuFactory);
+                mMockSurfaceControlViewHostFactory, maximizeMenuFactory, mMockHandleMenuFactory);
         windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener,
                 mMockTouchEventListener, mMockTouchEventListener);
         windowDecor.setExclusionRegionListener(mMockExclusionRegionListener);
         windowDecor.setOpenInBrowserClickListener(mMockOpenInBrowserClickListener);
+        windowDecor.mDecorWindowContext = mContext;
         return windowDecor;
     }
 
@@ -692,8 +776,6 @@
                 "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 e548f8f..ed43aa3 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
@@ -229,7 +229,7 @@
         val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId,
                 onClickListener, onTouchListener, appIcon, appName, displayController,
                 splitScreenController, shouldShowWindowingPill = true,
-                shouldShowBrowserPill = true, captionWidth = HANDLE_WIDTH, captionHeight = 50,
+                null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50,
                 captionX = captionX
         )
         handleMenu.show()