Uninstall an app from workspace via TAPL

1. Implement function to uninstall app from appIcon.
2. Add test uninstall to TaplTestsLauncher3.

Bug: 210927656
Test: Launcher3Tests:com.android.launcher3.ui.TaplTestsLauncher3#testTryUninstallFromWorkspace
Change-Id: Iedd3bd3a46bc626fc414fd8c5bd07ebc0fa235bb
diff --git a/tests/Android.bp b/tests/Android.bp
index 3670c37..8def20f 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -32,6 +32,7 @@
     srcs: [
       "src/com/android/launcher3/ui/AbstractLauncherUiTest.java",
       "src/com/android/launcher3/ui/PortraitLandscapeRunner.java",
+      "src/com/android/launcher3/util/TestUtil.java",
       "src/com/android/launcher3/util/Wait.java",
       "src/com/android/launcher3/util/WidgetUtils.java",
       "src/com/android/launcher3/util/rule/FailureWatcher.java",
@@ -71,6 +72,11 @@
     platform_apis: true,
 }
 
+android_library {
+    name: "Launcher3TestResources",
+    resource_dirs: ["res"],
+}
+
 android_test {
     name: "Launcher3Tests",
     srcs: [
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 5b940a8..f193e24 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -38,6 +38,7 @@
 import com.android.launcher3.tapl.FolderIcon;
 import com.android.launcher3.tapl.Widgets;
 import com.android.launcher3.tapl.Workspace;
+import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 import com.android.launcher3.widget.picker.WidgetsRecyclerView;
 
@@ -50,6 +51,7 @@
 @RunWith(AndroidJUnit4.class)
 public class TaplTestsLauncher3 extends AbstractLauncherUiTest {
     private static final String APP_NAME = "LauncherTestApp";
+    private static final String DUMMY_APP_NAME = "Aardwolf";
 
     @Before
     public void setUp() throws Exception {
@@ -435,7 +437,49 @@
         }
     }
 
+    private void verifyAppUninstalledFromAllApps(Workspace workspace, String appName) {
+        final AllApps allApps = workspace.switchToAllApps();
+        allApps.freeze();
+        try {
+            assertNull(appName + " app was found on all apps after being uninstalled",
+                    allApps.tryGetAppIcon(appName));
+        } finally {
+            allApps.unfreeze();
+        }
+    }
+
+    @Test
+    @PortraitLandscape
+    public void testUninstallFromWorkspace() throws Exception {
+        TestUtil.installDummyApp();
+        try {
+            verifyAppUninstalledFromAllApps(
+                    createShortcutIfNotExist(DUMMY_APP_NAME).uninstall(), DUMMY_APP_NAME);
+        } finally {
+            TestUtil.uninstallDummyApp();
+        }
+    }
+
+    @Test
+    @PortraitLandscape
+    public void testUninstallFromAllApps() throws Exception {
+        TestUtil.installDummyApp();
+        try {
+            Workspace workspace = mLauncher.getWorkspace();
+            final AllApps allApps = workspace.switchToAllApps();
+            allApps.freeze();
+            try {
+                workspace = allApps.getAppIcon(DUMMY_APP_NAME).uninstall();
+            } finally {
+                allApps.unfreeze();
+            }
+            verifyAppUninstalledFromAllApps(workspace, DUMMY_APP_NAME);
+        } finally {
+            TestUtil.uninstallDummyApp();
+        }
+    }
+
     public static String getAppPackageName() {
         return getInstrumentation().getContext().getPackageName();
     }
-}
+}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
index 0582bc9..98eb32e 100644
--- a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
+++ b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
@@ -22,8 +22,6 @@
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.UiObject2;
 
-import com.android.launcher3.testing.TestProtocol;
-
 import java.util.regex.Pattern;
 
 public class AddToHomeScreenPrompt {
@@ -44,19 +42,10 @@
 
     public void addAutomatically() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
-            if (mLauncher.getNavigationModel()
-                    != LauncherInstrumentation.NavigationModel.THREE_BUTTON) {
-                if (!mLauncher.isLauncher3()) {
-                    mLauncher.expectEvent(
-                            TestProtocol.SEQUENCE_TIS,
-                            LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS);
-                    mLauncher.expectEvent(
-                            TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_UP_TIS);
-                }
-            }
-            mLauncher.waitForObjectInContainer(
-                    mWidgetCell.getParent().getParent().getParent().getParent(),
-                    By.text(ADD_AUTOMATICALLY)).click();
+            mLauncher.clickObject(
+                    mLauncher.waitForObjectInContainer(
+                            mWidgetCell.getParent().getParent().getParent().getParent(),
+                            By.text(ADD_AUTOMATICALLY)));
             mLauncher.waitUntilLauncherObjectGone(getSelector());
         }
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 78301e4..c1b0220 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -22,6 +22,7 @@
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.Direction;
@@ -51,8 +52,8 @@
         mLauncher.waitForObjectInContainer(appListRecycler, By.clazz(TextView.class));
         verifyNotFrozen("All apps freeze flags upon opening all apps");
         mIconHeight = mLauncher.getTestInfo(
-                TestProtocol.REQUEST_ICON_HEIGHT).
-                getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+                        TestProtocol.REQUEST_ICON_HEIGHT)
+                .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
     @Override
@@ -98,14 +99,14 @@
     }
 
     /**
-     * Finds an icon. Fails if the icon doesn't exist. Scrolls the app list when needed to make
-     * sure the icon is visible.
+     * Finds an icon. If the icon doesn't exist, return null.
+     * Scrolls the app list when needed to make sure the icon is visible.
      *
      * @param appName name of the app.
-     * @return The app.
+     * @return The app if found, and null if not found.
      */
-    @NonNull
-    public AppIcon getAppIcon(String appName) {
+    @Nullable
+    public AppIcon tryGetAppIcon(String appName) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "getting app icon " + appName + " on all apps")) {
@@ -150,19 +151,34 @@
                 }
                 verifyActiveContainer();
             }
-
             // Ignore bottom offset selection here as there might not be any scroll more scroll
             // region available.
-            mLauncher.assertTrue("Unable to scroll to a clickable icon: " + appName,
-                    hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector,
-                            deviceHeight));
+            if (hasClickableIcon(
+                    allAppsContainer, appListRecycler, appIconSelector, deviceHeight)) {
 
-            final UiObject2 appIcon = mLauncher.waitForObjectInContainer(appListRecycler,
-                    appIconSelector);
-            return new AppIcon(mLauncher, appIcon);
+                final UiObject2 appIcon = mLauncher.waitForObjectInContainer(appListRecycler,
+                        appIconSelector);
+                return new AllAppsAppIcon(mLauncher, appIcon);
+            } else {
+                return null;
+            }
         }
     }
 
+    /**
+     * Finds an icon. Fails if the icon doesn't exist. Scrolls the app list when needed to make
+     * sure the icon is visible.
+     *
+     * @param appName name of the app.
+     * @return The app.
+     */
+    @NonNull
+    public AppIcon getAppIcon(String appName) {
+        AppIcon appIcon = tryGetAppIcon(appName);
+        mLauncher.assertNotNull("Unable to scroll to a clickable icon: " + appName, appIcon);
+        return appIcon;
+    }
+
     private void scrollBackToBeginning() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to scroll back in all apps")) {
@@ -195,7 +211,7 @@
 
     private int getAllAppsScroll() {
         return mLauncher.getTestInfo(
-                TestProtocol.REQUEST_APPS_LIST_SCROLL_Y)
+                        TestProtocol.REQUEST_APPS_LIST_SCROLL_Y)
                 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
@@ -253,4 +269,4 @@
         if (testInfo == null) return;
         mLauncher.assertEquals(message, 0, testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD));
     }
-}
+}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsAppIcon.java b/tests/tapl/com/android/launcher3/tapl/AllAppsAppIcon.java
new file mode 100644
index 0000000..03d387e
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/AllAppsAppIcon.java
@@ -0,0 +1,38 @@
+/*
+ * 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.launcher3.tapl;
+
+import androidx.test.uiautomator.UiObject2;
+
+import java.util.regex.Pattern;
+
+/**
+ * App icon in all apps.
+ */
+final class AllAppsAppIcon extends AppIcon {
+
+    private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onAllAppsItemLongClick");
+
+    AllAppsAppIcon(LauncherInstrumentation launcher, UiObject2 icon) {
+        super(launcher, icon);
+    }
+
+    @Override
+    protected Pattern getLongClickEvent() {
+        return LONG_CLICK_EVENT;
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 6da59da..8fa9e12 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -32,9 +32,7 @@
 /**
  * App icon, whether in all apps or in workspace/
  */
-public final class AppIcon extends Launchable implements FolderDragTarget {
-
-    private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onAllAppsItemLongClick");
+public abstract class AppIcon extends Launchable implements FolderDragTarget {
 
     AppIcon(LauncherInstrumentation launcher, UiObject2 icon) {
         super(launcher, icon);
@@ -44,13 +42,15 @@
         return By.clazz(TextView.class).text(appName).pkg(launcher.getLauncherPackageName());
     }
 
+    protected abstract Pattern getLongClickEvent();
+
     /**
      * Long-clicks the icon to open its menu.
      */
     public AppIconMenu openMenu() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
             return new AppIconMenu(mLauncher, mLauncher.clickAndGet(
-                    mObject, "popup_container", LONG_CLICK_EVENT));
+                    mObject, "popup_container", getLongClickEvent()));
         }
     }
 
@@ -60,7 +60,7 @@
     public AppIconMenu openDeepShortcutMenu() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
             return new AppIconMenu(mLauncher, mLauncher.clickAndGet(
-                    mObject, "deep_shortcuts_container", LONG_CLICK_EVENT));
+                    mObject, "deep_shortcuts_container", getLongClickEvent()));
         }
     }
 
@@ -89,7 +89,7 @@
 
     @Override
     protected void addExpectedEventsForLongClick() {
-        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT);
+        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, getLongClickEvent());
     }
 
     @Override
@@ -122,4 +122,20 @@
         }
         return null;
     }
-}
+
+    /**
+     * Uninstall the appIcon by dragging it to the 'uninstall' drop point of the drop_target_bar.
+     *
+     * @return validated workspace after the existing appIcon being uninstalled.
+     */
+    public Workspace uninstall() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "uninstalling app icon")) {
+            return Workspace.uninstallAppIcon(
+                    mLauncher, this,
+                    this::addExpectedEventsForLongClick
+            );
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Folder.java b/tests/tapl/com/android/launcher3/tapl/Folder.java
index dba308d..f046b6e 100644
--- a/tests/tapl/com/android/launcher3/tapl/Folder.java
+++ b/tests/tapl/com/android/launcher3/tapl/Folder.java
@@ -43,7 +43,7 @@
     public AppIcon getAppIcon(String appName) {
         try (LauncherInstrumentation.Closable ignored = mLauncher.addContextLayer(
                 "Want to get app icon in folder")) {
-            return new AppIcon(mLauncher,
+            return new WorkspaceAppIcon(mLauncher,
                     mLauncher.waitForObjectInContainer(
                             mContainer,
                             AppIcon.getAppIconSelector(appName, mLauncher)));
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 37a205c..b42f5c1 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1284,10 +1284,8 @@
         return getRealDisplaySize().x - getWindowInsets().right - 1;
     }
 
-    void clickLauncherObject(UiObject2 object) {
-        waitForObjectEnabled(object, "clickLauncherObject");
-        expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_DOWN);
-        expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_UP);
+    void clickObject(UiObject2 object) {
+        waitForObjectEnabled(object, "clickObject");
         if (!isLauncher3() && getNavigationModel() != NavigationModel.THREE_BUTTON) {
             expectEvent(TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS);
             expectEvent(TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_UP_TIS);
@@ -1295,6 +1293,12 @@
         object.click();
     }
 
+    void clickLauncherObject(UiObject2 object) {
+        expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_DOWN);
+        expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_UP);
+        clickObject(object);
+    }
+
     void scrollToLastVisibleRow(
             UiObject2 container,
             Collection<UiObject2> items,
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 3f0d7fd..c71c11f 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.SPRING_LOADED_STATE_ORDINAL;
 
+import static junit.framework.TestCase.assertNotNull;
 import static junit.framework.TestCase.assertTrue;
 
 import android.graphics.Point;
@@ -31,8 +32,11 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
 
 import com.android.launcher3.testing.TestProtocol;
 
@@ -49,6 +53,7 @@
     private static final int DEFAULT_DRAG_STEPS = 10;
     private static final String DROP_BAR_RES_ID = "drop_target_bar";
     private static final String DELETE_TARGET_TEXT_ID = "delete_target_text";
+    private static final String UNINSTALL_TARGET_TEXT_ID = "uninstall_target_text";
 
     static final Pattern EVENT_CTRL_W_DOWN = Pattern.compile(
             "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_W"
@@ -56,7 +61,7 @@
     static final Pattern EVENT_CTRL_W_UP = Pattern.compile(
             "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_W"
                     + ".*?metaState=META_CTRL_ON");
-    private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onWorkspaceItemLongClick");
+    static final Pattern LONG_CLICK_EVENT = Pattern.compile("onWorkspaceItemLongClick");
 
     private final UiObject2 mHotseat;
 
@@ -81,8 +86,8 @@
             final int windowCornerRadius = (int) Math.ceil(mLauncher.getWindowCornerRadius());
             final int startY = deviceHeight - Math.max(bottomGestureMargin, windowCornerRadius) - 1;
             final int swipeHeight = mLauncher.getTestInfo(
-                    TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT).
-                    getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+                            TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT)
+                    .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
             LauncherInstrumentation.log(
                     "switchToAllApps: deviceHeight = " + deviceHeight + ", startY = " + startY
                             + ", swipeHeight = " + swipeHeight + ", slop = "
@@ -116,7 +121,7 @@
             final UiObject2 workspace = verifyActiveContainer();
             final UiObject2 icon = workspace.findObject(
                     AppIcon.getAppIconSelector(appName, mLauncher));
-            return icon != null ? new AppIcon(mLauncher, icon) : null;
+            return icon != null ? new WorkspaceAppIcon(mLauncher, icon) : null;
         }
     }
 
@@ -131,7 +136,7 @@
     public AppIcon getWorkspaceAppIcon(String appName) {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to get a workspace icon")) {
-            return new AppIcon(mLauncher,
+            return new WorkspaceAppIcon(mLauncher,
                     mLauncher.waitForObjectInContainer(
                             verifyActiveContainer(),
                             AppIcon.getAppIconSelector(appName, mLauncher)));
@@ -205,7 +210,7 @@
 
     @NonNull
     public AppIcon getHotseatAppIcon(String appName) {
-        return new AppIcon(mLauncher, mLauncher.waitForObjectInContainer(
+        return new WorkspaceAppIcon(mLauncher, mLauncher.waitForObjectInContainer(
                 mHotseat, AppIcon.getAppIconSelector(appName, mLauncher)));
     }
 
@@ -215,12 +220,13 @@
     }
 
     /*
-     * Get the center point of the delete icon in the drop target bar.
+     * Get the center point of the delete/uninstall icon in the drop target bar.
      */
-    private Point getDeleteDropPoint() {
-        return mLauncher.waitForObjectInContainer(
-                mLauncher.waitForLauncherObject(DROP_BAR_RES_ID),
-                DELETE_TARGET_TEXT_ID).getVisibleCenter();
+    private static Point getDropPointFromDropTargetBar(
+            LauncherInstrumentation launcher, String targetId) {
+        return launcher.waitForObjectInContainer(
+                launcher.waitForLauncherObject(DROP_BAR_RES_ID),
+                targetId).getVisibleCenter();
     }
 
     /**
@@ -235,7 +241,7 @@
                      "removing app icon from workspace")) {
             dragIconToWorkspace(
                     mLauncher, appIcon,
-                    () -> getDeleteDropPoint(),
+                    () -> getDropPointFromDropTargetBar(mLauncher, DELETE_TARGET_TEXT_ID),
                     true, /* decelerating */
                     appIcon.getLongPressIndicator(),
                     () -> mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT),
@@ -249,6 +255,47 @@
     }
 
     /**
+     * Uninstall the appIcon by dragging it to the 'uninstall' drop point of the drop_target_bar.
+     *
+     * @param launcher the root TAPL instrumentation object of {@link LauncherInstrumentation} type.
+     * @param appIcon to be uninstalled.
+     * @param expectLongClickEvents the runnable to be executed to verify expected longclick event.
+     * @return validated workspace after the existing appIcon being uninstalled.
+     */
+    static Workspace uninstallAppIcon(LauncherInstrumentation launcher, AppIcon appIcon,
+            Runnable expectLongClickEvents) {
+        try (LauncherInstrumentation.Closable c = launcher.addContextLayer(
+                "uninstalling app icon")) {
+            dragIconToWorkspace(
+                    launcher, appIcon,
+                    () -> getDropPointFromDropTargetBar(launcher, UNINSTALL_TARGET_TEXT_ID),
+                    true, /* decelerating */
+                    appIcon.getLongPressIndicator(),
+                    expectLongClickEvents,
+                    null);
+
+            launcher.waitUntilLauncherObjectGone(DROP_BAR_RES_ID);
+
+            final BySelector installerAlert = By.text(Pattern.compile(
+                    "Do you want to uninstall this app\\?",
+                    Pattern.DOTALL | Pattern.MULTILINE));
+            final UiDevice device = launcher.getDevice();
+            assertTrue("uninstall alert is not shown", device.wait(
+                    Until.hasObject(installerAlert), LauncherInstrumentation.WAIT_TIME_MS));
+            final UiObject2 ok = device.findObject(By.text("OK"));
+            assertNotNull("OK button is not shown", ok);
+            launcher.clickObject(ok);
+            assertTrue("Uninstall alert is not dismissed after clicking OK", device.wait(
+                    Until.gone(installerAlert), LauncherInstrumentation.WAIT_TIME_MS));
+
+            try (LauncherInstrumentation.Closable c1 = launcher.addContextLayer(
+                    "uninstalled app by dragging to the drop bar")) {
+                return new Workspace(launcher);
+            }
+        }
+    }
+
+    /**
      * Finds folder icons in the current workspace.
      *
      * @return a list of folder icons.
diff --git a/tests/tapl/com/android/launcher3/tapl/WorkspaceAppIcon.java b/tests/tapl/com/android/launcher3/tapl/WorkspaceAppIcon.java
new file mode 100644
index 0000000..5f4e469
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/WorkspaceAppIcon.java
@@ -0,0 +1,36 @@
+/*
+ * 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.launcher3.tapl;
+
+import androidx.test.uiautomator.UiObject2;
+
+import java.util.regex.Pattern;
+
+/**
+ * App icon in workspace.
+ */
+final class WorkspaceAppIcon extends AppIcon {
+
+    WorkspaceAppIcon(LauncherInstrumentation launcher, UiObject2 icon) {
+        super(launcher, icon);
+    }
+
+    @Override
+    protected Pattern getLongClickEvent() {
+        return Workspace.LONG_CLICK_EVENT;
+    }
+}