Merge "Create a launcher model override." into ub-launcher3-master
diff --git a/Android.bp b/Android.bp
index e3dd5e5..c583244 100644
--- a/Android.bp
+++ b/Android.bp
@@ -29,23 +29,3 @@
     ],
     platform_apis: true,
 }
-
-
-android_library {
-    name: "icon-loader",
-    sdk_version: "28",
-    static_libs: [
-        "androidx.core_core",
-    ],
-    resource_dirs: [
-        "res",
-    ],
-    srcs: [
-        "src/com/android/launcher3/icons/BaseIconFactory.java",
-        "src/com/android/launcher3/icons/BitmapInfo.java",
-        "src/com/android/launcher3/icons/IconNormalizer.java",
-        "src/com/android/launcher3/icons/FixedScaleDrawable.java",
-        "src/com/android/launcher3/icons/ShadowGenerator.java",
-        "src/com/android/launcher3/icons/ColorExtractor.java",
-    ],
-}
diff --git a/Android.mk b/Android.mk
index 5614e25..9d6e629 100644
--- a/Android.mk
+++ b/Android.mk
@@ -67,7 +67,8 @@
 LOCAL_STATIC_ANDROID_LIBRARIES := \
     androidx.recyclerview_recyclerview \
     androidx.dynamicanimation_dynamicanimation \
-    androidx.preference_preference
+    androidx.preference_preference \
+    iconloader
 
 LOCAL_STATIC_JAVA_LIBRARIES := LauncherPluginLib
 
diff --git a/build.gradle b/build.gradle
index 476e92b..33409c5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,19 +4,17 @@
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.2.1'
-        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.6'
+        classpath GRADLE_CLASS_PATH
+        classpath PROTOBUF_CLASS_PATH
     }
 }
 
-final String SUPPORT_LIBS_VERSION = '1.0.0-alpha1'
-
 apply plugin: 'com.android.application'
 apply plugin: 'com.google.protobuf'
 
 android {
-    compileSdkVersion 28
-    buildToolsVersion '28.0.3'
+    compileSdkVersion COMPILE_SDK.toInteger()
+    buildToolsVersion BUILD_TOOLS_VERSION
 
     defaultConfig {
         minSdkVersion 21
@@ -120,9 +118,15 @@
 }
 
 dependencies {
-    implementation "androidx.dynamicanimation:dynamicanimation:${SUPPORT_LIBS_VERSION}"
-    implementation "androidx.recyclerview:recyclerview:${SUPPORT_LIBS_VERSION}"
-    implementation 'com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-7'
+    implementation "androidx.dynamicanimation:dynamicanimation:${ANDROID_X_VERSION}"
+    implementation "androidx.recyclerview:recyclerview:${ANDROID_X_VERSION}"
+    implementation "androidx.preference:preference:${ANDROID_X_VERSION}"
+    implementation PROTOBUF_DEPENDENCY
+    implementation project(':IconLoader')
+
+    // This is already included in sysui_shared
+    aospImplementation fileTree(dir: "libs", include: 'plugin_core.jar')
+    l3goImplementation fileTree(dir: "libs", include: 'plugin_core.jar')
 
     quickstepImplementation fileTree(dir: "quickstep/libs", include: 'sysui_shared.jar')
 
@@ -133,7 +137,7 @@
     androidTestImplementation 'com.android.support.test:runner:1.0.0'
     androidTestImplementation 'com.android.support.test:rules:1.0.0'
     androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
-    androidTestImplementation "androidx.annotation:annotation:${SUPPORT_LIBS_VERSION}"
+    androidTestImplementation "androidx.annotation:annotation:${ANDROID_X_VERSION}"
 }
 
 protobuf {
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..b299cfe
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,13 @@
+# Until all the dependencies move to android X
+android.useAndroidX = true
+android.enableJetifier = true
+
+ANDROID_X_VERSION=1.0.0-beta01
+
+GRADLE_CLASS_PATH=com.android.tools.build:gradle:3.2.0-rc03
+
+PROTOBUF_CLASS_PATH=com.google.protobuf:protobuf-gradle-plugin:0.8.6
+PROTOBUF_DEPENDENCY=com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-7
+
+BUILD_TOOLS_VERSION=28.0.3
+COMPILE_SDK=28
\ No newline at end of file
diff --git a/iconloaderlib/Android.bp b/iconloaderlib/Android.bp
new file mode 100644
index 0000000..8a71f94
--- /dev/null
+++ b/iconloaderlib/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+android_library {
+    name: "iconloader",
+    sdk_version: "28",
+    min_sdk_version: "21",
+    static_libs: [
+        "androidx.core_core",
+    ],
+    resource_dirs: [
+        "res",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+}
diff --git a/iconloaderlib/AndroidManifest.xml b/iconloaderlib/AndroidManifest.xml
new file mode 100644
index 0000000..b30258d
--- /dev/null
+++ b/iconloaderlib/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.launcher3.icons">
+</manifest>
diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle
new file mode 100644
index 0000000..d080293
--- /dev/null
+++ b/iconloaderlib/build.gradle
@@ -0,0 +1,50 @@
+buildscript {
+    repositories {
+        mavenCentral()
+        google()
+    }
+    dependencies {
+        classpath GRADLE_CLASS_PATH
+    }
+}
+
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion COMPILE_SDK.toInteger()
+    buildToolsVersion BUILD_TOOLS_VERSION
+    publishNonDefault true
+
+    defaultConfig {
+        minSdkVersion 21
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+    }
+
+    sourceSets {
+        main {
+            java.srcDirs = ['src']
+            manifest.srcFile 'AndroidManifest.xml'
+            res.srcDirs = ['res']
+        }
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+
+    tasks.withType(JavaCompile) {
+        options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+    }
+}
+
+
+repositories {
+    mavenCentral()
+    google()
+}
+
+dependencies {
+    implementation "androidx.core:core:${ANDROID_X_VERSION}"
+}
diff --git a/res/drawable-v26/adaptive_icon_drawable_wrapper.xml b/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml
similarity index 100%
rename from res/drawable-v26/adaptive_icon_drawable_wrapper.xml
rename to iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml
diff --git a/res/drawable/ic_instant_app_badge.xml b/iconloaderlib/res/drawable/ic_instant_app_badge.xml
similarity index 93%
rename from res/drawable/ic_instant_app_badge.xml
rename to iconloaderlib/res/drawable/ic_instant_app_badge.xml
index cc53230..b74317e 100644
--- a/res/drawable/ic_instant_app_badge.xml
+++ b/iconloaderlib/res/drawable/ic_instant_app_badge.xml
@@ -21,23 +21,19 @@
 
     <path
         android:fillColor="@android:color/black"
-        android:fillType="evenOdd"
         android:strokeWidth="1"
         android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
     <path
         android:fillColor="@android:color/white"
-        android:fillType="evenOdd"
         android:strokeWidth="1"
         android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
     <path
         android:fillColor="@android:color/white"
-        android:fillType="evenOdd"
         android:strokeWidth="1"
         android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
     <path
         android:fillColor="@android:color/black"
         android:fillAlpha="0.87"
-        android:fillType="evenOdd"
         android:strokeWidth="1"
         android:pathData="M 6 10.4123279 L 8.63934949 10.4123279 L 8.63934949 15.6 L 12.5577168 7.84517705 L 9.94547194 7.84517705 L 9.94547194 2 Z" />
-</vector>
\ No newline at end of file
+</vector>
diff --git a/iconloaderlib/res/values/colors.xml b/iconloaderlib/res/values/colors.xml
new file mode 100644
index 0000000..873b2fc
--- /dev/null
+++ b/iconloaderlib/res/values/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2018, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<resources>
+    <color name="legacy_icon_background">#FFFFFF</color>
+</resources>
diff --git a/iconloaderlib/res/values/dimens.xml b/iconloaderlib/res/values/dimens.xml
new file mode 100644
index 0000000..e8c0c44
--- /dev/null
+++ b/iconloaderlib/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <dimen name="profile_badge_size">24dp</dimen>
+</resources>
diff --git a/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android.launcher3/icons/BaseIconFactory.java
similarity index 99%
rename from src/com/android/launcher3/icons/BaseIconFactory.java
rename to iconloaderlib/src/com/android.launcher3/icons/BaseIconFactory.java
index cd60de5..681c03c 100644
--- a/src/com/android/launcher3/icons/BaseIconFactory.java
+++ b/iconloaderlib/src/com/android.launcher3/icons/BaseIconFactory.java
@@ -18,7 +18,7 @@
 import android.os.Process;
 import android.os.UserHandle;
 
-import com.android.launcher3.R;
+import com.android.launcher3.icons.R;
 
 import static android.graphics.Paint.DITHER_FLAG;
 import static android.graphics.Paint.FILTER_BITMAP_FLAG;
diff --git a/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android.launcher3/icons/BitmapInfo.java
similarity index 100%
rename from src/com/android/launcher3/icons/BitmapInfo.java
rename to iconloaderlib/src/com/android.launcher3/icons/BitmapInfo.java
diff --git a/src/com/android/launcher3/icons/ColorExtractor.java b/iconloaderlib/src/com/android.launcher3/icons/ColorExtractor.java
similarity index 100%
rename from src/com/android/launcher3/icons/ColorExtractor.java
rename to iconloaderlib/src/com/android.launcher3/icons/ColorExtractor.java
diff --git a/src/com/android/launcher3/icons/FixedScaleDrawable.java b/iconloaderlib/src/com/android.launcher3/icons/FixedScaleDrawable.java
similarity index 100%
rename from src/com/android/launcher3/icons/FixedScaleDrawable.java
rename to iconloaderlib/src/com/android.launcher3/icons/FixedScaleDrawable.java
diff --git a/src/com/android/launcher3/icons/IconNormalizer.java b/iconloaderlib/src/com/android.launcher3/icons/IconNormalizer.java
similarity index 98%
rename from src/com/android/launcher3/icons/IconNormalizer.java
rename to iconloaderlib/src/com/android.launcher3/icons/IconNormalizer.java
index 8eb8252..05908df 100644
--- a/src/com/android/launcher3/icons/IconNormalizer.java
+++ b/iconloaderlib/src/com/android.launcher3/icons/IconNormalizer.java
@@ -20,9 +20,6 @@
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.AdaptiveIconDrawable;
diff --git a/src/com/android/launcher3/icons/ShadowGenerator.java b/iconloaderlib/src/com/android.launcher3/icons/ShadowGenerator.java
similarity index 100%
rename from src/com/android/launcher3/icons/ShadowGenerator.java
rename to iconloaderlib/src/com/android.launcher3/icons/ShadowGenerator.java
diff --git a/res/values/colors.xml b/res/values/colors.xml
index eb207af..3c8fe1e 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -34,7 +34,6 @@
     <color name="notification_icon_default_color">#757575</color> <!-- Gray 600 -->
 
     <color name="icon_background">#E0E0E0</color> <!-- Gray 300 -->
-    <color name="legacy_icon_background">#FFFFFF</color>
 
     <color name="all_apps_bg_hand_fill">#E5E5E5</color>
     <color name="all_apps_bg_hand_fill_dark">#9AA0A6</color>
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..b52bd4f
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+include ':IconLoader'
+project(':IconLoader').projectDir = new File(rootDir, 'iconloaderlib')
diff --git a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java b/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
index 00cc1a7..6c98d16 100644
--- a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
+++ b/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
@@ -121,7 +121,6 @@
 
     private Workspace mWorkspace;
 
-    private final boolean mHasSysUiScrim;
     private boolean mDrawTopScrim, mDrawBottomScrim;
 
     private final RectF mFinalMaskRect = new RectF();
@@ -149,15 +148,9 @@
 
         mMaskHeight = Utilities.pxFromDp(ALPHA_MASK_BITMAP_DP,
                 view.getResources().getDisplayMetrics());
-
-        mHasSysUiScrim = !mWallpaperColorInfo.supportsDarkText();
-        if (mHasSysUiScrim) {
-            mTopScrim = Themes.getAttrDrawable(view.getContext(), R.attr.workspaceStatusBarScrim);
-            mBottomMask = createDitheredAlphaMask();
-        } else {
-            mTopScrim = null;
-            mBottomMask = null;
-        }
+        mTopScrim = Themes.getAttrDrawable(view.getContext(), R.attr.workspaceStatusBarScrim);
+        mBottomMask = mTopScrim == null ? null : createDitheredAlphaMask();
+        mHideSysUiScrim = mTopScrim == null;
 
         view.addOnAttachStateChangeListener(this);
         onExtractedColorsChanged(mWallpaperColorInfo);
@@ -185,7 +178,7 @@
             canvas.restore();
         }
 
-        if (!mHideSysUiScrim && mHasSysUiScrim) {
+        if (!mHideSysUiScrim) {
             if (mSysUiProgress <= 0) {
                 mAnimateScrimOnNextDraw = false;
                 return;
@@ -213,8 +206,9 @@
     }
 
     public void onInsetsChanged(Rect insets) {
-        mDrawTopScrim = insets.top > 0;
-        mDrawBottomScrim = !mLauncher.getDeviceProfile().isVerticalBarLayout();
+        mDrawTopScrim = mTopScrim != null && insets.top > 0;
+        mDrawBottomScrim = mBottomMask != null &&
+                !mLauncher.getDeviceProfile().isVerticalBarLayout();
     }
 
     private void setScrimProgress(float progress) {
@@ -230,7 +224,7 @@
         mWallpaperColorInfo.addOnChangeListener(this);
         onExtractedColorsChanged(mWallpaperColorInfo);
 
-        if (mHasSysUiScrim) {
+        if (mTopScrim != null) {
             IntentFilter filter = new IntentFilter(ACTION_SCREEN_OFF);
             filter.addAction(ACTION_USER_PRESENT); // When the device wakes up + keyguard is gone
             mRoot.getContext().registerReceiver(mReceiver, filter);
@@ -240,7 +234,7 @@
     @Override
     public void onViewDetachedFromWindow(View view) {
         mWallpaperColorInfo.removeOnChangeListener(this);
-        if (mHasSysUiScrim) {
+        if (mTopScrim != null) {
             mRoot.getContext().unregisterReceiver(mReceiver);
         }
     }
@@ -259,14 +253,14 @@
     }
 
     public void setSize(int w, int h) {
-        if (mHasSysUiScrim) {
+        if (mTopScrim != null) {
             mTopScrim.setBounds(0, 0, w, h);
             mFinalMaskRect.set(0, h - mMaskHeight, w, h);
         }
     }
 
     public void hideSysUiScrim(boolean hideSysUiScrim) {
-        mHideSysUiScrim = hideSysUiScrim;
+        mHideSysUiScrim = hideSysUiScrim || (mTopScrim == null);
         if (!hideSysUiScrim) {
             mAnimateScrimOnNextDraw = true;
         }
@@ -281,18 +275,18 @@
     }
 
     private void reapplySysUiAlpha() {
-        if (mHasSysUiScrim) {
-            reapplySysUiAlphaNoInvalidate();
-            if (!mHideSysUiScrim) {
-                invalidate();
-            }
+        reapplySysUiAlphaNoInvalidate();
+        if (!mHideSysUiScrim) {
+            invalidate();
         }
     }
 
     private void reapplySysUiAlphaNoInvalidate() {
         float factor = mSysUiProgress * mSysUiAnimMultiplier;
         mBottomMaskPaint.setAlpha(Math.round(MAX_HOTSEAT_SCRIM_ALPHA * factor));
-        mTopScrim.setAlpha(Math.round(255 * factor));
+        if (mTopScrim != null) {
+            mTopScrim.setAlpha(Math.round(255 * factor));
+        }
     }
 
     public void invalidate() {
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 532d3e8..bc5aaee 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -296,11 +296,6 @@
                         Process.myUserHandle()).get(0);
     }
 
-    protected LauncherActivityInfo getChromeApp() {
-        return LauncherAppsCompat.getInstance(mTargetContext)
-                .getActivityList("com.android.chrome", Process.myUserHandle()).get(0);
-    }
-
     /**
      * Broadcast receiver which blocks until the result is received.
      */
diff --git a/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java b/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
index d7a7f6b..e95801a 100644
--- a/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
+++ b/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
@@ -1,22 +1,20 @@
 package com.android.launcher3.ui;
 
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import android.content.pm.LauncherActivityInfo;
-import android.graphics.Point;
+
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
-import android.view.MotionEvent;
 
-import com.android.launcher3.R;
-import com.android.launcher3.util.Condition;
-import com.android.launcher3.util.Wait;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.popup.ArrowPopup;
+import com.android.launcher3.tapl.AppIconMenu;
+import com.android.launcher3.tapl.AppIconMenuItem;
+import com.android.launcher3.views.OptionsPopupView;
 
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -27,47 +25,33 @@
 @RunWith(AndroidJUnit4.class)
 public class ShortcutsLaunchTest extends AbstractLauncherUiTest {
 
-    @Test
-    @Ignore
-    public void testAppLauncher_portrait() throws Exception {
-        lockRotation(true);
-        performTest();
+    private boolean isOptionsPopupVisible(Launcher launcher) {
+        final ArrowPopup popup = OptionsPopupView.getOptionsPopup(launcher);
+        return popup != null && popup.isShown();
     }
 
     @Test
-    @Ignore
-    public void testAppLauncher_landscape() throws Exception {
-        lockRotation(false);
-        performTest();
-    }
-
-    private void performTest() throws Exception {
+    @PortraitLandscape
+    public void testAppLauncher() throws Exception {
         mActivityMonitor.startLauncher();
-        LauncherActivityInfo testApp = getSettingsApp();
+        final LauncherActivityInfo testApp = getSettingsApp();
 
-        // Open all apps and wait for load complete
-        final UiObject2 appsContainer = TestViewHelpers.openAllApps();
-        Wait.atMost(null, Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT);
+        final AppIconMenu menu = mLauncher.
+                pressHome().
+                switchToAllApps().
+                getAppIcon(testApp.getLabel().toString()).
+                openMenu();
 
-        // Find settings app and verify shortcuts appear when long pressed
-        UiObject2 icon = scrollAndFind(appsContainer, By.text(testApp.getLabel().toString()));
-        // Press icon center until shortcuts appear
-        Point iconCenter = icon.getVisibleCenter();
-        TestViewHelpers.sendPointer(MotionEvent.ACTION_DOWN, iconCenter);
-        UiObject2 deepShortcutsContainer = TestViewHelpers.findViewById(
-                R.id.deep_shortcuts_container);
-        assertNotNull(deepShortcutsContainer);
-        TestViewHelpers.sendPointer(MotionEvent.ACTION_UP, iconCenter);
+        executeOnLauncher(
+                launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
+                        isOptionsPopupVisible(launcher)));
 
-        // Verify that launching a shortcut opens a page with the same text
-        assertTrue(deepShortcutsContainer.getChildCount() > 0);
+        final AppIconMenuItem menuItem = menu.getMenuItem(1);
+        final String itemName = menuItem.getText();
 
-        // Pick second children as it starts showing shortcuts.
-        UiObject2 shortcut = deepShortcutsContainer.getChildren().get(1)
-                .findObject(TestViewHelpers.getSelectorForId(R.id.bubble_text));
-        shortcut.click();
+        menuItem.launch();
         assertTrue(mDevice.wait(Until.hasObject(By.pkg(
                 testApp.getComponentName().getPackageName())
-                .text(shortcut.getText())), DEFAULT_UI_TIMEOUT));
+                .text(itemName)), DEFAULT_UI_TIMEOUT));
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index b7ae9f1..1c0ecb9 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.tapl;
 
+import android.graphics.Point;
 import android.widget.TextView;
 
 import androidx.test.uiautomator.By;
@@ -56,4 +57,15 @@
     UiObject2 getIcon() {
         return mIcon;
     }
+
+    /**
+     * Long-clicks the icon to open its menu.
+     */
+    public AppIconMenu openMenu() {
+        final Point iconCenter = mIcon.getVisibleCenter();
+        mLauncher.longTap(iconCenter.x, iconCenter.y);
+        final UiObject2 deepShortcutsContainer = mLauncher.waitForLauncherObject(
+                "deep_shortcuts_container");
+        return new AppIconMenu(mLauncher, deepShortcutsContainer);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
new file mode 100644
index 0000000..2a03f9a
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenu.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.tapl;
+
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.uiautomator.UiObject2;
+
+/**
+ * Context menu of an app icon.
+ */
+public class AppIconMenu {
+    private final LauncherInstrumentation mLauncher;
+    private final UiObject2 mDeepShortcutsContainer;
+
+    AppIconMenu(LauncherInstrumentation launcher,
+            UiObject2 deepShortcutsContainer) {
+        mLauncher = launcher;
+        mDeepShortcutsContainer = deepShortcutsContainer;
+    }
+
+    /**
+     * Returns a menu item with a given number. Fails if it doesn't exist.
+     */
+    public AppIconMenuItem getMenuItem(int itemNumber) {
+        assertTrue(mDeepShortcutsContainer.getChildCount() > itemNumber);
+
+        final UiObject2 shortcut = mLauncher.waitForObjectInContainer(
+                mDeepShortcutsContainer.getChildren().get(itemNumber), "bubble_text");
+        return new AppIconMenuItem(mLauncher, shortcut);
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
new file mode 100644
index 0000000..7b2abeb
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.tapl;
+
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+/**
+ * Menu item in an app icon menu.
+ */
+public class AppIconMenuItem {
+    private final LauncherInstrumentation mLauncher;
+    final UiObject2 mShortcut;
+
+    AppIconMenuItem(LauncherInstrumentation launcher,
+            UiObject2 shortcut) {
+        mLauncher = launcher;
+        mShortcut = shortcut;
+    }
+
+    /**
+     * Returns the visible text of the menu item.
+     */
+    public String getText() {
+        return mShortcut.getText();
+    }
+
+    /**
+     * Launches the action for the menu item.
+     */
+    public Background launch() {
+        assertTrue("Clicking a menu item didn't open a new window: " + mShortcut.getText(),
+                mShortcut.clickAndWait(Until.newWindow(), LauncherInstrumentation.WAIT_TIME_MS));
+        return new Background(mLauncher);
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 31abc53..67106f7 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -28,6 +28,13 @@
 import android.view.Surface;
 import android.view.accessibility.AccessibilityEvent;
 
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
 import com.android.launcher3.TestProtocol;
 import com.android.quickstep.SwipeUpSetting;
 
@@ -36,13 +43,6 @@
 import java.lang.ref.WeakReference;
 import java.util.concurrent.TimeoutException;
 
-import androidx.annotation.NonNull;
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.BySelector;
-import androidx.test.uiautomator.UiDevice;
-import androidx.test.uiautomator.UiObject2;
-import androidx.test.uiautomator.Until;
-
 /**
  * The main tapl object. The only object that can be explicitly constructed by the using code. It
  * produces all other objects.
@@ -394,6 +394,11 @@
         return mDevice;
     }
 
+    void longTap(int x, int y) {
+        mDevice.drag(x, y, x, y, 0);
+    }
+
+
     void swipe(int startX, int startY, int endX, int endY) {
         executeAndWaitForEvent(
                 () -> mDevice.swipe(startX, startY, endX, endY, 60),