merged ub-launcher3-dorval, and resolved conflicts
Bug: 36904684
Bug: 37929893
Bug: 36068989

Test: make -j 32 dist checkbuild
Change-Id: If9b11b212852cb1048d54db2224dab4acf2d93e0
diff --git a/tests/Android.mk b/tests/Android.mk
index c91e793..6e6453c 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -20,11 +20,14 @@
 LOCAL_STATIC_JAVA_LIBRARIES := \
 	android-support-test \
 	ub-uiautomator \
-	legacy-android-test
+	legacy-android-test \
+	mockito-target-minus-junit4
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_FULL_LIBS_MANIFEST_FILES := $(LOCAL_PATH)/AndroidManifest-common.xml
 
 LOCAL_SDK_VERSION := current
+LOCAL_MIN_SDK_VERSION := 21
 
 LOCAL_PACKAGE_NAME := Launcher3Tests
 
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
new file mode 100644
index 0000000..0a29147
--- /dev/null
+++ b/tests/AndroidManifest-common.xml
@@ -0,0 +1,63 @@
+<?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.
+-->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.launcher3.tests">
+
+    <uses-sdk android:targetSdkVersion="25" android:minSdkVersion="21"/>
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+
+        <receiver
+            android:name="com.android.launcher3.testcomponent.AppWidgetNoConfig"
+            android:label="No Config">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+            </intent-filter>
+            <meta-data android:name="android.appwidget.provider"
+                       android:resource="@xml/appwidget_no_config" />
+        </receiver>
+
+        <receiver
+            android:name="com.android.launcher3.testcomponent.AppWidgetWithConfig"
+            android:label="With Config">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+            </intent-filter>
+            <meta-data android:name="android.appwidget.provider"
+                       android:resource="@xml/appwidget_with_config" />
+        </receiver>
+
+        <activity
+            android:name="com.android.launcher3.testcomponent.WidgetConfigActivity">
+            <intent-filter>
+                <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/AndroidManifest.xml b/tests/AndroidManifest.xml
index afe8952..43030ae 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -18,6 +18,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="com.android.launcher3.tests">
 
+    <uses-sdk android:targetSdkVersion="25" android:minSdkVersion="21"/>
     <uses-sdk tools:overrideLibrary="android.support.test.uiautomator.v18"/>
 
     <application android:debuggable="true">
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/res/layout/test_layout_appwidget_blue.xml b/tests/res/layout/test_layout_appwidget_blue.xml
new file mode 100644
index 0000000..8111978
--- /dev/null
+++ b/tests/res/layout/test_layout_appwidget_blue.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:background="#FF0000FF"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
\ No newline at end of file
diff --git a/tests/res/layout/test_layout_appwidget_red.xml b/tests/res/layout/test_layout_appwidget_red.xml
new file mode 100644
index 0000000..48d3e81
--- /dev/null
+++ b/tests/res/layout/test_layout_appwidget_red.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:background="#FFFF0000"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
\ No newline at end of file
diff --git a/tests/res/layout/test_layout_appwidget_view.xml b/tests/res/layout/test_layout_appwidget_view.xml
new file mode 100644
index 0000000..7c87e60
--- /dev/null
+++ b/tests/res/layout/test_layout_appwidget_view.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#FFFF00">
+
+    <ImageView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_margin="20dp"
+        android:background="#FF0000"
+        android:id="@android:id/icon" />
+</FrameLayout>
\ No newline at end of file
diff --git a/tests/res/raw/cache_data_updated_task_data.txt b/tests/res/raw/cache_data_updated_task_data.txt
new file mode 100644
index 0000000..8199687
--- /dev/null
+++ b/tests/res/raw/cache_data_updated_task_data.txt
@@ -0,0 +1,28 @@
+# Model data used by CacheDataUpdatedTaskTest
+
+classMap s com.android.launcher3.ShortcutInfo
+
+# Items for the BgDataModel
+
+# App shortcuts
+bgItem s itemType=0 title=app1-class1 intent=component=app1/class1 id=1
+bgItem s itemType=0 title=app1-class2 intent=component=app1/class2 id=2
+bgItem s itemType=0 title=app2-class1 intent=component=app2/class1 id=3
+bgItem s itemType=0 title=app2-class2 intent=component=app2/class2 id=4
+
+# Auto install app shortcut
+bgItem s itemType=0 status=2 title=app3-class1 intent=component=app3/class1 id=5
+bgItem s itemType=0 status=2 title=app3-class2 intent=component=app3/class2 id=6
+
+# Custom shortcuts
+bgItem s itemType=1 title=app1-shrt intent=component=app1/class3 id=7
+bgItem s itemType=1 title=app4-shrt intent=component=app4/class1 id=8
+
+# Restored custom shortcut
+bgItem s itemType=1 status=1 title=app3-shrt intent=component=app3/class3 id=9
+bgItem s itemType=1 status=1 title=app5-shrt intent=component=app5/class1 id=10
+
+allApps componentName=app1/class1 intent=component=app1/class1
+allApps componentName=app1/class2 intent=component=app1/class2
+allApps componentName=app2/class1 intent=component=app2/class1
+allApps componentName=app2/class2 intent=component=app2/class2
\ No newline at end of file
diff --git a/tests/res/raw/package_install_state_change_task_data.txt b/tests/res/raw/package_install_state_change_task_data.txt
new file mode 100644
index 0000000..84f9c16
--- /dev/null
+++ b/tests/res/raw/package_install_state_change_task_data.txt
@@ -0,0 +1,24 @@
+# Model data used by PackageInstallStateChangeTaskTest
+
+classMap s com.android.launcher3.ShortcutInfo
+classMap w com.android.launcher3.LauncherAppWidgetInfo
+
+# Items for the BgDataModel
+
+# App shortcuts
+bgItem s itemType=0 title=app1-class1 intent=component=app1/class1 id=1
+bgItem s itemType=0 title=app1-class2 intent=component=app1/class2 id=2
+bgItem s itemType=0 title=app2-class1 intent=component=app2/class1 id=3
+bgItem s itemType=0 title=app2-class2 intent=component=app2/class2 id=4
+
+# Promise icons for app3
+bgItem s itemType=0 status=2 title=app3-class1 intent=component=app3/class1 id=5
+bgItem s itemType=0 status=2 title=app3-class2 intent=component=app3/class2 id=6
+bgItem s itemType=1 status=1 title=app3-shrt intent=component=app3/class3 id=7
+
+# Promise icon for app4
+bgItem s itemType=1 status=1 title=app4-shrt intent=component=app4/class1 id=8
+
+# Widget
+bgItem w providerName=app4/provider1 id=9
+bgItem w providerName=app5/provider1 id=10
\ No newline at end of file
diff --git a/tests/res/xml/appwidget_no_config.xml b/tests/res/xml/appwidget_no_config.xml
new file mode 100644
index 0000000..d24dfe3
--- /dev/null
+++ b/tests/res/xml/appwidget_no_config.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:minWidth="180dp"
+    android:minHeight="110dp"
+    android:updatePeriodMillis="86400000"
+    android:initialLayout="@layout/test_layout_appwidget_red"
+    android:resizeMode="horizontal|vertical"
+    android:widgetCategory="home_screen">
+</appwidget-provider>
\ No newline at end of file
diff --git a/tests/res/xml/appwidget_with_config.xml b/tests/res/xml/appwidget_with_config.xml
new file mode 100644
index 0000000..3e96c6f
--- /dev/null
+++ b/tests/res/xml/appwidget_with_config.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:minWidth="180dp"
+    android:minHeight="110dp"
+    android:updatePeriodMillis="86400000"
+    android:initialLayout="@layout/test_layout_appwidget_blue"
+    android:configure="com.android.launcher3.testcomponent.WidgetConfigActivity"
+    android:resizeMode="horizontal|vertical"
+    android:widgetCategory="home_screen">
+</appwidget-provider>
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
new file mode 100644
index 0000000..d0ba907
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
@@ -0,0 +1,183 @@
+package com.android.launcher3.model;
+
+import android.content.ComponentName;
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.util.Pair;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.config.ProviderConfig;
+import com.android.launcher3.util.GridOccupancy;
+import com.android.launcher3.util.LongArrayMap;
+import com.android.launcher3.util.Provider;
+
+import org.mockito.ArgumentCaptor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link AddWorkspaceItemsTask}
+ */
+public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase {
+
+    private final ComponentName mComponent1 = new ComponentName("a", "b");
+    private final ComponentName mComponent2 = new ComponentName("b", "b");
+
+    private ArrayList<Long> existingScreens;
+    private ArrayList<Long> newScreens;
+    private LongArrayMap<GridOccupancy> screenOccupancy;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        existingScreens = new ArrayList<>();
+        screenOccupancy = new LongArrayMap<>();
+        newScreens = new ArrayList<>();
+
+        idp.numColumns = 5;
+        idp.numRows = 5;
+    }
+
+    private AddWorkspaceItemsTask newTask(ItemInfo... items) {
+        return new AddWorkspaceItemsTask(Provider.of(Arrays.asList(items))) {
+
+            @Override
+            protected void updateScreens(Context context, ArrayList<Long> workspaceScreens) { }
+        };
+    }
+
+    public void testFindSpaceForItem_prefers_second() {
+        // First screen has only one hole of size 1
+        int nextId = setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
+
+        // Second screen has 2 holes of sizes 3x2 and 2x3
+        setupWorkspaceWithHoles(nextId, 2, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
+
+        Pair<Long, int[]> spaceFound = newTask()
+                .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 1, 1);
+        assertEquals(2L, (long) spaceFound.first);
+        assertTrue(screenOccupancy.get(spaceFound.first)
+                .isRegionVacant(spaceFound.second[0], spaceFound.second[1], 1, 1));
+
+        // Find a larger space
+        spaceFound = newTask()
+                .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 2, 3);
+        assertEquals(2L, (long) spaceFound.first);
+        assertTrue(screenOccupancy.get(spaceFound.first)
+                .isRegionVacant(spaceFound.second[0], spaceFound.second[1], 2, 3));
+    }
+
+    public void testFindSpaceForItem_adds_new_screen() throws Exception {
+        // First screen has 2 holes of sizes 3x2 and 2x3
+        setupWorkspaceWithHoles(1, 1, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
+        commitScreensToDb();
+
+        when(appState.getContext()).thenReturn(getMockContext());
+
+        ArrayList<Long> oldScreens = new ArrayList<>(existingScreens);
+        Pair<Long, int[]> spaceFound = newTask()
+                .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 3, 3);
+        assertFalse(oldScreens.contains(spaceFound.first));
+        assertTrue(newScreens.contains(spaceFound.first));
+    }
+
+    public void testAddItem_existing_item_ignored() throws Exception {
+        ShortcutInfo info = new ShortcutInfo();
+        info.intent = new Intent().setComponent(mComponent1);
+
+        // Setup a screen with a hole
+        setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
+        commitScreensToDb();
+
+        when(appState.getContext()).thenReturn(getMockContext());
+
+        // Nothing was added
+        assertTrue(executeTaskForTest(newTask(info)).isEmpty());
+    }
+
+    public void testAddItem_some_items_added() throws Exception {
+        ShortcutInfo info = new ShortcutInfo();
+        info.intent = new Intent().setComponent(mComponent1);
+
+        ShortcutInfo info2 = new ShortcutInfo();
+        info2.intent = new Intent().setComponent(mComponent2);
+
+        // Setup a screen with a hole
+        setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
+        commitScreensToDb();
+
+        when(appState.getContext()).thenReturn(getMockContext());
+
+        executeTaskForTest(newTask(info, info2)).get(0).run();
+        ArgumentCaptor<ArrayList> notAnimated = ArgumentCaptor.forClass(ArrayList.class);
+        ArgumentCaptor<ArrayList> animated = ArgumentCaptor.forClass(ArrayList.class);
+
+        // only info2 should be added because info was already added to the workspace
+        // in setupWorkspaceWithHoles()
+        verify(callbacks).bindAppsAdded(any(ArrayList.class), notAnimated.capture(),
+                animated.capture(), any(ArrayList.class));
+        assertTrue(notAnimated.getValue().isEmpty());
+
+        assertEquals(1, animated.getValue().size());
+        assertTrue(animated.getValue().contains(info2));
+    }
+
+    private int setupWorkspaceWithHoles(int startId, long screenId, Rect... holes) {
+        GridOccupancy occupancy = new GridOccupancy(idp.numColumns, idp.numRows);
+        occupancy.markCells(0, 0, idp.numColumns, idp.numRows, true);
+        for (Rect r : holes) {
+            occupancy.markCells(r, false);
+        }
+
+        existingScreens.add(screenId);
+        screenOccupancy.append(screenId, occupancy);
+
+        for (int x = 0; x < idp.numColumns; x++) {
+            for (int y = 0; y < idp.numRows; y++) {
+                if (!occupancy.cells[x][y]) {
+                    continue;
+                }
+
+                ShortcutInfo info = new ShortcutInfo();
+                info.intent = new Intent().setComponent(mComponent1);
+                info.id = startId++;
+                info.screenId = screenId;
+                info.cellX = x;
+                info.cellY = y;
+                info.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+                bgDataModel.addItem(targetContext, info, false);
+            }
+        }
+        return startId;
+    }
+
+    private void commitScreensToDb() throws Exception {
+        LauncherSettings.Settings.call(getMockContentResolver(),
+                LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
+
+        Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI;
+        ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+        // Clear the table
+        ops.add(ContentProviderOperation.newDelete(uri).build());
+        int count = existingScreens.size();
+        for (int i = 0; i < count; i++) {
+            ContentValues v = new ContentValues();
+            long screenId = existingScreens.get(i);
+            v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
+            v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
+            ops.add(ContentProviderOperation.newInsert(uri).withValues(v).build());
+        }
+        getMockContentResolver().applyBatch(ProviderConfig.AUTHORITY, ops);
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java b/tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
new file mode 100644
index 0000000..b9944db
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
@@ -0,0 +1,226 @@
+package com.android.launcher3.model;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherActivityInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.os.Process;
+import android.os.UserHandle;
+import android.support.annotation.NonNull;
+import android.support.test.InstrumentationRegistry;
+import android.test.ProviderTestCase2;
+
+import com.android.launcher3.AllAppsList;
+import com.android.launcher3.AppFilter;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.DeferredHandler;
+import com.android.launcher3.IconCache;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherModel.BaseModelUpdateTask;
+import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.config.ProviderConfig;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.Provider;
+import com.android.launcher3.util.TestLauncherProvider;
+
+import org.mockito.ArgumentCaptor;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.List;
+
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Base class for writing tests for Model update tasks.
+ */
+public class BaseModelUpdateTaskTestCase extends ProviderTestCase2<TestLauncherProvider> {
+
+    public final HashMap<Class, HashMap<String, Field>> fieldCache = new HashMap<>();
+
+    public Context targetContext;
+    public UserHandle myUser;
+
+    public InvariantDeviceProfile idp;
+    public LauncherAppState appState;
+    public LauncherModel model;
+    public ModelWriter modelWriter;
+    public MyIconCache iconCache;
+
+    public BgDataModel bgDataModel;
+    public AllAppsList allAppsList;
+    public Callbacks callbacks;
+
+    public BaseModelUpdateTaskTestCase() {
+        super(TestLauncherProvider.class, ProviderConfig.AUTHORITY);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        callbacks = mock(Callbacks.class);
+        appState = mock(LauncherAppState.class);
+        model = mock(LauncherModel.class);
+        modelWriter = mock(ModelWriter.class);
+        when(appState.getModel()).thenReturn(model);
+        when(model.getWriter(anyBoolean())).thenReturn(modelWriter);
+
+        myUser = Process.myUserHandle();
+
+        bgDataModel = new BgDataModel();
+        targetContext = InstrumentationRegistry.getTargetContext();
+        idp = new InvariantDeviceProfile();
+        iconCache = new MyIconCache(targetContext, idp);
+
+        allAppsList = new AllAppsList(iconCache, new AppFilter());
+
+        when(appState.getIconCache()).thenReturn(iconCache);
+        when(appState.getInvariantDeviceProfile()).thenReturn(idp);
+    }
+
+    /**
+     * Synchronously executes the task and returns all the UI callbacks posted.
+     */
+    public List<Runnable> executeTaskForTest(BaseModelUpdateTask task) throws Exception {
+        LauncherModel mockModel = mock(LauncherModel.class);
+        when(mockModel.getCallback()).thenReturn(callbacks);
+
+        Field f = BaseModelUpdateTask.class.getDeclaredField("mModel");
+        f.setAccessible(true);
+        f.set(task, mockModel);
+
+        DeferredHandler mockHandler = mock(DeferredHandler.class);
+        f = BaseModelUpdateTask.class.getDeclaredField("mUiHandler");
+        f.setAccessible(true);
+        f.set(task, mockHandler);
+
+        task.execute(appState, bgDataModel, allAppsList);
+        ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mockHandler, atLeast(0)).post(captor.capture());
+
+        return captor.getAllValues();
+    }
+
+    /**
+     * Initializes mock data for the test.
+     */
+    public void initializeData(String resourceName) throws Exception {
+        Context myContext = InstrumentationRegistry.getContext();
+        Resources res = myContext.getResources();
+        int id = res.getIdentifier(resourceName, "raw", myContext.getPackageName());
+        try (BufferedReader reader =
+                     new BufferedReader(new InputStreamReader(res.openRawResource(id)))) {
+            String line;
+            HashMap<String, Class> classMap = new HashMap<>();
+            while((line = reader.readLine()) != null) {
+                line = line.trim();
+                if (line.startsWith("#") || line.isEmpty()) {
+                    continue;
+                }
+                String[] commands = line.split(" ");
+                switch (commands[0]) {
+                    case "classMap":
+                        classMap.put(commands[1], Class.forName(commands[2]));
+                        break;
+                    case "bgItem":
+                        bgDataModel.addItem(targetContext,
+                                (ItemInfo) initItem(classMap.get(commands[1]), commands, 2), false);
+                        break;
+                    case "allApps":
+                        allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1), null);
+                        break;
+                }
+            }
+        }
+    }
+
+    private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception {
+        HashMap<String, Field> cache = fieldCache.get(clazz);
+        if (cache == null) {
+            cache = new HashMap<>();
+            Class c = clazz;
+            while (c != null) {
+                for (Field f : c.getDeclaredFields()) {
+                    f.setAccessible(true);
+                    cache.put(f.getName(), f);
+                }
+                c = c.getSuperclass();
+            }
+            fieldCache.put(clazz, cache);
+        }
+
+        Object item = clazz.newInstance();
+        for (int i = startIndex; i < fieldDef.length; i++) {
+            String[] fieldData = fieldDef[i].split("=", 2);
+            Field f = cache.get(fieldData[0]);
+            Class type = f.getType();
+            if (type == int.class || type == long.class) {
+                f.set(item, Integer.parseInt(fieldData[1]));
+            } else if (type == CharSequence.class || type == String.class) {
+                f.set(item, fieldData[1]);
+            } else if (type == Intent.class) {
+                if (!fieldData[1].startsWith("#Intent")) {
+                    fieldData[1] = "#Intent;" + fieldData[1] + ";end";
+                }
+                f.set(item, Intent.parseUri(fieldData[1], 0));
+            } else if (type == ComponentName.class) {
+                f.set(item, ComponentName.unflattenFromString(fieldData[1]));
+            } else {
+                throw new Exception("Added parsing logic for "
+                        + f.getName() + " of type " + f.getType());
+            }
+        }
+        return item;
+    }
+
+    public static class MyIconCache extends IconCache {
+
+        private final HashMap<ComponentKey, CacheEntry> mCache = new HashMap<>();
+
+        public MyIconCache(Context context, InvariantDeviceProfile idp) {
+            super(context, idp);
+        }
+
+        @Override
+        protected CacheEntry cacheLocked(
+                @NonNull ComponentName componentName,
+                @NonNull Provider<LauncherActivityInfo> infoProvider,
+                UserHandle user, boolean usePackageIcon, boolean useLowResIcon) {
+            CacheEntry entry = mCache.get(new ComponentKey(componentName, user));
+            if (entry == null) {
+                entry = new CacheEntry();
+                entry.icon = getDefaultIcon(user);
+            }
+            return entry;
+        }
+
+        public void addCache(ComponentName key, String title) {
+            CacheEntry entry = new CacheEntry();
+            entry.icon = newIcon();
+            entry.title = title;
+            mCache.put(new ComponentKey(key, Process.myUserHandle()), entry);
+        }
+
+        public Bitmap newIcon() {
+            return Bitmap.createBitmap(1, 1, Config.ARGB_8888);
+        }
+
+        @Override
+        protected Bitmap makeDefaultIcon(UserHandle user) {
+            return newIcon();
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
new file mode 100644
index 0000000..d595e6c
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -0,0 +1,82 @@
+package com.android.launcher3.model;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.ShortcutInfo;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+/**
+ * Tests for {@link CacheDataUpdatedTask}
+ */
+public class CacheDataUpdatedTaskTest extends BaseModelUpdateTaskTestCase {
+
+    private static final String NEW_LABEL_PREFIX = "new-label-";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        initializeData("cache_data_updated_task_data");
+        // Add dummy entries in the cache to simulate update
+        for (ItemInfo info : bgDataModel.itemsIdMap) {
+            iconCache.addCache(info.getTargetComponent(), NEW_LABEL_PREFIX + info.id);
+        }
+    }
+
+    private CacheDataUpdatedTask newTask(int op, String... pkg) {
+        return new CacheDataUpdatedTask(op, myUser, new HashSet<>(Arrays.asList(pkg)));
+    }
+
+    public void testCacheUpdate_update_apps() throws Exception {
+        // Clear all icons from apps list so that its easy to check what was updated
+        for (AppInfo info : allAppsList.data) {
+            info.iconBitmap = null;
+        }
+
+        executeTaskForTest(newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, "app1"));
+
+        // Verify that only the app icons of app1 (id 1 & 2) are updated. Custom shortcut (id 7)
+        // is not updated
+        verifyUpdate(1L, 2L);
+
+        // Verify that only app1 var updated in allAppsList
+        assertFalse(allAppsList.data.isEmpty());
+        for (AppInfo info : allAppsList.data) {
+            if (info.componentName.getPackageName().equals("app1")) {
+                assertNotNull(info.iconBitmap);
+            } else {
+                assertNull(info.iconBitmap);
+            }
+        }
+    }
+
+    public void testSessionUpdate_ignores_normal_apps() throws Exception {
+        executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app1"));
+
+        // app1 has no restored shortcuts. Verify that nothing was updated.
+        verifyUpdate();
+    }
+
+    public void testSessionUpdate_updates_pending_apps() throws Exception {
+        executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app3"));
+
+        // app3 has only restored apps (id 5, 6) and shortcuts (id 9). Verify that only apps were
+        // were updated
+        verifyUpdate(5L, 6L);
+    }
+
+    private void verifyUpdate(Long... idsUpdated) {
+        HashSet<Long> updates = new HashSet<>(Arrays.asList(idsUpdated));
+        for (ItemInfo info : bgDataModel.itemsIdMap) {
+            if (updates.contains(info.id)) {
+                assertEquals(NEW_LABEL_PREFIX + info.id, info.title);
+                assertNotNull(((ShortcutInfo) info).iconBitmap);
+            } else {
+                assertNotSame(NEW_LABEL_PREFIX + info.id, info.title);
+                assertNull(((ShortcutInfo) info).iconBitmap);
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
new file mode 100644
index 0000000..173c556
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -0,0 +1,236 @@
+package com.android.launcher3.model;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.database.MatrixCursor;
+import android.graphics.Bitmap;
+import android.os.Process;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.IconCache;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.compat.LauncherAppsCompat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import static com.android.launcher3.LauncherSettings.BaseLauncherColumns.INTENT;
+import static com.android.launcher3.LauncherSettings.Favorites.CELLX;
+import static com.android.launcher3.LauncherSettings.Favorites.CELLY;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+import static com.android.launcher3.LauncherSettings.Favorites.ICON;
+import static com.android.launcher3.LauncherSettings.Favorites.ICON_PACKAGE;
+import static com.android.launcher3.LauncherSettings.Favorites.ICON_RESOURCE;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+import static com.android.launcher3.LauncherSettings.Favorites.PROFILE_ID;
+import static com.android.launcher3.LauncherSettings.Favorites.RESTORED;
+import static com.android.launcher3.LauncherSettings.Favorites.SCREEN;
+import static com.android.launcher3.LauncherSettings.Favorites.TITLE;
+import static com.android.launcher3.LauncherSettings.Favorites._ID;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link LoaderCursor}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LoaderCursorTest {
+
+    private LauncherAppState mMockApp;
+    private IconCache mMockIconCache;
+
+    private MatrixCursor mCursor;
+    private InvariantDeviceProfile mIDP;
+    private Context mContext;
+    private LauncherAppsCompat mLauncherApps;
+
+    private LoaderCursor mLoaderCursor;
+
+    @Before
+    public void setup() {
+        mIDP = new InvariantDeviceProfile();
+        mCursor = new MatrixCursor(new String[] {
+                ICON, ICON_PACKAGE, ICON_RESOURCE, TITLE,
+                _ID, CONTAINER, ITEM_TYPE, PROFILE_ID,
+                SCREEN, CELLX, CELLY, RESTORED, INTENT
+        });
+        mContext = InstrumentationRegistry.getTargetContext();
+
+        mMockApp = mock(LauncherAppState.class);
+        mMockIconCache = mock(IconCache.class);
+        when(mMockApp.getIconCache()).thenReturn(mMockIconCache);
+        when(mMockApp.getInvariantDeviceProfile()).thenReturn(mIDP);
+        when(mMockApp.getContext()).thenReturn(mContext);
+        mLauncherApps = LauncherAppsCompat.getInstance(mContext);
+
+        mLoaderCursor = new LoaderCursor(mCursor, mMockApp);
+        mLoaderCursor.allUsers.put(0, Process.myUserHandle());
+    }
+
+    private void initCursor(int itemType, String title) {
+        mCursor.newRow()
+                .add(_ID, 1)
+                .add(PROFILE_ID, 0)
+                .add(ITEM_TYPE, itemType)
+                .add(TITLE, title)
+                .add(CONTAINER, CONTAINER_DESKTOP);
+    }
+
+    @Test
+    public void getAppShortcutInfo_dontAllowMissing_invalidComponent() {
+        initCursor(ITEM_TYPE_APPLICATION, "");
+        assertTrue(mLoaderCursor.moveToNext());
+        ComponentName cn = new ComponentName(mContext.getPackageName(), "dummy-do");
+        assertNull(mLoaderCursor.getAppShortcutInfo(
+                new Intent().setComponent(cn), false /* allowMissingTarget */, true));
+    }
+
+    @Test
+    public void getAppShortcutInfo_dontAllowMissing_validComponent() {
+        initCursor(ITEM_TYPE_APPLICATION, "");
+        assertTrue(mLoaderCursor.moveToNext());
+
+        ComponentName cn = mLauncherApps.getActivityList(null, mLoaderCursor.user)
+                .get(0).getComponentName();
+        ShortcutInfo info = mLoaderCursor.getAppShortcutInfo(
+                new Intent().setComponent(cn), false /* allowMissingTarget */, true);
+        assertNotNull(info);
+        assertTrue(Utilities.isLauncherAppTarget(info.intent));
+    }
+
+    @Test
+    public void getAppShortcutInfo_allowMissing_invalidComponent() {
+        initCursor(ITEM_TYPE_APPLICATION, "");
+        assertTrue(mLoaderCursor.moveToNext());
+
+        ComponentName cn = new ComponentName(mContext.getPackageName(), "dummy-do");
+        ShortcutInfo info = mLoaderCursor.getAppShortcutInfo(
+                new Intent().setComponent(cn), true  /* allowMissingTarget */, true);
+        assertNotNull(info);
+        assertTrue(Utilities.isLauncherAppTarget(info.intent));
+    }
+
+    @Test
+    public void loadSimpleShortcut() {
+        initCursor(ITEM_TYPE_SHORTCUT, "my-shortcut");
+        assertTrue(mLoaderCursor.moveToNext());
+
+        Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
+        when(mMockIconCache.getDefaultIcon(eq(mLoaderCursor.user))).thenReturn(icon);
+        ShortcutInfo info = mLoaderCursor.loadSimpleShortcut();
+        assertEquals(icon, info.iconBitmap);
+        assertEquals("my-shortcut", info.title);
+        assertEquals(ITEM_TYPE_SHORTCUT, info.itemType);
+    }
+
+    @Test
+    public void checkItemPlacement_wrongWorkspaceScreen() {
+        ArrayList<Long> workspaceScreens = new ArrayList<>(Arrays.asList(1L, 3L));
+        mIDP.numRows = 4;
+        mIDP.numColumns = 4;
+        mIDP.numHotseatIcons = 3;
+
+        // Item on unknown screen are not placed
+        assertFalse(mLoaderCursor.checkItemPlacement(
+                newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 4L), workspaceScreens));
+        assertFalse(mLoaderCursor.checkItemPlacement(
+                newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 5L), workspaceScreens));
+        assertFalse(mLoaderCursor.checkItemPlacement(
+                newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 2L), workspaceScreens));
+
+        assertTrue(mLoaderCursor.checkItemPlacement(
+                newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1L), workspaceScreens));
+        assertTrue(mLoaderCursor.checkItemPlacement(
+                newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 3L), workspaceScreens));
+
+    }
+    @Test
+    public void checkItemPlacement_outsideBounds() {
+        ArrayList<Long> workspaceScreens = new ArrayList<>(Arrays.asList(1L, 2L));
+        mIDP.numRows = 4;
+        mIDP.numColumns = 4;
+        mIDP.numHotseatIcons = 3;
+
+        // Item outside screen bounds are not placed
+        assertFalse(mLoaderCursor.checkItemPlacement(
+                newItemInfo(4, 4, 1, 1, CONTAINER_DESKTOP, 1L), workspaceScreens));
+    }
+
+    @Test
+    public void checkItemPlacement_overlappingItems() {
+        ArrayList<Long> workspaceScreens = new ArrayList<>(Arrays.asList(1L, 2L));
+        mIDP.numRows = 4;
+        mIDP.numColumns = 4;
+        mIDP.numHotseatIcons = 3;
+
+        // Overlapping items are not placed
+        assertTrue(mLoaderCursor.checkItemPlacement(
+                newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1L), workspaceScreens));
+        assertFalse(mLoaderCursor.checkItemPlacement(
+                newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1L), workspaceScreens));
+
+        assertTrue(mLoaderCursor.checkItemPlacement(
+                newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 2L), workspaceScreens));
+        assertFalse(mLoaderCursor.checkItemPlacement(
+                newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 2L), workspaceScreens));
+
+        assertTrue(mLoaderCursor.checkItemPlacement(
+                newItemInfo(1, 1, 1, 1, CONTAINER_DESKTOP, 1L), workspaceScreens));
+        assertTrue(mLoaderCursor.checkItemPlacement(
+                newItemInfo(2, 2, 2, 2, CONTAINER_DESKTOP, 1L), workspaceScreens));
+
+        assertFalse(mLoaderCursor.checkItemPlacement(
+                newItemInfo(3, 2, 1, 2, CONTAINER_DESKTOP, 1L), workspaceScreens));
+    }
+
+    @Test
+    public void checkItemPlacement_hotseat() {
+        ArrayList<Long> workspaceScreens = new ArrayList<>();
+        mIDP.numRows = 4;
+        mIDP.numColumns = 4;
+        mIDP.numHotseatIcons = 3;
+
+        // Hotseat items are only placed based on screenId
+        assertTrue(mLoaderCursor.checkItemPlacement(
+                newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 1L), workspaceScreens));
+        assertTrue(mLoaderCursor.checkItemPlacement(
+                newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 2L), workspaceScreens));
+
+        assertFalse(mLoaderCursor.checkItemPlacement(
+                newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 3L), workspaceScreens));
+    }
+
+    private ItemInfo newItemInfo(int cellX, int cellY, int spanX, int spanY,
+            long container, long screenId) {
+        ItemInfo info = new ItemInfo();
+        info.cellX = cellX;
+        info.cellY = cellY;
+        info.spanX = spanX;
+        info.spanY = spanY;
+        info.container = container;
+        info.screenId = screenId;
+        return info;
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
new file mode 100644
index 0000000..d655562
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
@@ -0,0 +1,61 @@
+package com.android.launcher3.model;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppWidgetInfo;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.compat.PackageInstallerCompat;
+import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+/**
+ * Tests for {@link PackageInstallStateChangedTask}
+ */
+public class PackageInstallStateChangedTaskTest extends BaseModelUpdateTaskTestCase {
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        initializeData("package_install_state_change_task_data");
+    }
+
+    private PackageInstallStateChangedTask newTask(String pkg, int progress) {
+        PackageInstallInfo installInfo = new PackageInstallInfo(pkg);
+        installInfo.progress = progress;
+        installInfo.state = PackageInstallerCompat.STATUS_INSTALLING;
+        return new PackageInstallStateChangedTask(installInfo);
+    }
+
+    public void testSessionUpdate_ignore_installed() throws Exception {
+        executeTaskForTest(newTask("app1", 30));
+
+        // No shortcuts were updated
+        verifyProgressUpdate(0);
+    }
+
+    public void testSessionUpdate_shortcuts_updated() throws Exception {
+        executeTaskForTest(newTask("app3", 30));
+
+        verifyProgressUpdate(30, 5L, 6L, 7L);
+    }
+
+    public void testSessionUpdate_widgets_updated() throws Exception {
+        executeTaskForTest(newTask("app4", 30));
+
+        verifyProgressUpdate(30, 8L, 9L);
+    }
+
+    private void verifyProgressUpdate(int progress, Long... idsUpdated) {
+        HashSet<Long> updates = new HashSet<>(Arrays.asList(idsUpdated));
+        for (ItemInfo info : bgDataModel.itemsIdMap) {
+            if (info instanceof ShortcutInfo) {
+                assertEquals(updates.contains(info.id) ? progress: 0,
+                        ((ShortcutInfo) info).getInstallProgress());
+            } else {
+                assertEquals(updates.contains(info.id) ? progress: -1,
+                        ((LauncherAppWidgetInfo) info).installProgress);
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/shortcuts/ShortcutFilterTest.java b/tests/src/com/android/launcher3/popup/PopupPopulatorTest.java
similarity index 63%
rename from tests/src/com/android/launcher3/shortcuts/ShortcutFilterTest.java
rename to tests/src/com/android/launcher3/popup/PopupPopulatorTest.java
index 05d0ffb..2ad9b35 100644
--- a/tests/src/com/android/launcher3/shortcuts/ShortcutFilterTest.java
+++ b/tests/src/com/android/launcher3/popup/PopupPopulatorTest.java
@@ -14,11 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.shortcuts;
+package com.android.launcher3.popup;
 
 import android.content.pm.ShortcutInfo;
 import android.support.test.runner.AndroidJUnit4;
 
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -26,40 +28,64 @@
 import java.util.Collections;
 import java.util.List;
 
-import static com.android.launcher3.shortcuts.ShortcutFilter.MAX_SHORTCUTS;
-import static com.android.launcher3.shortcuts.ShortcutFilter.NUM_DYNAMIC;
+import static com.android.launcher3.popup.PopupPopulator.MAX_ITEMS;
+import static com.android.launcher3.popup.PopupPopulator.NUM_DYNAMIC;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 /**
- * Tests the sorting and filtering of shortcuts in {@link ShortcutFilter}.
+ * Tests the sorting and filtering of shortcuts in {@link PopupPopulator}.
  */
 @RunWith(AndroidJUnit4.class)
-public class ShortcutFilterTest {
+public class PopupPopulatorTest {
 
     @Test
     public void testSortAndFilterShortcuts() {
         filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(3, 0), 3, 0);
         filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(0, 3), 0, 3);
-        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(5, 0), MAX_SHORTCUTS, 0);
-        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(0, 5), 0, MAX_SHORTCUTS);
+        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(5, 0), MAX_ITEMS, 0);
+        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(0, 5), 0, MAX_ITEMS);
         filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(3, 3),
-                MAX_SHORTCUTS - NUM_DYNAMIC, NUM_DYNAMIC);
+                MAX_ITEMS - NUM_DYNAMIC, NUM_DYNAMIC);
         filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(5, 5),
-                MAX_SHORTCUTS - NUM_DYNAMIC, NUM_DYNAMIC);
-        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(5, 1), MAX_SHORTCUTS - 1, 1);
-        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(1, 5), 1, MAX_SHORTCUTS - 1);
+                MAX_ITEMS - NUM_DYNAMIC, NUM_DYNAMIC);
+        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(5, 1), MAX_ITEMS - 1, 1);
+        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(1, 5), 1, MAX_ITEMS - 1);
         filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(5, 3),
-                MAX_SHORTCUTS - NUM_DYNAMIC, NUM_DYNAMIC);
+                MAX_ITEMS - NUM_DYNAMIC, NUM_DYNAMIC);
         filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(3, 5),
-                MAX_SHORTCUTS - NUM_DYNAMIC, NUM_DYNAMIC);
+                MAX_ITEMS - NUM_DYNAMIC, NUM_DYNAMIC);
+    }
+
+    @Test
+    public void testDeDupeShortcutId() {
+        // Successfully remove one of the shortcuts
+        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(3, 0), 2, 0, generateId(true, 1));
+        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(0, 3), 0, 2, generateId(false, 1));
+        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(2, 2), 2, 1, generateId(false, 1));
+        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(2, 2), 1, 2, generateId(true, 1));
+        // Successfully keep all shortcuts when id doesn't exist
+        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(3, 0), 3, 0, generateId(false, 1));
+        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(3, 0), 3, 0, generateId(true, 4));
+        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(2, 2), 2, 2, generateId(false, 4));
+        filterShortcutsAndAssertNumStaticAndDynamic(createShortcutsList(2, 2), 2, 2, generateId(true, 4));
+    }
+
+    private String generateId(boolean isStatic, int rank) {
+        return (isStatic ? "static" : "dynamic") + rank;
     }
 
     private void filterShortcutsAndAssertNumStaticAndDynamic(
             List<ShortcutInfoCompat> shortcuts, int expectedStatic, int expectedDynamic) {
+        filterShortcutsAndAssertNumStaticAndDynamic(shortcuts, expectedStatic, expectedDynamic, null);
+    }
+
+    private void filterShortcutsAndAssertNumStaticAndDynamic(List<ShortcutInfoCompat> shortcuts,
+            int expectedStatic, int expectedDynamic, String shortcutIdToRemove) {
         Collections.shuffle(shortcuts);
-        List<ShortcutInfoCompat> filteredShortcuts = ShortcutFilter.sortAndFilterShortcuts(shortcuts);
+        List<ShortcutInfoCompat> filteredShortcuts = PopupPopulator.sortAndFilterShortcuts(
+                shortcuts, shortcutIdToRemove);
         assertIsSorted(filteredShortcuts);
 
         int numStatic = 0;
@@ -110,6 +136,7 @@
     private class Shortcut extends ShortcutInfoCompat {
         private boolean mIsStatic;
         private int mRank;
+        private String mId;
 
         public Shortcut(ShortcutInfo shortcutInfo) {
             super(shortcutInfo);
@@ -119,6 +146,7 @@
             this(null);
             mIsStatic = isStatic;
             mRank = rank;
+            mId = generateId(isStatic, rank);
         }
 
         @Override
@@ -135,5 +163,10 @@
         public int getRank() {
             return mRank;
         }
+
+        @Override
+        public String getId() {
+            return mId;
+        }
     }
 }
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/testcomponent/AppWidgetNoConfig.java b/tests/src/com/android/launcher3/testcomponent/AppWidgetNoConfig.java
new file mode 100644
index 0000000..9b320d8
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/AppWidgetNoConfig.java
@@ -0,0 +1,26 @@
+/*
+ * 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.appwidget.AppWidgetProvider;
+
+/**
+ * A simple app widget without any configuration screen.
+ */
+public class AppWidgetNoConfig extends AppWidgetProvider {
+
+
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/AppWidgetWithConfig.java b/tests/src/com/android/launcher3/testcomponent/AppWidgetWithConfig.java
new file mode 100644
index 0000000..033e6e6
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/AppWidgetWithConfig.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+/**
+ * A simple app widget with configuration sceen.
+ */
+public class AppWidgetWithConfig extends AppWidgetNoConfig {
+
+}
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..8580992
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/RequestPinItemActivity.java
@@ -0,0 +1,105 @@
+/*
+ * 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;
+import android.widget.RemoteViews;
+
+/**
+ * Sample activity to request pinning an item.
+ */
+@TargetApi(26)
+public class RequestPinItemActivity extends BaseTestingActivity {
+
+    private PendingIntent mCallback = null;
+    private String mShortcutId = "test-id";
+    private int mRemoteViewColor = Color.TRANSPARENT;
+
+    @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 setRemoteViewColor(int color) {
+        mRemoteViewColor = color;
+    }
+
+    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) {
+        Bundle extras = null;
+        if (mRemoteViewColor != Color.TRANSPARENT) {
+            int layoutId = getResources().getIdentifier(
+                    "test_layout_appwidget_view", "layout", getPackageName());
+            RemoteViews views = new RemoteViews(getPackageName(), layoutId);
+            views.setInt(android.R.id.icon, "setBackgroundColor", mRemoteViewColor);
+            extras = new Bundle();
+            extras.putParcelable(AppWidgetManager.EXTRA_APPWIDGET_PREVIEW, views);
+        }
+
+        AppWidgetManager.getInstance(this).requestPinAppWidget(cn, extras, mCallback);
+    }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/WidgetConfigActivity.java b/tests/src/com/android/launcher3/testcomponent/WidgetConfigActivity.java
new file mode 100644
index 0000000..d76ad04
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/WidgetConfigActivity.java
@@ -0,0 +1,44 @@
+/*
+ * 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.os.Bundle;
+
+/**
+ * Simple activity for widget configuration
+ */
+public class WidgetConfigActivity extends BaseTestingActivity {
+
+    public static final String SUFFIX_FINISH = "-finish";
+    public static final String EXTRA_CODE = "code";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        addButton("Cancel", "clickCancel");
+        addButton("OK", "clickOK");
+    }
+
+    public void clickCancel() {
+        setResult(RESULT_CANCELED);
+        finish();
+    }
+
+    public void clickOK() {
+        setResult(RESULT_OK);
+        finish();
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/AllAppsAppLaunchTest.java b/tests/src/com/android/launcher3/ui/AllAppsAppLaunchTest.java
index abe6b95..0ced7cf 100644
--- a/tests/src/com/android/launcher3/ui/AllAppsAppLaunchTest.java
+++ b/tests/src/com/android/launcher3/ui/AllAppsAppLaunchTest.java
@@ -1,13 +1,13 @@
 package com.android.launcher3.ui;
 
+import android.content.pm.LauncherActivityInfo;
+import android.os.Process;
 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 com.android.launcher3.compat.LauncherActivityInfoCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.util.Condition;
 import com.android.launcher3.util.Wait;
 
@@ -17,14 +17,14 @@
 @LargeTest
 public class AllAppsAppLaunchTest extends LauncherInstrumentationTestCase {
 
-    private LauncherActivityInfoCompat mSettingsApp;
+    private LauncherActivityInfo mSettingsApp;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
 
         mSettingsApp = LauncherAppsCompat.getInstance(mTargetContext)
-                .getActivityList("com.android.settings", UserHandleCompat.myUserHandle()).get(0);
+                .getActivityList("com.android.settings", Process.myUserHandle()).get(0);
     }
 
     public void testAppLauncher_portrait() throws Exception {
diff --git a/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java b/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
index 56fc90a..9361750 100644
--- a/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
+++ b/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
@@ -1,13 +1,13 @@
 package com.android.launcher3.ui;
 
+import android.content.pm.LauncherActivityInfo;
+import android.os.Process;
 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 com.android.launcher3.compat.LauncherActivityInfoCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.util.Condition;
 import com.android.launcher3.util.Wait;
 
@@ -17,14 +17,15 @@
 @LargeTest
 public class AllAppsIconToHomeTest extends LauncherInstrumentationTestCase {
 
-    private LauncherActivityInfoCompat mSettingsApp;
+    private LauncherActivityInfo mSettingsApp;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+        setDefaultLauncher();
 
         mSettingsApp = LauncherAppsCompat.getInstance(mTargetContext)
-                .getActivityList("com.android.settings", UserHandleCompat.myUserHandle()).get(0);
+                .getActivityList("com.android.settings", Process.myUserHandle()).get(0);
     }
 
     public void testDragIcon_portrait() throws Throwable {
@@ -47,7 +48,7 @@
 
         // Drag icon to homescreen.
         UiObject2 icon = scrollAndFind(appsContainer, By.text(mSettingsApp.getLabel().toString()));
-        dragToWorkspace(icon);
+        dragToWorkspace(icon, true);
 
         // Verify that the icon works on homescreen.
         mDevice.findObject(By.text(mSettingsApp.getLabel().toString())).click();
diff --git a/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java b/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
index e858d17..47b43f5 100644
--- a/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
+++ b/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
@@ -1,13 +1,31 @@
+/*
+ * 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;
 
-import android.app.SearchManager;
-import android.appwidget.AppWidgetProviderInfo;
+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;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.support.test.uiautomator.By;
@@ -19,30 +37,39 @@
 import android.test.InstrumentationTestCase;
 import android.view.MotionEvent;
 
-import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.LauncherClings;
 import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.testcomponent.AppWidgetNoConfig;
+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.atomic.AtomicReference;
+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;
 
     protected UiDevice mDevice;
     protected Context mTargetContext;
@@ -74,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);
     }
 
     /**
@@ -89,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() {
@@ -141,29 +186,52 @@
     /**
      * Drags an icon to the center of homescreen.
      */
-    protected void dragToWorkspace(UiObject2 icon) {
+    protected void dragToWorkspace(UiObject2 icon, boolean expectedToShowShortcuts) {
         Point center = icon.getVisibleCenter();
 
         // Action Down
         sendPointer(MotionEvent.ACTION_DOWN, center);
 
-        // Wait until "Remove/Delete target is visible
+        UiObject2 dragLayer = findViewById(R.id.drag_layer);
+
+        if (expectedToShowShortcuts) {
+            // Make sure shortcuts show up, and then move a bit to hide them.
+            assertNotNull(findViewById(R.id.deep_shortcuts_container));
+
+            Point moveLocation = new Point(center);
+            int distanceToMove = mTargetContext.getResources().getDimensionPixelSize(
+                    R.dimen.deep_shortcuts_start_drag_threshold) + 50;
+            if (moveLocation.y - distanceToMove >= dragLayer.getVisibleBounds().top) {
+                moveLocation.y -= distanceToMove;
+            } else {
+                moveLocation.y += distanceToMove;
+            }
+            movePointer(center, moveLocation);
+
+            assertNull(findViewById(R.id.deep_shortcuts_container));
+        }
+
+        // Wait until Remove/Delete target is visible
         assertNotNull(findViewById(R.id.delete_target_text));
 
-        Point moveLocation = findViewById(R.id.drag_layer).getVisibleCenter();
+        Point moveLocation = dragLayer.getVisibleCenter();
 
         // Move to center
-        while(!moveLocation.equals(center)) {
-            center.x = getNextMoveValue(moveLocation.x, center.x);
-            center.y = getNextMoveValue(moveLocation.y, center.y);
-            sendPointer(MotionEvent.ACTION_MOVE, center);
-        }
+        movePointer(center, moveLocation);
         sendPointer(MotionEvent.ACTION_UP, center);
 
         // Wait until remove target is gone.
         mDevice.wait(Until.gone(getSelectorForId(R.id.delete_target_text)), DEFAULT_UI_TIMEOUT);
     }
 
+    private void movePointer(Point from, Point to) {
+        while(!from.equals(to)) {
+            from.x = getNextMoveValue(to.x, from.x);
+            from.y = getNextMoveValue(to.y, from.y);
+            sendPointer(MotionEvent.ACTION_MOVE, from);
+        }
+    }
+
     private int getNextMoveValue(int targetValue, int oldValue) {
         if (targetValue - oldValue > 10) {
             return oldValue + 10;
@@ -174,7 +242,7 @@
         }
     }
 
-    private void sendPointer(int action, Point point) {
+    protected void sendPointer(int action, Point point) {
         MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(),
                 SystemClock.uptimeMillis(), action, point.x, point.y, 0);
         getInstrumentation().sendPointerSync(event);
@@ -189,34 +257,32 @@
                 LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
         LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
                 LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
-        LauncherClings.markFirstRunClingDismissed(mTargetContext);
-        ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mTargetContext);
+        resetLoaderState();
+    }
 
-        runTestOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                // Reset the loader state
-                LauncherAppState.getInstance().getModel().resetLoadedState(true, true);
-            }
-        });
+    protected void resetLoaderState() {
+        try {
+            runTestOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mTargetContext);
+                    LauncherAppState.getInstance(mTargetContext).getModel().forceReload();
+                }
+            });
+        } catch (Throwable t) {
+            throw new IllegalArgumentException(t);
+        }
     }
 
     /**
      * Runs the callback on the UI thread and returns the result.
      */
     protected <T> T getOnUiThread(final Callable<T> callback) {
-        final AtomicReference<T> result = new AtomicReference<>(null);
         try {
-            runTestOnUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    try {
-                        result.set(callback.call());
-                    } catch (Exception e) { }
-                }
-            });
-        } catch (Throwable t) { }
-        return result.get();
+            return new MainThreadExecutor().submit(callback).get();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
     }
 
     /**
@@ -224,36 +290,14 @@
      * @param hasConfigureScreen if true, a provider with a config screen is returned.
      */
     protected LauncherAppWidgetProviderInfo findWidgetProvider(final boolean hasConfigureScreen) {
-        LauncherAppWidgetProviderInfo info = getOnUiThread(new Callable<LauncherAppWidgetProviderInfo>() {
+        LauncherAppWidgetProviderInfo info =
+                getOnUiThread(new Callable<LauncherAppWidgetProviderInfo>() {
             @Override
             public LauncherAppWidgetProviderInfo call() throws Exception {
-                InvariantDeviceProfile idv =
-                        LauncherAppState.getInstance().getInvariantDeviceProfile();
-
-                ComponentName searchComponent = ((SearchManager) mTargetContext
-                        .getSystemService(Context.SEARCH_SERVICE)).getGlobalSearchActivity();
-                String searchPackage = searchComponent == null
-                        ? null : searchComponent.getPackageName();
-
-                for (AppWidgetProviderInfo info :
-                        AppWidgetManagerCompat.getInstance(mTargetContext).getAllProviders()) {
-                    if ((info.configure != null) ^ hasConfigureScreen) {
-                        continue;
-                    }
-                    // Exclude the widgets in search package, as Launcher already binds them in
-                    // QSB, so they can cause conflicts.
-                    if (info.provider.getPackageName().equals(searchPackage)) {
-                        continue;
-                    }
-                    LauncherAppWidgetProviderInfo widgetInfo = LauncherAppWidgetProviderInfo
-                            .fromProviderInfo(mTargetContext, info);
-                    if (widgetInfo.minSpanX >= idv.numColumns
-                            || widgetInfo.minSpanY >= idv.numRows) {
-                        continue;
-                    }
-                    return widgetInfo;
-                }
-                return null;
+                ComponentName cn = new ComponentName(getInstrumentation().getContext(),
+                        hasConfigureScreen ? AppWidgetWithConfig.class : AppWidgetNoConfig.class);
+                return AppWidgetManagerCompat.getInstance(mTargetContext)
+                        .findProvider(cn, Process.myUserHandle());
             }
         });
         if (info == null) {
@@ -270,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
new file mode 100644
index 0000000..3a0b613
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
@@ -0,0 +1,69 @@
+package com.android.launcher3.ui;
+
+import android.content.pm.LauncherActivityInfo;
+import android.graphics.Point;
+import android.os.Process;
+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.MotionEvent;
+
+import com.android.launcher3.R;
+import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.util.Condition;
+import com.android.launcher3.util.Wait;
+
+/**
+ * Test for verifying that shortcuts are shown and can be launched after long pressing an app
+ */
+@LargeTest
+public class ShortcutsLaunchTest extends LauncherInstrumentationTestCase {
+
+    private LauncherActivityInfo mSettingsApp;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        setDefaultLauncher();
+
+        mSettingsApp = LauncherAppsCompat.getInstance(mTargetContext)
+                .getActivityList("com.android.settings", Process.myUserHandle()).get(0);
+    }
+
+    public void testAppLauncher_portrait() throws Exception {
+        lockRotation(true);
+        performTest();
+    }
+
+    public void testAppLauncher_landscape() throws Exception {
+        lockRotation(false);
+        performTest();
+    }
+
+    private void performTest() throws Exception {
+        startLauncher();
+
+        // Open all apps and wait for load complete
+        final UiObject2 appsContainer = openAllApps();
+        assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT));
+
+        // Find settings app and verify shortcuts appear when long pressed
+        UiObject2 icon = scrollAndFind(appsContainer, By.text(mSettingsApp.getLabel().toString()));
+        // Press icon center until shortcuts appear
+        Point iconCenter = icon.getVisibleCenter();
+        sendPointer(MotionEvent.ACTION_DOWN, iconCenter);
+        UiObject2 deepShortcutsContainer = findViewById(R.id.deep_shortcuts_container);
+        assertNotNull(deepShortcutsContainer);
+        sendPointer(MotionEvent.ACTION_UP, iconCenter);
+
+        // Verify that launching a shortcut opens a page with the same text
+        assertTrue(deepShortcutsContainer.getChildCount() > 0);
+        UiObject2 shortcut = deepShortcutsContainer.getChildren().get(0)
+                .findObject(getSelectorForId(R.id.bubble_text));
+        shortcut.click();
+        assertTrue(mDevice.wait(Until.hasObject(By.pkg(
+                mSettingsApp.getComponentName().getPackageName())
+                .text(shortcut.getText())), DEFAULT_UI_TIMEOUT));
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java b/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java
new file mode 100644
index 0000000..5d86d1e
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java
@@ -0,0 +1,75 @@
+package com.android.launcher3.ui;
+
+import android.content.pm.LauncherActivityInfo;
+import android.graphics.Point;
+import android.os.Process;
+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.MotionEvent;
+
+import com.android.launcher3.R;
+import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.util.Condition;
+import com.android.launcher3.util.Wait;
+
+/**
+ * Test for dragging a deep shortcut to the home screen.
+ */
+@LargeTest
+public class ShortcutsToHomeTest extends LauncherInstrumentationTestCase {
+
+    private LauncherActivityInfo mSettingsApp;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        setDefaultLauncher();
+
+        mSettingsApp = LauncherAppsCompat.getInstance(mTargetContext)
+                .getActivityList("com.android.settings", Process.myUserHandle()).get(0);
+    }
+
+    public void testDragIcon_portrait() throws Throwable {
+        lockRotation(true);
+        performTest();
+    }
+
+    public void testDragIcon_landscape() throws Throwable {
+        lockRotation(false);
+        performTest();
+    }
+
+    private void performTest() throws Throwable {
+        clearHomescreen();
+        startLauncher();
+
+        // Open all apps and wait for load complete.
+        final UiObject2 appsContainer = openAllApps();
+        assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT));
+
+        // Find the app and long press it to show shortcuts.
+        UiObject2 icon = scrollAndFind(appsContainer, By.text(mSettingsApp.getLabel().toString()));
+        // Press icon center until shortcuts appear
+        Point iconCenter = icon.getVisibleCenter();
+        sendPointer(MotionEvent.ACTION_DOWN, iconCenter);
+        UiObject2 deepShortcutsContainer = findViewById(R.id.deep_shortcuts_container);
+        assertNotNull(deepShortcutsContainer);
+        sendPointer(MotionEvent.ACTION_UP, iconCenter);
+
+        // Drag the first shortcut to the home screen.
+        assertTrue(deepShortcutsContainer.getChildCount() > 0);
+        UiObject2 shortcut = deepShortcutsContainer.getChildren().get(0)
+                .findObject(getSelectorForId(R.id.bubble_text));
+        String shortcutName = shortcut.getText();
+        dragToWorkspace(shortcut, false);
+
+        // Verify that the shortcut works on home screen
+        // (the app opens and has the same text as the shortcut).
+        mDevice.findObject(By.text(shortcutName)).click();
+        assertTrue(mDevice.wait(Until.hasObject(By.pkg(
+                mSettingsApp.getComponentName().getPackageName())
+                .text(shortcutName)), DEFAULT_UI_TIMEOUT));
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
new file mode 100644
index 0000000..0b4e34f
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.appwidget.AppWidgetManager;
+import android.content.Intent;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+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.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.testcomponent.WidgetConfigActivity;
+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.concurrent.Callable;
+
+/**
+ * Test to verify widget configuration is properly shown.
+ */
+@LargeTest
+public class AddConfigWidgetTest extends LauncherInstrumentationTestCase {
+
+    private LauncherAppWidgetProviderInfo mWidgetInfo;
+    private SimpleActivityMonitor mActivityMonitor;
+    private MainThreadExecutor mMainThreadExecutor;
+    private AppWidgetManager mAppWidgetManager;
+
+    private int mWidgetId;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mWidgetInfo = findWidgetProvider(true /* hasConfigureScreen */);
+        mActivityMonitor = new SimpleActivityMonitor();
+        ((Application) getInstrumentation().getTargetContext().getApplicationContext())
+                .registerActivityLifecycleCallbacks(mActivityMonitor);
+        mMainThreadExecutor = new MainThreadExecutor();
+        mAppWidgetManager = AppWidgetManager.getInstance(mTargetContext);
+
+        grantWidgetPermission();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        ((Application) getInstrumentation().getTargetContext().getApplicationContext())
+                .unregisterActivityLifecycleCallbacks(mActivityMonitor);
+        super.tearDown();
+    }
+
+    public void testWidgetConfig() throws Throwable {
+        runTest(false, true);
+    }
+
+    public void testWidgetConfig_rotate() throws Throwable {
+        runTest(true, true);
+    }
+
+    public void testConfigCancelled() throws Throwable {
+        runTest(false, false);
+    }
+
+    public void testConfigCancelled_rotate() throws Throwable {
+        runTest(true, false);
+    }
+
+    /**
+     * @param rotateConfig should the config screen be rotated
+     * @param acceptConfig accept the config activity
+     */
+    private void runTest(boolean rotateConfig, boolean acceptConfig) throws Throwable {
+        lockRotation(true);
+
+        clearHomescreen();
+        startLauncher();
+
+        // Open widget tray and wait for load complete.
+        final UiObject2 widgetContainer = openWidgetsTray();
+        assertTrue(Wait.atMost(Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT));
+
+        // Drag widget to homescreen
+        WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
+        UiObject2 widget = scrollAndFind(widgetContainer, By.clazz(WidgetCell.class)
+                .hasDescendant(By.text(mWidgetInfo.getLabel(mTargetContext.getPackageManager()))));
+        dragToWorkspace(widget, false);
+        // Widget id for which the config activity was opened
+        mWidgetId = monitor.getWidgetId();
+
+        if (rotateConfig) {
+            // Rotate the screen and verify that the config activity is recreated
+            monitor = new WidgetConfigStartupMonitor();
+            lockRotation(false);
+            assertEquals(mWidgetId, monitor.getWidgetId());
+        }
+
+        // Verify that the widget id is valid and bound
+        assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
+
+        setResult(acceptConfig);
+        if (acceptConfig) {
+            assertTrue(Wait.atMost(new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT));
+            assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
+        } else {
+            // Verify that the widget id is deleted.
+            assertTrue(Wait.atMost(new Condition() {
+                @Override
+                public boolean isTrue() throws Throwable {
+                    return mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null;
+                }
+            }, DEFAULT_ACTIVITY_TIMEOUT));
+        }
+    }
+
+    private void setResult(boolean success) {
+
+        getInstrumentation().getTargetContext().sendBroadcast(
+                WidgetConfigActivity.getCommandIntent(WidgetConfigActivity.class,
+                        success ? "clickOK" : "clickCancel"));
+    }
+
+    /**
+     * Condition for searching widget id
+     */
+    private class WidgetSearchCondition extends Condition
+            implements Callable<Boolean>, Workspace.ItemOperator {
+
+        @Override
+        public boolean isTrue() throws Throwable {
+            return mMainThreadExecutor.submit(this).get();
+        }
+
+        @Override
+        public boolean evaluate(ItemInfo info, View view) {
+            return info instanceof LauncherAppWidgetInfo &&
+                    ((LauncherAppWidgetInfo) info).providerName.equals(mWidgetInfo.provider) &&
+                    ((LauncherAppWidgetInfo) info).appWidgetId == mWidgetId;
+        }
+
+        @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(this) != null;
+        }
+    }
+
+    /**
+     * Broadcast receiver for receiving widget config activity status.
+     */
+    private class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver {
+
+        public WidgetConfigStartupMonitor() {
+            super(WidgetConfigActivity.class.getName());
+        }
+
+        public int getWidgetId() throws InterruptedException {
+            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/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
similarity index 72%
rename from tests/src/com/android/launcher3/ui/AddWidgetTest.java
rename to tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index a0ca60c..3c92c57 100644
--- a/tests/src/com/android/launcher3/ui/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -1,16 +1,31 @@
-package com.android.launcher3.ui;
+/*
+ * 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.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiObject2;
 import android.test.suitebuilder.annotation.LargeTest;
 import android.view.View;
 
-import com.android.launcher3.CellLayout;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.Workspace.ItemOperator;
+import com.android.launcher3.ui.LauncherInstrumentationTestCase;
 import com.android.launcher3.util.Condition;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.widget.WidgetCell;
@@ -26,6 +41,7 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+        grantWidgetPermission();
 
         widgetInfo = findWidgetProvider(false /* hasConfigureScreen */);
     }
@@ -51,7 +67,7 @@
         // Drag widget to homescreen
         UiObject2 widget = scrollAndFind(widgetContainer, By.clazz(WidgetCell.class)
                 .hasDescendant(By.text(widgetInfo.getLabel(mTargetContext.getPackageManager()))));
-        dragToWorkspace(widget);
+        dragToWorkspace(widget, false);
 
         assertNotNull(launcher.getWorkspace().getFirstMatch(new ItemOperator() {
             @Override
diff --git a/tests/src/com/android/launcher3/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
similarity index 88%
rename from tests/src/com/android/launcher3/BindWidgetTest.java
rename to tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index 5c5069f..df2b662 100644
--- a/tests/src/com/android/launcher3/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -1,6 +1,20 @@
-package com.android.launcher3;
+/*
+ * 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.annotation.TargetApi;
 import android.appwidget.AppWidgetHost;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -9,15 +23,23 @@
 import android.content.pm.PackageInstaller.SessionParams;
 import android.content.pm.PackageManager;
 import android.database.Cursor;
-import android.os.Build;
 import android.os.Bundle;
 import android.support.test.uiautomator.UiSelector;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppWidgetHostView;
+import com.android.launcher3.LauncherAppWidgetInfo;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.PendingAppWidgetHostView;
+import com.android.launcher3.Workspace;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.ui.LauncherInstrumentationTestCase;
-import com.android.launcher3.util.ManagedProfileHeuristic;
+import com.android.launcher3.util.ContentWriter;
+import com.android.launcher3.util.LooperExecuter;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.WidgetHostViewLoader;
 
@@ -32,7 +54,6 @@
  * Note running these tests will clear the workspace on the device.
  */
 @LargeTest
-@TargetApi(Build.VERSION_CODES.LOLLIPOP)
 public class BindWidgetTest extends LauncherInstrumentationTestCase {
 
     private ContentResolver mResolver;
@@ -218,28 +239,16 @@
         mResolver.insert(LauncherSettings.WorkspaceScreens.CONTENT_URI, v);
 
         // Insert the item
-        v = new ContentValues();
+        ContentWriter writer = new ContentWriter(mTargetContext);
         item.id = LauncherSettings.Settings.call(
                 mResolver, LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
                 .getLong(LauncherSettings.Settings.EXTRA_VALUE);
         item.screenId = screenId;
-        item.onAddToDatabase(mTargetContext, v);
-        v.put(LauncherSettings.Favorites._ID, item.id);
-        mResolver.insert(LauncherSettings.Favorites.CONTENT_URI, v);
+        item.onAddToDatabase(writer);
+        writer.put(LauncherSettings.Favorites._ID, item.id);
+        mResolver.insert(LauncherSettings.Favorites.CONTENT_URI, writer.getValues(mTargetContext));
+        resetLoaderState();
 
-        // Reset loader
-        try {
-            runTestOnUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    LauncherClings.markFirstRunClingDismissed(mTargetContext);
-                    ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mTargetContext);
-                    LauncherAppState.getInstance().getModel().resetLoadedState(true, true);
-                }
-            });
-        } catch (Throwable t) {
-            throw new IllegalArgumentException(t);
-        }
         // Launch the home activity
         startLauncher();
         // Verify UI
@@ -264,13 +273,13 @@
         item.spanY = info.minSpanY;
         item.minSpanX = info.minSpanX;
         item.minSpanY = info.minSpanY;
-        item.user = mWidgetManager.getUser(info);
+        item.user = info.getUser();
         item.cellX = 0;
         item.cellY = 1;
         item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
 
         if (bindWidget) {
-            PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(mTargetContext, info);
+            PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(info);
             pendingInfo.spanX = item.spanX;
             pendingInfo.spanY = item.spanY;
             pendingInfo.minSpanX = item.minSpanX;
@@ -330,14 +339,11 @@
     /**
      * Blocks the current thread until all the jobs in the main worker thread are complete.
      */
-    private void waitUntilLoaderIdle() throws InterruptedException {
-        final CountDownLatch latch = new CountDownLatch(1);
-        LauncherModel.sWorker.post(new Runnable() {
-            @Override
-            public void run() {
-                latch.countDown();
-            }
-        });
-        assertTrue(latch.await(5, TimeUnit.SECONDS));
+    private void waitUntilLoaderIdle() throws Exception {
+        new LooperExecuter(LauncherModel.getWorkerLooper())
+                .submit(new Runnable() {
+                    @Override
+                    public void run() { }
+                }).get(DEFAULT_WORKER_TIMEOUT_SECS, TimeUnit.SECONDS);
     }
 }
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..b798dfa
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.graphics.Color;
+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 testPinWidgetNoConfig_customPreview() throws Throwable {
+        // Command to set custom preview
+        Intent command =  RequestPinItemActivity.getCommandIntent(
+                RequestPinItemActivity.class, "setRemoteViewColor").putExtra(
+                RequestPinItemActivity.EXTRA_PARAM + "0", Color.RED);
+
+        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());
+            }
+        }, command);
+    }
+
+    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 testPinShortcut() throws Throwable {
+        // Command to set the shortcut id
+        Intent command = RequestPinItemActivity.getCommandIntent(
+                RequestPinItemActivity.class, "setShortcutId").putExtra(
+                RequestPinItemActivity.EXTRA_PARAM + "0", mShortcutId);
+
+        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);
+            }
+        }, command);
+    }
+
+    private void runTest(String activityMethod, boolean isWidget, ItemOperator itemMatcher,
+            Intent... commandIntents) 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));
+
+        for (Intent command : commandIntents) {
+            mTargetContext.sendBroadcast(command);
+        }
+
+        // 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;
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/FocusLogicTest.java b/tests/src/com/android/launcher3/util/FocusLogicTest.java
index eee567f..79aed80 100644
--- a/tests/src/com/android/launcher3/util/FocusLogicTest.java
+++ b/tests/src/com/android/launcher3/util/FocusLogicTest.java
@@ -49,8 +49,6 @@
          assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_MOVE_END));
          assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_PAGE_UP));
          assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_PAGE_DOWN));
-         assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DEL));
-         assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_FORWARD_DEL));
     }
 
     public void testCreateSparseMatrix() {
diff --git a/tests/src/com/android/launcher3/util/SimpleActivityMonitor.java b/tests/src/com/android/launcher3/util/SimpleActivityMonitor.java
new file mode 100644
index 0000000..6154ab6
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/SimpleActivityMonitor.java
@@ -0,0 +1,65 @@
+/*
+ * 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.util;
+
+import android.app.Activity;
+import android.app.Application.*;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+
+/**
+ * Simple monitor to keep a list of active activities.
+ */
+public class SimpleActivityMonitor implements ActivityLifecycleCallbacks {
+
+    public final ArrayList<Activity> created = new ArrayList<>();
+    public final ArrayList<Activity> started = new ArrayList<>();
+    public final ArrayList<Activity> resumed = new ArrayList<>();
+
+    @Override
+    public void onActivityCreated(Activity activity, Bundle bundle) {
+        created.add(activity);
+    }
+
+    @Override
+    public void onActivityStarted(Activity activity) {
+        started.add(activity);
+    }
+
+    @Override
+    public void onActivityResumed(Activity activity) {
+        resumed.add(activity);
+    }
+
+    @Override
+    public void onActivityPaused(Activity activity) {
+        resumed.remove(activity);
+    }
+
+    @Override
+    public void onActivityStopped(Activity activity) {
+        started.remove(activity);
+    }
+
+    @Override
+    public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { }
+
+    @Override
+    public void onActivityDestroyed(Activity activity) {
+        created.remove(activity);
+    }
+}