Merge "Remove first icon from notification footer after it animates." into ub-launcher3-dorval
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index 7bc3692..84a8bce 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -83,7 +83,7 @@
 
         LauncherAppState app = LauncherAppState.getInstanceNoCreate();
         if (app != null) {
-            app.reloadWorkspace();
+            app.getModel().forceReload();
         }
         asyncResult.finish();
     }
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index e0e53a6..2e75579 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -134,15 +134,6 @@
         PackageInstallerCompat.getInstance(mContext).onStop();
     }
 
-    /**
-     * Reloads the workspace items from the DB and re-binds the workspace. This should generally
-     * not be called as DB updates are automatically followed by UI update
-     */
-    public void reloadWorkspace() {
-        mModel.resetLoadedState(false, true);
-        mModel.startLoaderFromBackground();
-    }
-
     LauncherModel setLauncher(Launcher launcher) {
         getLocalProvider(mContext).setLauncherProviderChangeListener(launcher);
         mModel.initialize(launcher);
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 5fd5081..590c242 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -71,8 +71,6 @@
 import com.android.launcher3.shortcuts.ShortcutInfoCompat;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.ContentWriter;
-import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.ManagedProfileHeuristic;
 import com.android.launcher3.util.MultiHashMap;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -93,6 +91,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.Executor;
 
 /**
@@ -123,12 +122,11 @@
     }
     @Thunk static final Handler sWorker = new Handler(sWorkerThread.getLooper());
 
-    // We start off with everything not loaded.  After that, we assume that
+    // Indicates whether the current model data is valid or not.
+    // We start off with everything not loaded. After that, we assume that
     // our monitoring of the package manager provides all updates and we never
-    // need to do a requery.  These are only ever touched from the loader thread.
-    private boolean mWorkspaceLoaded;
-    private boolean mAllAppsLoaded;
-    private boolean mDeepShortcutsLoaded;
+    // need to do a requery. This is only ever touched from the loader thread.
+    private boolean mModelLoaded;
 
     /**
      * Set of runnables to be called on the background thread after the workspace binding
@@ -148,11 +146,11 @@
     private final Runnable mShortcutPermissionCheckRunnable = new Runnable() {
         @Override
         public void run() {
-            if (mDeepShortcutsLoaded) {
+            if (mModelLoaded) {
                 boolean hasShortcutHostPermission =
                         DeepShortcutManager.getInstance(mApp.getContext()).hasHostPermission();
                 if (hasShortcutHostPermission != mHasShortcutHostPermission) {
-                    mApp.reloadWorkspace();
+                    forceReload();
                 }
             }
         }
@@ -480,8 +478,16 @@
         }
     }
 
-    void forceReload() {
-        resetLoadedState(true, true);
+    /**
+     * Reloads the workspace items from the DB and re-binds the workspace. This should generally
+     * not be called as DB updates are automatically followed by UI update
+     */
+    public void forceReload() {
+        synchronized (mLock) {
+            // Stop any existing loaders first, so they don't set mModelLoaded to true later
+            stopLoaderLocked();
+            mModelLoaded = false;
+        }
 
         // Do this here because if the launcher activity is running it will be restarted.
         // If it's not running startLoaderFromBackground will merely tell it that it needs
@@ -489,19 +495,6 @@
         startLoaderFromBackground();
     }
 
-    public void resetLoadedState(boolean resetAllAppsLoaded, boolean resetWorkspaceLoaded) {
-        synchronized (mLock) {
-            // Stop any existing loaders first, so they don't set mAllAppsLoaded or
-            // mWorkspaceLoaded to true later
-            stopLoaderLocked();
-            if (resetAllAppsLoaded) mAllAppsLoaded = false;
-            if (resetWorkspaceLoaded) mWorkspaceLoaded = false;
-            // Always reset deep shortcuts loaded.
-            // TODO: why?
-            mDeepShortcutsLoaded = false;
-        }
-    }
-
     /**
      * When the launcher is in the background, it's possible for it to miss paired
      * configuration changes.  So whenever we trigger the loader from the background
@@ -553,9 +546,8 @@
                 // If there is already one running, tell it to stop.
                 stopLoaderLocked();
                 mLoaderTask = new LoaderTask(mApp.getContext(), synchronousBindPage);
-                // TODO: mDeepShortcutsLoaded does not need to be true for synchronous bind.
-                if (synchronousBindPage != PagedView.INVALID_RESTORE_PAGE && mAllAppsLoaded
-                        && mWorkspaceLoaded && mDeepShortcutsLoaded && !mIsLoaderTaskRunning) {
+                if (synchronousBindPage != PagedView.INVALID_RESTORE_PAGE
+                        && mModelLoaded && !mIsLoaderTaskRunning) {
                     mLoaderTask.runBindSynchronousPage(synchronousBindPage);
                     return true;
                 } else {
@@ -607,28 +599,6 @@
             mPageToBindFirst = pageToBindFirst;
         }
 
-        private void loadAndBindWorkspace() {
-            mIsLoadingAndBindingWorkspace = true;
-
-            // Load the workspace
-            if (DEBUG_LOADERS) {
-                Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded);
-            }
-
-            if (!mWorkspaceLoaded) {
-                loadWorkspace();
-                synchronized (LoaderTask.this) {
-                    if (mStopped) {
-                        return;
-                    }
-                    mWorkspaceLoaded = true;
-                }
-            }
-
-            // Bind the workspace
-            bindWorkspace(mPageToBindFirst);
-        }
-
         private void waitForIdle() {
             // Wait until the either we're stopped or the other threads are done.
             // This way we don't start loading all apps until the workspace has settled
@@ -671,7 +641,7 @@
                 throw new RuntimeException("Should not call runBindSynchronousPage() without " +
                         "valid page index");
             }
-            if (!mAllAppsLoaded || !mWorkspaceLoaded) {
+            if (!mModelLoaded) {
                 // Ensure that we don't try and bind a specified page when the pages have not been
                 // loaded already (we should load everything asynchronously in that case)
                 throw new RuntimeException("Expecting AllApps and Workspace to be loaded");
@@ -703,6 +673,14 @@
             bindDeepShortcuts();
         }
 
+        private void verifyNotStopped() throws CancellationException {
+            synchronized (LoaderTask.this) {
+                if (mStopped) {
+                    throw new CancellationException("Loader stopped");
+                }
+            }
+        }
+
         public void run() {
             synchronized (mLock) {
                 if (mStopped) {
@@ -710,41 +688,62 @@
                 }
                 mIsLoaderTaskRunning = true;
             }
-            // Optimize for end-user experience: if the Launcher is up and // running with the
-            // All Apps interface in the foreground, load All Apps first. Otherwise, load the
-            // workspace first (default).
-            keep_running: {
-                if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace");
-                loadAndBindWorkspace();
 
-                if (mStopped) {
-                    break keep_running;
-                }
+            try {
+                if (DEBUG_LOADERS) Log.d(TAG, "step 1.1: loading workspace");
+                // Set to false in bindWorkspace()
+                mIsLoadingAndBindingWorkspace = true;
+                loadWorkspace();
 
+                verifyNotStopped();
+                if (DEBUG_LOADERS) Log.d(TAG, "step 1.2: bind workspace workspace");
+                bindWorkspace(mPageToBindFirst);
+
+                // Take a break
+                if (DEBUG_LOADERS) Log.d(TAG, "step 1 completed, wait for idle");
                 waitForIdle();
+                verifyNotStopped();
 
                 // second step
-                if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps");
-                loadAndBindAllApps();
+                if (DEBUG_LOADERS) Log.d(TAG, "step 2.1: loading all apps");
+                loadAllApps();
 
+                verifyNotStopped();
+                if (DEBUG_LOADERS) Log.d(TAG, "step 2.2: Update icon cache");
+                updateIconCache();
+
+                // Take a break
+                if (DEBUG_LOADERS) Log.d(TAG, "step 2 completed, wait for idle");
                 waitForIdle();
+                verifyNotStopped();
 
                 // third step
-                if (DEBUG_LOADERS) Log.d(TAG, "step 3: loading deep shortcuts");
-                loadAndBindDeepShortcuts();
-            }
+                if (DEBUG_LOADERS) Log.d(TAG, "step 3.1: loading deep shortcuts");
+                loadDeepShortcuts();
 
-            // Clear out this reference, otherwise we end up holding it until all of the
-            // callback runnables are done.
-            mContext = null;
+                verifyNotStopped();
+                if (DEBUG_LOADERS) Log.d(TAG, "step 3.2: bind deep shortcuts");
+                bindDeepShortcuts();
 
-            synchronized (mLock) {
-                // If we are still the last one to be scheduled, remove ourselves.
-                if (mLoaderTask == this) {
-                    mLoaderTask = null;
+                synchronized (mLock) {
+                    // Everything loaded bind the data.
+                    mModelLoaded = true;
+                    mHasLoaderCompletedOnce = true;
                 }
-                mIsLoaderTaskRunning = false;
-                mHasLoaderCompletedOnce = true;
+            } catch (CancellationException e) {
+              // Loader stopped, ignore
+            } finally {
+                // Clear out this reference, otherwise we end up holding it until all of the
+                // callback runnables are done.
+                mContext = null;
+
+                synchronized (mLock) {
+                    // If we are still the last one to be scheduled, remove ourselves.
+                    if (mLoaderTask == this) {
+                        mLoaderTask = null;
+                    }
+                    mIsLoaderTaskRunning = false;
+                }
             }
         }
 
@@ -1605,29 +1604,6 @@
             }
         }
 
-        private void loadAndBindAllApps() {
-            if (DEBUG_LOADERS) {
-                Log.d(TAG, "loadAndBindAllApps mAllAppsLoaded=" + mAllAppsLoaded);
-            }
-            if (!mAllAppsLoaded) {
-                loadAllApps();
-                synchronized (LoaderTask.this) {
-                    if (mStopped) {
-                        return;
-                    }
-                }
-                updateIconCache();
-                synchronized (LoaderTask.this) {
-                    if (mStopped) {
-                        return;
-                    }
-                    mAllAppsLoaded = true;
-                }
-            } else {
-                onlyBindAllApps();
-            }
-        }
-
         private void updateIconCache() {
             // Ignore packages which have a promise icon.
             HashSet<String> packagesToIgnore = new HashSet<>();
@@ -1768,11 +1744,8 @@
             }
         }
 
-        private void loadAndBindDeepShortcuts() {
-            if (DEBUG_LOADERS) {
-                Log.d(TAG, "loadAndBindDeepShortcuts mDeepShortcutsLoaded=" + mDeepShortcutsLoaded);
-            }
-            if (!mDeepShortcutsLoaded) {
+        private void loadDeepShortcuts() {
+            if (!mModelLoaded) {
                 sBgDataModel.deepShortcutMap.clear();
                 DeepShortcutManager shortcutManager = DeepShortcutManager.getInstance(mContext);
                 mHasShortcutHostPermission = shortcutManager.hasHostPermission();
@@ -1785,14 +1758,7 @@
                         }
                     }
                 }
-                synchronized (LoaderTask.this) {
-                    if (mStopped) {
-                        return;
-                    }
-                    mDeepShortcutsLoaded = true;
-                }
             }
-            bindDeepShortcuts();
         }
     }
 
@@ -1834,6 +1800,12 @@
     }
 
     void enqueueModelUpdateTask(BaseModelUpdateTask task) {
+        if (!mModelLoaded && mLoaderTask == null) {
+            if (DEBUG_LOADERS) {
+                Log.d(TAG, "enqueueModelUpdateTask Ignoring task since loader is pending=" + task);
+            }
+            return;
+        }
         task.init(this);
         runOnWorkerThread(task);
     }
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index e71ef2c..ad9a13e 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -174,7 +174,7 @@
         if (Utilities.ATLEAST_MARSHMALLOW && Binder.getCallingPid() != Process.myPid()) {
             LauncherAppState app = LauncherAppState.getInstanceNoCreate();
             if (app != null) {
-                app.reloadWorkspace();
+                app.getModel().forceReload();
             }
         }
     }
@@ -205,7 +205,7 @@
             // Deprecated behavior to support legacy devices which rely on provider callbacks.
             LauncherAppState app = LauncherAppState.getInstanceNoCreate();
             if (app != null && "true".equals(uri.getQueryParameter("isExternalAdd"))) {
-                app.reloadWorkspace();
+                app.getModel().forceReload();
             }
 
             String notify = uri.getQueryParameter("notify");
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 24882aa..0a29147 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -23,7 +23,9 @@
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
 
-        <receiver android:name="com.android.launcher3.testcomponent.AppWidgetNoConfig">
+        <receiver
+            android:name="com.android.launcher3.testcomponent.AppWidgetNoConfig"
+            android:label="No Config">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
             </intent-filter>
@@ -31,7 +33,9 @@
                        android:resource="@xml/appwidget_no_config" />
         </receiver>
 
-        <receiver android:name="com.android.launcher3.testcomponent.AppWidgetWithConfig">
+        <receiver
+            android:name="com.android.launcher3.testcomponent.AppWidgetWithConfig"
+            android:label="With Config">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
             </intent-filter>
@@ -45,5 +49,15 @@
                 <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
             </intent-filter>
         </activity>
+        <activity
+            android:name="com.android.launcher3.testcomponent.RequestPinItemActivity"
+            android:label="Test Pin Item"
+            android:icon="@drawable/test_drawable_pin_item">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
diff --git a/tests/res/drawable/test_drawable_pin_item.xml b/tests/res/drawable/test_drawable_pin_item.xml
new file mode 100644
index 0000000..1d07256
--- /dev/null
+++ b/tests/res/drawable/test_drawable_pin_item.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="48.0"
+    android:viewportHeight="48.0">
+
+    <path
+        android:fillColor="#66000000"
+        android:pathData="M0,24a24,24 0 1,0 48,0a24,24 0 1,0 -48,0"/>
+    <path
+        android:fillColor="#FF1B5E20"
+        android:pathData="M2,24a22,22 0 1,0 44,0a22,22 0 1,0 -44,0"/>
+
+    <group
+        android:translateX="12"
+        android:translateY="12">
+        <path
+            android:fillColor="#FFFFFFFF"
+            android:pathData="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z"/>
+    </group>
+</vector>
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java b/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java
new file mode 100644
index 0000000..904590c
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2017 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.testcomponent;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+/**
+ * Base activity with utility methods to help automate testing.
+ */
+public class BaseTestingActivity extends Activity implements View.OnClickListener {
+
+    public static final String SUFFIX_COMMAND = "-command";
+    public static final String EXTRA_METHOD = "method";
+    public static final String EXTRA_PARAM = "param_";
+
+    private static final int MARGIN_DP = 20;
+
+    private final String mAction = this.getClass().getName();
+
+    private LinearLayout mView;
+    private int mMargin;
+
+    private final BroadcastReceiver mCommandReceiver = new BroadcastReceiver() {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            handleCommand(intent);
+        }
+    };
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mMargin = Math.round(TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP, MARGIN_DP, getResources().getDisplayMetrics()));
+        mView = new LinearLayout(this);
+        mView.setPadding(mMargin, mMargin, mMargin, mMargin);
+        mView.setOrientation(LinearLayout.VERTICAL);
+        setContentView(mView);
+
+        registerReceiver(mCommandReceiver, new IntentFilter(mAction + SUFFIX_COMMAND));
+    }
+
+    protected void addButton(String title, String method) {
+        Button button = new Button(this);
+        button.setText(title);
+        button.setTag(method);
+        button.setOnClickListener(this);
+
+        LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+        lp.bottomMargin = mMargin;
+        mView.addView(button, lp);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        sendBroadcast(new Intent(mAction).putExtra(Intent.EXTRA_INTENT, getIntent()));
+    }
+
+    @Override
+    protected void onDestroy() {
+        unregisterReceiver(mCommandReceiver);
+        super.onDestroy();
+    }
+
+    @Override
+    public void onClick(View view) {
+        handleCommand(new Intent().putExtra(EXTRA_METHOD, (String) view.getTag()));
+    }
+
+    private void handleCommand(Intent cmd) {
+        String methodName = cmd.getStringExtra(EXTRA_METHOD);
+        try {
+            Method method = null;
+            for (Method m : this.getClass().getDeclaredMethods()) {
+                if (methodName.equals(m.getName()) &&
+                        !Modifier.isStatic(m.getModifiers()) &&
+                        Modifier.isPublic(m.getModifiers())) {
+                    method = m;
+                    break;
+                }
+            }
+            Object[] args = new Object[method.getParameterTypes().length];
+            Bundle extras = cmd.getExtras();
+            for (int i = 0; i < args.length; i++) {
+                args[i] = extras.get(EXTRA_PARAM + i);
+            }
+            method.invoke(this, args);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static Intent getCommandIntent(Class<?> clazz, String method) {
+        return new Intent(clazz.getName() + SUFFIX_COMMAND)
+                .putExtra(EXTRA_METHOD, method);
+    }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/RequestPinItemActivity.java b/tests/src/com/android/launcher3/testcomponent/RequestPinItemActivity.java
new file mode 100644
index 0000000..c2dd225
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/RequestPinItemActivity.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 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.testcomponent;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.IntentSender;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+
+/**
+ * Sample activity to request pinning an item.
+ */
+@TargetApi(26)
+public class RequestPinItemActivity extends BaseTestingActivity {
+
+    private PendingIntent mCallback = null;
+    private String mShortcutId;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        addButton("Pin Shortcut", "pinShortcut");
+        addButton("Pin Widget without config ", "pinWidgetNoConfig");
+        addButton("Pin Widget with config", "pinWidgetWithConfig");
+    }
+
+    public void setCallback(PendingIntent callback) {
+        mCallback = callback;
+    }
+
+    public void setShortcutId(String id) {
+        mShortcutId = id;
+    }
+
+    public void pinShortcut() {
+        ShortcutManager sm = getSystemService(ShortcutManager.class);
+
+        // Generate icon
+        int r = sm.getIconMaxWidth() / 2;
+        Bitmap icon = Bitmap.createBitmap(r * 2, r * 2, Bitmap.Config.ARGB_8888);
+        Paint p = new Paint();
+        p.setColor(Color.RED);
+        new Canvas(icon).drawCircle(r, r, r, p);
+
+        ShortcutInfo info = new ShortcutInfo.Builder(this, mShortcutId)
+                .setIntent(getPackageManager().getLaunchIntentForPackage(getPackageName()))
+                .setIcon(Icon.createWithBitmap(icon))
+                .setShortLabel("Test shortcut")
+                .build();
+
+        IntentSender callback = mCallback == null ? null : mCallback.getIntentSender();
+        sm.requestPinShortcut(info, callback);
+    }
+
+    public void pinWidgetNoConfig() {
+        requestWidget(new ComponentName(this, AppWidgetNoConfig.class));
+    }
+
+    public void pinWidgetWithConfig() {
+        requestWidget(new ComponentName(this, AppWidgetWithConfig.class));
+    }
+
+    private void requestWidget(ComponentName cn) {
+        AppWidgetManager.getInstance(this).requestPinAppWidget(cn, mCallback);
+    }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/WidgetConfigActivity.java b/tests/src/com/android/launcher3/testcomponent/WidgetConfigActivity.java
index c0509bc..d76ad04 100644
--- a/tests/src/com/android/launcher3/testcomponent/WidgetConfigActivity.java
+++ b/tests/src/com/android/launcher3/testcomponent/WidgetConfigActivity.java
@@ -15,50 +15,30 @@
  */
 package com.android.launcher3.testcomponent;
 
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.os.Bundle;
 
 /**
  * Simple activity for widget configuration
  */
-public class WidgetConfigActivity extends Activity {
+public class WidgetConfigActivity extends BaseTestingActivity {
 
     public static final String SUFFIX_FINISH = "-finish";
     public static final String EXTRA_CODE = "code";
-    public static final String EXTRA_INTENT = "intent";
-
-    private final BroadcastReceiver mFinishReceiver = new BroadcastReceiver() {
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            WidgetConfigActivity.this.setResult(
-                    intent.getIntExtra(EXTRA_CODE, RESULT_CANCELED),
-                    (Intent) intent.getParcelableExtra(EXTRA_INTENT));
-            WidgetConfigActivity.this.finish();
-        }
-    };
-
-    private final String mAction = this.getClass().getName();
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        registerReceiver(mFinishReceiver, new IntentFilter(mAction + SUFFIX_FINISH));
+        addButton("Cancel", "clickCancel");
+        addButton("OK", "clickOK");
     }
 
-    @Override
-    protected void onResume() {
-        super.onResume();
-        sendBroadcast(new Intent(mAction).putExtra(Intent.EXTRA_INTENT, getIntent()));
+    public void clickCancel() {
+        setResult(RESULT_CANCELED);
+        finish();
     }
 
-    @Override
-    protected void onDestroy() {
-        unregisterReceiver(mFinishReceiver);
-        super.onDestroy();
+    public void clickOK() {
+        setResult(RESULT_OK);
+        finish();
     }
 }
diff --git a/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java b/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
index 3f77bfd..9361750 100644
--- a/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
+++ b/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
@@ -22,6 +22,7 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+        setDefaultLauncher();
 
         mSettingsApp = LauncherAppsCompat.getInstance(mTargetContext)
                 .getActivityList("com.android.settings", Process.myUserHandle()).get(0);
diff --git a/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java b/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
index 4bc40c6..47b43f5 100644
--- a/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
+++ b/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
@@ -15,10 +15,14 @@
  */
 package com.android.launcher3.ui;
 
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.graphics.Point;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
@@ -46,16 +50,24 @@
 import com.android.launcher3.testcomponent.AppWidgetWithConfig;
 import com.android.launcher3.util.ManagedProfileHeuristic;
 
+import java.io.BufferedReader;
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
 import java.util.Locale;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Base class for all instrumentation tests providing various utility methods.
  */
 public class LauncherInstrumentationTestCase extends InstrumentationTestCase {
 
+    public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
+    public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5;
+
     public static final long DEFAULT_UI_TIMEOUT = 3000;
     public static final long DEFAULT_WORKER_TIMEOUT_SECS = 5;
 
@@ -89,11 +101,14 @@
      * Starts the launcher activity in the target package and returns the Launcher instance.
      */
     protected Launcher startLauncher() {
-        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
+        return (Launcher) getInstrumentation().startActivitySync(getHomeIntent());
+    }
+
+    protected Intent getHomeIntent() {
+        return new Intent(Intent.ACTION_MAIN)
                 .addCategory(Intent.CATEGORY_HOME)
                 .setPackage(mTargetPackage)
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        return (Launcher) getInstrumentation().startActivitySync(homeIntent);
     }
 
     /**
@@ -104,16 +119,31 @@
         if (mTargetContext.getPackageManager().checkPermission(
                 mTargetPackage, android.Manifest.permission.BIND_APPWIDGET)
                 != PackageManager.PERMISSION_GRANTED) {
-            ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation().executeShellCommand(
-                    "appwidget grantbind --package " + mTargetPackage);
-            // Read the input stream fully.
-            FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
-            while (fis.read() != -1);
-            fis.close();
+            runShellCommand("appwidget grantbind --package " + mTargetPackage);
         }
     }
 
     /**
+     * Sets the target launcher as default launcher.
+     */
+    protected void setDefaultLauncher() throws IOException {
+        ActivityInfo launcher = mTargetContext.getPackageManager()
+                .queryIntentActivities(getHomeIntent(), 0).get(0).activityInfo;
+        runShellCommand("cmd package set-home-activity " +
+                new ComponentName(launcher.packageName, launcher.name).flattenToString());
+    }
+
+    protected void runShellCommand(String command) throws IOException {
+        ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation()
+                .executeShellCommand(command);
+
+        // Read the input stream fully.
+        FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
+        while (fis.read() != -1);
+        fis.close();
+    }
+
+    /**
      * Opens all apps and returns the recycler view
      */
     protected UiObject2 openAllApps() {
@@ -236,8 +266,7 @@
                 @Override
                 public void run() {
                     ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mTargetContext);
-                    LauncherAppState.getInstance(mTargetContext).getModel()
-                            .resetLoadedState(true, true);
+                    LauncherAppState.getInstance(mTargetContext).getModel().forceReload();
                 }
             });
         } catch (Throwable t) {
@@ -285,4 +314,35 @@
         String name = mTargetContext.getResources().getResourceEntryName(id);
         return By.res(mTargetPackage, name);
     }
+
+
+    /**
+     * Broadcast receiver which blocks until the result is received.
+     */
+    public class BlockingBroadcastReceiver extends BroadcastReceiver {
+
+        private final CountDownLatch latch = new CountDownLatch(1);
+        private Intent mIntent;
+
+        public BlockingBroadcastReceiver(String action) {
+            mTargetContext.registerReceiver(this, new IntentFilter(action));
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mIntent = intent;
+            latch.countDown();
+        }
+
+        public Intent blockingGetIntent() throws InterruptedException {
+            latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS);
+            mTargetContext.unregisterReceiver(this);
+            return mIntent;
+        }
+
+        public Intent blockingGetExtraIntent() throws InterruptedException {
+            Intent intent = blockingGetIntent();
+            return intent == null ? null : (Intent) intent.getParcelableExtra(Intent.EXTRA_INTENT);
+        }
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java b/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
index c6828f0..ee3a628 100644
--- a/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
+++ b/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
@@ -25,6 +25,7 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+        setDefaultLauncher();
 
         mSettingsApp = LauncherAppsCompat.getInstance(mTargetContext)
                 .getActivityList("com.android.settings", Process.myUserHandle()).get(0);
diff --git a/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java b/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java
index d573eea..061e865 100644
--- a/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java
+++ b/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java
@@ -25,6 +25,7 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+        setDefaultLauncher();
 
         mSettingsApp = LauncherAppsCompat.getInstance(mTargetContext)
                 .getActivityList("com.android.settings", Process.myUserHandle()).get(0);
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 7cbd292..0b4e34f 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -18,10 +18,7 @@
 import android.app.Activity;
 import android.app.Application;
 import android.appwidget.AppWidgetManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiObject2;
 import android.test.suitebuilder.annotation.LargeTest;
@@ -41,8 +38,6 @@
 import com.android.launcher3.widget.WidgetCell;
 
 import java.util.concurrent.Callable;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 
 /**
  * Test to verify widget configuration is properly shown.
@@ -50,9 +45,6 @@
 @LargeTest
 public class AddConfigWidgetTest extends LauncherInstrumentationTestCase {
 
-    public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
-    public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5;
-
     private LauncherAppWidgetProviderInfo mWidgetInfo;
     private SimpleActivityMonitor mActivityMonitor;
     private MainThreadExecutor mMainThreadExecutor;
@@ -69,6 +61,8 @@
                 .registerActivityLifecycleCallbacks(mActivityMonitor);
         mMainThreadExecutor = new MainThreadExecutor();
         mAppWidgetManager = AppWidgetManager.getInstance(mTargetContext);
+
+        grantWidgetPermission();
     }
 
     @Override
@@ -126,12 +120,11 @@
         // Verify that the widget id is valid and bound
         assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
 
+        setResult(acceptConfig);
         if (acceptConfig) {
-            setResult(Activity.RESULT_OK);
             assertTrue(Wait.atMost(new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT));
             assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
         } else {
-            setResult(Activity.RESULT_CANCELED);
             // Verify that the widget id is deleted.
             assertTrue(Wait.atMost(new Condition() {
                 @Override
@@ -142,10 +135,11 @@
         }
     }
 
-    private void setResult(int resultCode) {
-        String action = WidgetConfigActivity.class.getName() + WidgetConfigActivity.SUFFIX_FINISH;
+    private void setResult(boolean success) {
+
         getInstrumentation().getTargetContext().sendBroadcast(
-                new Intent(action).putExtra(WidgetConfigActivity.EXTRA_CODE, resultCode));
+                WidgetConfigActivity.getCommandIntent(WidgetConfigActivity.class,
+                        success ? "clickOK" : "clickCancel"));
     }
 
     /**
@@ -185,28 +179,17 @@
     /**
      * Broadcast receiver for receiving widget config activity status.
      */
-    private class WidgetConfigStartupMonitor extends BroadcastReceiver {
+    private class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver {
 
-        private final CountDownLatch latch = new CountDownLatch(1);
-        private Intent mIntent;
-
-        WidgetConfigStartupMonitor() {
-            getInstrumentation().getTargetContext().registerReceiver(this,
-                    new IntentFilter(WidgetConfigActivity.class.getName()));
-        }
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            mIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT);
-            latch.countDown();
+        public WidgetConfigStartupMonitor() {
+            super(WidgetConfigActivity.class.getName());
         }
 
         public int getWidgetId() throws InterruptedException {
-            latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS);
-            getInstrumentation().getTargetContext().unregisterReceiver(this);
-            assertNotNull(mIntent);
-            assertEquals(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, mIntent.getAction());
-            int widgetId = mIntent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
+            Intent intent = blockingGetExtraIntent();
+            assertNotNull(intent);
+            assertEquals(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, intent.getAction());
+            int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                     LauncherAppWidgetInfo.NO_ID);
             assertNotSame(widgetId, LauncherAppWidgetInfo.NO_ID);
             return widgetId;
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index b7e1ca9..3c92c57 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -41,6 +41,7 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+        grantWidgetPermission();
 
         widgetInfo = findWidgetProvider(false /* hasConfigureScreen */);
     }
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
new file mode 100644
index 0000000..5ef5ec1
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2017 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.ui.widget;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.Intent;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.View;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppWidgetInfo;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.R;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace.ItemOperator;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.testcomponent.AppWidgetNoConfig;
+import com.android.launcher3.testcomponent.AppWidgetWithConfig;
+import com.android.launcher3.testcomponent.RequestPinItemActivity;
+import com.android.launcher3.ui.LauncherInstrumentationTestCase;
+import com.android.launcher3.util.Condition;
+import com.android.launcher3.util.SimpleActivityMonitor;
+import com.android.launcher3.util.Wait;
+import com.android.launcher3.widget.WidgetCell;
+
+import java.util.UUID;
+import java.util.concurrent.Callable;
+
+/**
+ * Test to verify pin item request flow.
+ */
+@LargeTest
+public class RequestPinItemTest  extends LauncherInstrumentationTestCase {
+
+    private SimpleActivityMonitor mActivityMonitor;
+    private MainThreadExecutor mMainThreadExecutor;
+
+    private String mCallbackAction;
+    private String mShortcutId;
+    private int mAppWidgetId;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        grantWidgetPermission();
+        setDefaultLauncher();
+
+        mActivityMonitor = new SimpleActivityMonitor();
+        ((Application) getInstrumentation().getTargetContext().getApplicationContext())
+                .registerActivityLifecycleCallbacks(mActivityMonitor);
+        mMainThreadExecutor = new MainThreadExecutor();
+
+        mCallbackAction = UUID.randomUUID().toString();
+        mShortcutId = UUID.randomUUID().toString();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ((Application) getInstrumentation().getTargetContext().getApplicationContext())
+                .unregisterActivityLifecycleCallbacks(mActivityMonitor);
+        super.tearDown();
+    }
+
+    public void testPinWidgetNoConfig() throws Throwable {
+        runTest("pinWidgetNoConfig", true, new ItemOperator() {
+            @Override
+            public boolean evaluate(ItemInfo info, View view) {
+                return info instanceof LauncherAppWidgetInfo &&
+                        ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
+                        ((LauncherAppWidgetInfo) info).providerName.getClassName()
+                                .equals(AppWidgetNoConfig.class.getName());
+            }
+        });
+    }
+
+    public void testPinWidgetWithConfig() throws Throwable {
+        runTest("pinWidgetWithConfig", true, new ItemOperator() {
+            @Override
+            public boolean evaluate(ItemInfo info, View view) {
+                return info instanceof LauncherAppWidgetInfo &&
+                        ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
+                        ((LauncherAppWidgetInfo) info).providerName.getClassName()
+                                .equals(AppWidgetWithConfig.class.getName());
+            }
+        });
+    }
+
+
+    public void testPinWidgetShortcut() throws Throwable {
+        runTest("pinShortcut", false, new ItemOperator() {
+            @Override
+            public boolean evaluate(ItemInfo info, View view) {
+                return info instanceof ShortcutInfo &&
+                        info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT &&
+                        ShortcutKey.fromItemInfo(info).getId().equals(mShortcutId);
+            }
+        });
+    }
+
+    private void runTest(String activityMethod, boolean isWidget, ItemOperator itemMatcher)
+            throws Throwable {
+        if (!Utilities.isAtLeastO()) {
+            return;
+        }
+        lockRotation(true);
+
+        clearHomescreen();
+        startLauncher();
+
+        // Open all apps and wait for load complete
+        final UiObject2 appsContainer = openAllApps();
+        assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT));
+
+        // Open Pin item activity
+        BlockingBroadcastReceiver openMonitor = new BlockingBroadcastReceiver(
+                RequestPinItemActivity.class.getName());
+        scrollAndFind(appsContainer, By.text("Test Pin Item")).click();
+        assertNotNull(openMonitor.blockingGetExtraIntent());
+
+        // Set callback
+        PendingIntent callback = PendingIntent.getBroadcast(mTargetContext, 0,
+                new Intent(mCallbackAction), PendingIntent.FLAG_ONE_SHOT);
+        mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
+                RequestPinItemActivity.class, "setCallback").putExtra(
+                RequestPinItemActivity.EXTRA_PARAM + "0", callback));
+
+        if (!isWidget) {
+            // Set shortcut id
+            mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
+                    RequestPinItemActivity.class, "setShortcutId").putExtra(
+                    RequestPinItemActivity.EXTRA_PARAM + "0", mShortcutId));
+        }
+
+        // call the requested method to start the flow
+        mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
+                RequestPinItemActivity.class, activityMethod));
+        UiObject2 widgetCell = mDevice.wait(
+                Until.findObject(By.clazz(WidgetCell.class)), DEFAULT_ACTIVITY_TIMEOUT);
+        assertNotNull(widgetCell);
+
+        // Accept confirmation:
+        BlockingBroadcastReceiver resultReceiver = new BlockingBroadcastReceiver(mCallbackAction);
+        mDevice.wait(Until.findObject(By.text(mTargetContext.getString(
+                R.string.place_automatically).toUpperCase())), DEFAULT_UI_TIMEOUT).click();
+        Intent result = resultReceiver.blockingGetIntent();
+        assertNotNull(result);
+        mAppWidgetId = result.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
+        if (isWidget) {
+            assertNotSame(-1, mAppWidgetId);
+        }
+
+        // Go back to home
+        mTargetContext.startActivity(getHomeIntent());
+        assertTrue(Wait.atMost(new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT));
+    }
+
+    /**
+     * Condition for for an item
+     */
+    private class ItemSearchCondition extends Condition implements Callable<Boolean> {
+
+        private final ItemOperator mOp;
+
+        ItemSearchCondition(ItemOperator op) {
+            mOp = op;
+        }
+
+        @Override
+        public boolean isTrue() throws Throwable {
+            return mMainThreadExecutor.submit(this).get();
+        }
+
+        @Override
+        public Boolean call() throws Exception {
+            // Find the resumed launcher
+            Launcher launcher = null;
+            for (Activity a : mActivityMonitor.resumed) {
+                if (a instanceof Launcher) {
+                    launcher = (Launcher) a;
+                }
+            }
+            if (launcher == null) {
+                return false;
+            }
+            return launcher.getWorkspace().getFirstMatch(mOp) != null;
+        }
+    }
+}