Merge "Add NotificationListener to launcher." into ub-launcher3-master
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index b6e5bb0..5588289 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -88,6 +88,8 @@
 
         <activity android:name="com.android.launcher3.dragndrop.AddItemActivity"
             android:theme="@android:style/Theme.DeviceDefault.Light.Dialog.Alert"
+            android:excludeFromRecents="true"
+            android:autoRemoveFromRecents="true"
             android:label="@string/action_add_to_workspace" >
             <intent-filter>
                 <action android:name="android.content.pm.action.CONFIRM_PIN_ITEM" />
diff --git a/build.gradle b/build.gradle
index 3a812a9..51ac5a1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -46,12 +46,16 @@
         androidTest {
             java.srcDirs = ['tests/src']
             res.srcDirs = ['tests/res']
-            manifest.srcFile "tests/AndroidManifest.xml"
+            manifest.srcFile "tests/AndroidManifest-common.xml"
         }
 
         aosp {
             manifest.srcFile "AndroidManifest.xml"
         }
+
+        aospAndroidTest {
+            manifest.srcFile "tests/AndroidManifest.xml"
+        }
     }
 }
 
diff --git a/res/layout/widgets_list_row_view.xml b/res/layout/widgets_list_row_view.xml
index 30a34d4..530e856 100644
--- a/res/layout/widgets_list_row_view.xml
+++ b/res/layout/widgets_list_row_view.xml
@@ -41,7 +41,7 @@
         android:paddingRight="@dimen/widget_section_horizontal_padding"
         android:paddingTop="@dimen/widget_section_vertical_padding"
         android:singleLine="true"
-        android:textColor="@color/widgets_view_section_text_color"
+        android:textColor="?android:attr/textColorPrimary"
         android:textSize="16sp"
         launcher:customShadows="false"
         launcher:deferShadowGeneration="true"
diff --git a/res/values/colors.xml b/res/values/colors.xml
index ccbae58..809fc6d 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -46,8 +46,7 @@
     <color name="spring_loaded_highlighted_panel_border_color">#FFF</color>
 
     <!-- Widgets view -->
-    <color name="widgets_view_section_text_color">#FFFFFF</color>
-    <color name="widgets_view_item_text_color">#C4C4C4</color>
+    <color name="widgets_view_item_text_color">#3B3B3B</color>
 
     <!-- Used as a fallback since colorSecondary doesn't exist pre-API 25 -->
     <color name="fallback_secondary_color">#FF37474F</color>
diff --git a/src/com/android/launcher3/AnotherWindowDropTarget.java b/src/com/android/launcher3/AnotherWindowDropTarget.java
deleted file mode 100644
index 052e5d0..0000000
--- a/src/com/android/launcher3/AnotherWindowDropTarget.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2015 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;
-
-import android.content.Context;
-import android.graphics.PointF;
-import android.graphics.Rect;
-
-/**
- * Drop target used when another window (i.e. another process) has accepted a global system drag.
- * If the accepted item was a shortcut, we delete it from Launcher.
- */
-public class AnotherWindowDropTarget implements DropTarget {
-    final Launcher mLauncher;
-
-    public AnotherWindowDropTarget (Context context) { mLauncher = Launcher.getLauncher(context); }
-
-    @Override
-    public boolean isDropEnabled() { return true; }
-
-    @Override
-    public void onDrop(DragObject dragObject) {
-        dragObject.deferDragViewCleanupPostAnimation = false;
-        LauncherModel.deleteItemFromDatabase(mLauncher, (ShortcutInfo) dragObject.dragInfo);
-    }
-
-    @Override
-    public void onDragEnter(DragObject dragObject) {}
-
-    @Override
-    public void onDragOver(DragObject dragObject) {}
-
-    @Override
-    public void onDragExit(DragObject dragObject) {}
-
-    @Override
-    public boolean acceptDrop(DragObject dragObject) {
-        return dragObject.dragInfo instanceof ShortcutInfo;
-    }
-
-    @Override
-    public void prepareAccessibilityDrop() {}
-
-    // These methods are implemented in Views
-    @Override
-    public void getHitRectRelativeToDragLayer(Rect outRect) {}
-}
diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java
index 4c88e7e..5fff2e7 100644
--- a/src/com/android/launcher3/FolderInfo.java
+++ b/src/com/android/launcher3/FolderInfo.java
@@ -93,7 +93,7 @@
     }
 
     @Override
-    void onAddToDatabase(ContentWriter writer) {
+    public void onAddToDatabase(ContentWriter writer) {
         super.onAddToDatabase(writer);
         writer.put(LauncherSettings.Favorites.TITLE, title)
                 .put(LauncherSettings.Favorites.OPTIONS, options);
diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java
index 82c7ab8..aec6c7d 100644
--- a/src/com/android/launcher3/ItemInfo.java
+++ b/src/com/android/launcher3/ItemInfo.java
@@ -166,7 +166,7 @@
     /**
      * Write the fields of this item to the DB
      */
-    void onAddToDatabase(ContentWriter writer) {
+    public void onAddToDatabase(ContentWriter writer) {
         if (screenId == Workspace.EXTRA_EMPTY_SCREEN_ID) {
             // We should never persist an item on the extra empty screen.
             throw new RuntimeException("Screen id should not be EXTRA_EMPTY_SCREEN_ID");
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index aa5b8c8..26e388d 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -49,6 +49,7 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.Parcelable;
 import android.os.Process;
 import android.os.StrictMode;
 import android.os.SystemClock;
@@ -96,6 +97,7 @@
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.dragndrop.PinItemDragListener;
 import com.android.launcher3.dynamicui.ExtractedColors;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
@@ -126,6 +128,7 @@
 import com.android.launcher3.util.ViewOnDrawExecutor;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
+import com.android.launcher3.widget.WidgetAddFlowHandler;
 import com.android.launcher3.widget.WidgetHostViewLoader;
 import com.android.launcher3.widget.WidgetsContainerView;
 
@@ -691,8 +694,9 @@
                     // Since the view was just bound, also launch the configure activity if needed
                     LauncherAppWidgetProviderInfo provider = mAppWidgetManager
                             .getLauncherAppWidgetInfo(widgetId);
-                    if (provider != null && provider.configure != null) {
-                        startRestoredWidgetReconfigActivity(provider, widgetInfo);
+                    if (provider != null) {
+                        new WidgetAddFlowHandler(provider)
+                                .startConfigActivity(this, widgetInfo, REQUEST_RECONFIGURE_APPWIDGET);
                     }
                 }
                 break;
@@ -739,7 +743,7 @@
             } else if (resultCode == RESULT_OK) {
                 addAppWidgetImpl(
                         appWidgetId, requestArgs, null,
-                        requestArgs.getWidgetProvider(this),
+                        requestArgs.getWidgetHandler(),
                         ON_ACTIVITY_RESULT_ANIMATION_DELAY);
             }
             return;
@@ -832,7 +836,7 @@
     }
 
     @Override
-    protected void onActivityResult(
+    public void onActivityResult(
             final int requestCode, final int resultCode, final Intent data) {
         handleActivityResult(requestCode, resultCode, data);
         if (mLauncherCallbacks != null) {
@@ -898,7 +902,7 @@
         if (resultCode == RESULT_OK) {
             animationType = Workspace.COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION;
             final AppWidgetHostView layout = mAppWidgetHost.createView(this, appWidgetId,
-                    requestArgs.getWidgetProvider(this));
+                    requestArgs.getWidgetHandler().getProviderInfo(this));
             boundWidget = layout;
             onCompleteRunnable = new Runnable() {
                 @Override
@@ -1777,6 +1781,14 @@
             if (mLauncherCallbacks != null) {
                 mLauncherCallbacks.onHomeIntent();
             }
+
+            Parcelable dragExtra = intent
+                    .getParcelableExtra(PinItemDragListener.EXTRA_PIN_ITEM_DRAG_LISTENER);
+            if (dragExtra instanceof PinItemDragListener) {
+                PinItemDragListener dragListener = (PinItemDragListener) dragExtra;
+                dragListener.setLauncher(this);
+                mDragLayer.setOnDragListener(dragListener);
+            }
         }
 
         if (mLauncherCallbacks != null) {
@@ -2010,7 +2022,7 @@
         }
     }
 
-    private void setWaitingForResult(PendingRequestArgs args) {
+    public void setWaitingForResult(PendingRequestArgs args) {
         boolean isLocked = isWorkspaceLocked();
         mPendingRequestArgs = args;
         if (isLocked != isWorkspaceLocked()) {
@@ -2025,24 +2037,18 @@
     }
 
     void addAppWidgetFromDropImpl(int appWidgetId, ItemInfo info, AppWidgetHostView boundWidget,
-            LauncherAppWidgetProviderInfo appWidgetInfo) {
+            WidgetAddFlowHandler addFlowHandler) {
         if (LOGD) {
             Log.d(TAG, "Adding widget from drop");
         }
-        addAppWidgetImpl(appWidgetId, info, boundWidget, appWidgetInfo, 0);
+        addAppWidgetImpl(appWidgetId, info, boundWidget, addFlowHandler, 0);
     }
 
     void addAppWidgetImpl(int appWidgetId, ItemInfo info,
-            AppWidgetHostView boundWidget, LauncherAppWidgetProviderInfo appWidgetInfo,
-            int delay) {
-        if (appWidgetInfo.configure != null) {
-            setWaitingForResult(PendingRequestArgs.forWidgetInfo(appWidgetId, appWidgetInfo, info));
+            AppWidgetHostView boundWidget, WidgetAddFlowHandler addFlowHandler, int delay) {
+        if (!addFlowHandler.startConfigActivity(this, appWidgetId, info, REQUEST_CREATE_APPWIDGET)) {
+            // If the configuration flow was not started, add the widget
 
-            // Launch over to configure widget, if needed
-            mAppWidgetManager.startConfigActivity(appWidgetInfo, appWidgetId, this,
-                    mAppWidgetHost, REQUEST_CREATE_APPWIDGET);
-        } else {
-            // Otherwise just add it
             Runnable onComplete = new Runnable() {
                 @Override
                 public void run() {
@@ -2051,7 +2057,7 @@
                             null);
                 }
             };
-            completeAddAppWidget(appWidgetId, info, boundWidget, appWidgetInfo);
+            completeAddAppWidget(appWidgetId, info, boundWidget, addFlowHandler.getProviderInfo(this));
             mWorkspace.removeExtraEmptyScreenDelayed(true, onComplete, delay, false);
         }
     }
@@ -2103,6 +2109,7 @@
     private void addAppWidgetFromDrop(PendingAddWidgetInfo info) {
         AppWidgetHostView hostView = info.boundWidget;
         int appWidgetId;
+        WidgetAddFlowHandler addFlowHandler = info.getHander();
         if (hostView != null) {
             // In the case where we've prebound the widget, we remove it from the DragLayer
             if (LOGD) {
@@ -2111,7 +2118,7 @@
             getDragLayer().removeView(hostView);
 
             appWidgetId = hostView.getAppWidgetId();
-            addAppWidgetFromDropImpl(appWidgetId, info, hostView, info.info);
+            addAppWidgetFromDropImpl(appWidgetId, info, hostView, addFlowHandler);
 
             // Clear the boundWidget so that it doesn't get destroyed.
             info.boundWidget = null;
@@ -2124,17 +2131,9 @@
             boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed(
                     appWidgetId, info.info, options);
             if (success) {
-                addAppWidgetFromDropImpl(appWidgetId, info, null, info.info);
+                addAppWidgetFromDropImpl(appWidgetId, info, null, addFlowHandler);
             } else {
-                setWaitingForResult(PendingRequestArgs.forWidgetInfo(appWidgetId, info.info, info));
-                Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND);
-                intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
-                intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.componentName);
-                intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE,
-                        info.info.getUser());
-                // TODO: we need to make sure that this accounts for the options bundle.
-                // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options);
-                startActivityForResult(intent, REQUEST_BIND_APPWIDGET);
+                addFlowHandler.startBindFlow(this, appWidgetId, info, REQUEST_BIND_APPWIDGET);
             }
         }
     }
@@ -2350,30 +2349,22 @@
 
         final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag();
         if (v.isReadyForClickSetup()) {
+            LauncherAppWidgetProviderInfo appWidgetInfo =
+                    mAppWidgetManager.findProvider(info.providerName, info.user);
+            if (appWidgetInfo == null) {
+                return;
+            }
+            WidgetAddFlowHandler addFlowHandler = new WidgetAddFlowHandler(appWidgetInfo);
+
             if (info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
                 if (!info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) {
                     // This should not happen, as we make sure that an Id is allocated during bind.
                     return;
                 }
-                LauncherAppWidgetProviderInfo appWidgetInfo =
-                        mAppWidgetManager.findProvider(info.providerName, info.user);
-                if (appWidgetInfo != null) {
-                    setWaitingForResult(PendingRequestArgs
-                            .forWidgetInfo(info.appWidgetId, appWidgetInfo, info));
-
-                    Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND);
-                    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, info.appWidgetId);
-                    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, appWidgetInfo.provider);
-                    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE,
-                            appWidgetInfo.getUser());
-                    startActivityForResult(intent, REQUEST_BIND_PENDING_APPWIDGET);
-                }
+                addFlowHandler.startBindFlow(this, info.appWidgetId, info,
+                        REQUEST_BIND_PENDING_APPWIDGET);
             } else {
-                LauncherAppWidgetProviderInfo appWidgetInfo =
-                        mAppWidgetManager.getLauncherAppWidgetInfo(info.appWidgetId);
-                if (appWidgetInfo != null) {
-                    startRestoredWidgetReconfigActivity(appWidgetInfo, info);
-                }
+                addFlowHandler.startConfigActivity(this, info, REQUEST_RECONFIGURE_APPWIDGET);
             }
         } else {
             final String packageName = info.providerName.getPackageName();
@@ -2381,13 +2372,6 @@
         }
     }
 
-    private void startRestoredWidgetReconfigActivity(
-            LauncherAppWidgetProviderInfo provider, LauncherAppWidgetInfo info) {
-        setWaitingForResult(PendingRequestArgs.forWidgetInfo(info.appWidgetId, provider, info));
-        mAppWidgetManager.startConfigActivity(provider,
-                info.appWidgetId, this, mAppWidgetHost, REQUEST_RECONFIGURE_APPWIDGET);
-    }
-
     /**
      * Event handler for the "grid" button that appears on the home screen, which
      * enters all apps mode.
diff --git a/src/com/android/launcher3/LauncherAppWidgetHostView.java b/src/com/android/launcher3/LauncherAppWidgetHostView.java
index 1429df5..61b8a7a 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/LauncherAppWidgetHostView.java
@@ -135,8 +135,6 @@
 
         // The provider info or the views might have changed.
         checkIfAutoAdvance();
-
-        mIsScrollable = checkScrollableRecursively(this);
     }
 
     private boolean checkScrollableRecursively(ViewGroup viewGroup) {
@@ -383,6 +381,8 @@
                 }
             });
         }
+
+        mIsScrollable = checkScrollableRecursively(this);
     }
 
     @Override
diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/LauncherAppWidgetInfo.java
index b68a64b..1e0f285 100644
--- a/src/com/android/launcher3/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/LauncherAppWidgetInfo.java
@@ -65,7 +65,7 @@
     /**
      * Indicates that the widget hasn't been instantiated yet.
      */
-    static final int NO_ID = -1;
+    public static final int NO_ID = -1;
 
     /**
      * Indicates that this is a locally defined widget and hence has no system allocated id.
@@ -76,7 +76,7 @@
      * Identifier for this widget when talking with
      * {@link android.appwidget.AppWidgetManager} for updates.
      */
-    int appWidgetId = NO_ID;
+    public int appWidgetId = NO_ID;
 
     public ComponentName providerName;
 
@@ -126,7 +126,7 @@
     }
 
     @Override
-    void onAddToDatabase(ContentWriter writer) {
+    public void onAddToDatabase(ContentWriter writer) {
         super.onAddToDatabase(writer);
         writer.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId)
                 .put(LauncherSettings.Favorites.APPWIDGET_PROVIDER, providerName.flattenToString())
diff --git a/src/com/android/launcher3/MainThreadExecutor.java b/src/com/android/launcher3/MainThreadExecutor.java
index 866b17c..4ca0a59 100644
--- a/src/com/android/launcher3/MainThreadExecutor.java
+++ b/src/com/android/launcher3/MainThreadExecutor.java
@@ -16,65 +16,18 @@
 
 package com.android.launcher3;
 
-import android.os.Handler;
 import android.os.Looper;
 
-import java.util.List;
-import java.util.concurrent.AbstractExecutorService;
-import java.util.concurrent.TimeUnit;
+import com.android.launcher3.util.LooperExecuter;
 
 /**
  * An executor service that executes its tasks on the main thread.
  *
  * Shutting down this executor is not supported.
  */
-public class MainThreadExecutor extends AbstractExecutorService {
+public class MainThreadExecutor extends LooperExecuter {
 
-    private Handler mHandler = new Handler(Looper.getMainLooper());
-
-    @Override
-    public void execute(Runnable runnable) {
-        if (Looper.getMainLooper() == Looper.myLooper()) {
-            runnable.run();
-        } else {
-            mHandler.post(runnable);
-        }
-    }
-
-    /**
-     * Not supported and throws an exception when used.
-     */
-    @Override
-    @Deprecated
-    public void shutdown() {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * Not supported and throws an exception when used.
-     */
-    @Override
-    @Deprecated
-    public List<Runnable> shutdownNow() {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public boolean isShutdown() {
-        return false;
-    }
-
-    @Override
-    public boolean isTerminated() {
-        return false;
-    }
-
-    /**
-     * Not supported and throws an exception when used.
-     */
-    @Override
-    @Deprecated
-    public boolean awaitTermination(long l, TimeUnit timeUnit) throws InterruptedException {
-        throw new UnsupportedOperationException();
+    public MainThreadExecutor() {
+        super(Looper.getMainLooper());
     }
 }
diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java
index 1a42395..b35dcb7 100644
--- a/src/com/android/launcher3/ShortcutInfo.java
+++ b/src/com/android/launcher3/ShortcutInfo.java
@@ -161,7 +161,7 @@
     }
 
     @Override
-    void onAddToDatabase(ContentWriter writer) {
+    public void onAddToDatabase(ContentWriter writer) {
         super.onAddToDatabase(writer);
         writer.put(LauncherSettings.BaseLauncherColumns.TITLE, title)
                 .put(LauncherSettings.BaseLauncherColumns.INTENT, getIntent())
diff --git a/src/com/android/launcher3/compat/PinItemRequestCompat.java b/src/com/android/launcher3/compat/PinItemRequestCompat.java
index 74d7765..481310a 100644
--- a/src/com/android/launcher3/compat/PinItemRequestCompat.java
+++ b/src/com/android/launcher3/compat/PinItemRequestCompat.java
@@ -21,13 +21,14 @@
 import android.content.Intent;
 import android.content.pm.ShortcutInfo;
 import android.os.Bundle;
+import android.os.Parcel;
 import android.os.Parcelable;
 
 /**
  * A wrapper around platform implementation of PinItemRequestCompat until the
  * updated SDK is available.
  */
-public class PinItemRequestCompat {
+public class PinItemRequestCompat implements Parcelable {
 
     public static final String EXTRA_PIN_ITEM_REQUEST = "android.content.pm.extra.PIN_ITEM_REQUEST";
 
@@ -83,6 +84,32 @@
         }
     }
 
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int i) {
+        parcel.writeParcelable(mObject, i);
+    }
+
+    public Intent toIntent() {
+        return new Intent().putExtra(EXTRA_PIN_ITEM_REQUEST, mObject);
+    }
+
+    public static final Parcelable.Creator<PinItemRequestCompat> CREATOR =
+            new Parcelable.Creator<PinItemRequestCompat>() {
+                public PinItemRequestCompat createFromParcel(Parcel source) {
+                    Parcelable object = source.readParcelable(null);
+                    return new PinItemRequestCompat(object);
+                }
+
+                public PinItemRequestCompat[] newArray(int size) {
+                    return new PinItemRequestCompat[size];
+                }
+            };
+
     public static PinItemRequestCompat getPinItemRequest(Intent intent) {
         Parcelable extra = intent.getParcelableExtra(EXTRA_PIN_ITEM_REQUEST);
         return extra == null ? null : new PinItemRequestCompat(extra);
diff --git a/src/com/android/launcher3/compat/ShortcutConfigActivityInfo.java b/src/com/android/launcher3/compat/ShortcutConfigActivityInfo.java
index ece7759..1cfbd20 100644
--- a/src/com/android/launcher3/compat/ShortcutConfigActivityInfo.java
+++ b/src/com/android/launcher3/compat/ShortcutConfigActivityInfo.java
@@ -35,6 +35,7 @@
 import android.widget.Toast;
 
 import com.android.launcher3.IconCache;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 
@@ -67,7 +68,7 @@
 
     public abstract Drawable getFullResIcon(IconCache cache);
 
-    public boolean startConfigActivity(Activity activity, int requestCode) {
+    public boolean startConfigActivity(Launcher activity, int requestCode) {
         Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT)
                 .setComponent(getComponent());
         try {
@@ -136,7 +137,7 @@
         }
 
         @Override
-        public boolean startConfigActivity(Activity activity, int requestCode) {
+        public boolean startConfigActivity(Launcher activity, int requestCode) {
             if (getUser().equals(Process.myUserHandle())) {
                 return super.startConfigActivity(activity, requestCode);
             }
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 6c6f141..423fdab 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -17,12 +17,23 @@
 package com.android.launcher3.dragndrop;
 
 import android.annotation.TargetApi;
+import android.app.ActivityOptions;
 import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetManager;
+import android.content.ClipData;
+import android.content.ClipDescription;
 import android.content.Intent;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
 import android.os.Build;
 import android.os.Bundle;
+import android.util.Log;
+import android.view.MotionEvent;
 import android.view.View;
+import android.view.View.*;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.InstallShortcutReceiver;
@@ -38,13 +49,18 @@
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.WidgetHostViewLoader;
+import com.android.launcher3.widget.WidgetImageView;
 
 @TargetApi(Build.VERSION_CODES.N_MR1)
-public class AddItemActivity extends BaseActivity {
+public class AddItemActivity extends BaseActivity implements OnLongClickListener, OnTouchListener {
+
+    private static final int SHADOW_SIZE = 10;
 
     private static final int REQUEST_BIND_APPWIDGET = 1;
     private static final String STATE_EXTRA_WIDGET_ID = "state.widget.id";
 
+    private final PointF mLastTouchPos = new PointF();
+
     private PinItemRequestCompat mRequest;
     private LauncherAppState mApp;
     private InvariantDeviceProfile mIdp;
@@ -86,11 +102,54 @@
                 finish();
             }
         }
+
+        mWidgetCell.setOnTouchListener(this);
+        mWidgetCell.setOnLongClickListener(this);
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        mLastTouchPos.set(motionEvent.getX(), motionEvent.getY());
+        return false;
+    }
+
+    @Override
+    public boolean onLongClick(View view) {
+        // Find the position of the preview relative to the touch location.
+        WidgetImageView img = mWidgetCell.getWidgetView();
+        Rect bounds = img.getBitmapBounds();
+        bounds.offset(img.getLeft() - (int) mLastTouchPos.x, img.getTop() - (int) mLastTouchPos.y);
+
+        // Start home and pass the draw request params
+        PinItemDragListener listener = new PinItemDragListener(mRequest, bounds);
+        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setPackage(getPackageName())
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .putExtra(PinItemDragListener.EXTRA_PIN_ITEM_DRAG_LISTENER, listener);
+        startActivity(homeIntent,
+                ActivityOptions.makeCustomAnimation(this, 0, android.R.anim.fade_out).toBundle());
+
+        // Start a system drag and drop. We use a transparent bitmap as preview for system drag
+        // as the preview is handled internally by launcher.
+        ClipDescription description = new ClipDescription("", new String[]{listener.getMimeType()});
+        ClipData data = new ClipData(description, new ClipData.Item(""));
+        view.startDragAndDrop(data, new DragShadowBuilder(view) {
+
+            @Override
+            public void onDrawShadow(Canvas canvas) { }
+
+            @Override
+            public void onProvideShadowMetrics(Point outShadowSize, Point outShadowTouchPoint) {
+                outShadowSize.set(SHADOW_SIZE, SHADOW_SIZE);
+                outShadowTouchPoint.set(SHADOW_SIZE / 2, SHADOW_SIZE / 2);
+            }
+        }, null, View.DRAG_FLAG_GLOBAL);
+        return false;
     }
 
     private void setupShortcut() {
-        WidgetItem item = new WidgetItem(new PinShortcutRequestActivityInfo(
-                mRequest.getShortcutInfo(), this));
+        WidgetItem item = new WidgetItem(new PinShortcutRequestActivityInfo(mRequest, this));
         mWidgetCell.applyFromCellItem(item, mApp.getWidgetCache());
         mWidgetCell.ensurePreview();
     }
diff --git a/src/com/android/launcher3/dragndrop/AnotherWindowDragSource.java b/src/com/android/launcher3/dragndrop/AnotherWindowDragSource.java
deleted file mode 100644
index 1623010..0000000
--- a/src/com/android/launcher3/dragndrop/AnotherWindowDragSource.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2016 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.dragndrop;
-
-import android.content.Context;
-import android.view.View;
-
-import com.android.launcher3.DragSource;
-import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
-
-/**
- * DragSource used when the drag started at another window.
- */
-public class AnotherWindowDragSource implements DragSource {
-
-    private final Context mContext;
-
-    AnotherWindowDragSource(Context context) {
-        mContext = context;
-    }
-
-    @Override
-    public boolean supportsAppInfoDropTarget() {
-        return false;
-    }
-
-    @Override
-    public boolean supportsDeleteDropTarget() {
-        return false;
-    }
-
-    @Override
-    public float getIntrinsicIconScaleFactor() {
-        return 1;
-    }
-
-    @Override
-    public void onDropCompleted(View target, DragObject d,
-            boolean isFlingToDelete, boolean success) {
-        if (!success) {
-            Launcher.getLauncher(mContext).exitSpringLoadedDragModeDelayed(false, 0, null);
-        }
-
-    }
-
-    @Override
-    public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
-        // TODO: Probably log something
-    }
-}
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 745776d..80c2860 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -36,7 +36,6 @@
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutInfo;
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.util.TouchController;
@@ -447,7 +446,8 @@
     /**
      * Call this from a drag source view.
      */
-    public boolean onDragEvent(DragEvent event) {
+    public boolean onDragEvent(long dragStartTime, DragEvent event) {
+        mFlingToDeleteHelper.recordDragEvent(dragStartTime, event);
         return mDragDriver != null && mDragDriver.onDragEvent(event);
     }
 
diff --git a/src/com/android/launcher3/dragndrop/DragDriver.java b/src/com/android/launcher3/dragndrop/DragDriver.java
index a90cfff..65c0f29 100644
--- a/src/com/android/launcher3/dragndrop/DragDriver.java
+++ b/src/com/android/launcher3/dragndrop/DragDriver.java
@@ -16,20 +16,13 @@
 
 package com.android.launcher3.dragndrop;
 
-import android.content.ClipData;
-import android.content.ClipDescription;
 import android.content.Context;
-import android.content.Intent;
 import android.view.DragEvent;
 import android.view.MotionEvent;
 
 import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.InstallShortcutReceiver;
-import com.android.launcher3.ShortcutInfo;
 import com.android.launcher3.Utilities;
 
-import java.util.ArrayList;
-
 /**
  * Base class for driving a drag/drop operation.
  */
@@ -107,7 +100,6 @@
     private final DragObject mDragObject;
     private final Context mContext;
 
-    boolean mReceivedDropEvent = false;
     float mLastX = 0;
     float mLastY = 0;
 
@@ -149,65 +141,21 @@
             case DragEvent.ACTION_DROP:
                 mLastX = event.getX();
                 mLastY = event.getY();
-                mReceivedDropEvent =
-                        updateInfoFromClipData(event.getClipData(), event.getClipDescription());
-                return mReceivedDropEvent;
-
+                mEventListener.onDriverDragMove(event.getX(), event.getY());
+                mEventListener.onDriverDragEnd(mLastX, mLastY);
+                return true;
             case DragEvent.ACTION_DRAG_EXITED:
                 mEventListener.onDriverDragExitWindow();
                 return true;
 
             case DragEvent.ACTION_DRAG_ENDED:
-                if (mReceivedDropEvent) {
-                    mEventListener.onDriverDragEnd(mLastX, mLastY);
-                } else {
-                    mEventListener.onDriverDragCancel();
-                }
+                mEventListener.onDriverDragCancel();
                 return true;
 
             default:
                 return false;
         }
     }
-
-    private boolean updateInfoFromClipData(ClipData data, ClipDescription desc) {
-        if (data == null) {
-            return false;
-        }
-        ArrayList<Intent> intents = new ArrayList<>();
-        int itemCount = data.getItemCount();
-        for (int i = 0; i < itemCount; i++) {
-            Intent intent = data.getItemAt(i).getIntent();
-            if (intent == null) {
-                continue;
-            }
-
-            // Give preference to shortcut intents.
-            if (!Intent.ACTION_CREATE_SHORTCUT.equals(intent.getAction())) {
-                intents.add(intent);
-                continue;
-            }
-            ShortcutInfo info = InstallShortcutReceiver.fromShortcutIntent(mContext, intent);
-            if (info != null) {
-                mDragObject.dragInfo = info;
-                return true;
-            }
-            return true;
-        }
-
-        // Try creating shortcuts just using the intent and label
-        Intent fullIntent = new Intent().putExtra(Intent.EXTRA_SHORTCUT_NAME, desc.getLabel());
-        for (Intent intent : intents) {
-            fullIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent);
-            ShortcutInfo info = InstallShortcutReceiver.fromShortcutIntent(mContext, fullIntent);
-            if (info != null) {
-                mDragObject.dragInfo = info;
-                return true;
-            }
-        }
-
-        return false;
-    }
 }
 
 /**
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 4279cc3..de416e3 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -21,21 +21,13 @@
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.annotation.TargetApi;
-import android.content.ClipDescription;
 import android.content.Context;
-import android.content.Intent;
 import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
 import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.Region;
-import android.os.Build;
 import android.util.AttributeSet;
-import android.view.DragEvent;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -59,9 +51,7 @@
 import com.android.launcher3.PinchToOverviewListener;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutAndWidgetContainer;
-import com.android.launcher3.ShortcutInfo;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.Workspace;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.Folder;
@@ -373,49 +363,6 @@
         return false;
     }
 
-    @TargetApi(Build.VERSION_CODES.N)
-    private void handleSystemDragStart(DragEvent event) {
-        if (!FeatureFlags.LAUNCHER3_USE_SYSTEM_DRAG_DRIVER || !Utilities.ATLEAST_NOUGAT) {
-            return;
-        }
-        if (mLauncher.isWorkspaceLocked()) {
-            return;
-        }
-
-        ClipDescription description = event.getClipDescription();
-        if (!description.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) {
-            return;
-        }
-        ShortcutInfo info = new ShortcutInfo();
-        // Set a dummy intent until we get the final value
-        info.intent = new Intent();
-
-        // Since we are not going through the workspace for starting the drag, set drag related
-        // information on the workspace before starting the drag.
-        ExternalDragPreviewProvider previewProvider =
-                new ExternalDragPreviewProvider(mLauncher, info);
-        mLauncher.getWorkspace().prepareDragWithProvider(previewProvider);
-
-        DragOptions options = new DragOptions();
-        options.systemDndStartPoint = new Point((int) event.getX(), (int) event.getY());
-
-        int halfPadding = previewProvider.previewPadding / 2;
-        mDragController.startDrag(
-                Bitmap.createBitmap(1, 1, Config.ARGB_8888),
-                0, 0,
-                new AnotherWindowDragSource(mLauncher), info,
-                new Point(- halfPadding, halfPadding),
-                previewProvider.getPreviewBounds(), 1f, options);
-    }
-
-    @Override
-    public boolean onDragEvent (DragEvent event) {
-        if (event.getAction() == DragEvent.ACTION_DRAG_STARTED) {
-            handleSystemDragStart(event);
-        }
-        return mDragController.onDragEvent(event);
-    }
-
     /**
      * Determine the rect of the descendant in this DragLayer's coordinates
      *
diff --git a/src/com/android/launcher3/dragndrop/ExternalDragPreviewProvider.java b/src/com/android/launcher3/dragndrop/ExternalDragPreviewProvider.java
deleted file mode 100644
index e558487..0000000
--- a/src/com/android/launcher3/dragndrop/ExternalDragPreviewProvider.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2016 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.dragndrop;
-
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.graphics.DragPreviewProvider;
-import com.android.launcher3.graphics.HolographicOutlineHelper;
-
-/**
- * Extension of {@link DragPreviewProvider} which provides a dummy outline when drag starts from
- * a different window.
- * It just draws an empty circle to a placeholder outline.
- */
-public class ExternalDragPreviewProvider extends DragPreviewProvider {
-
-    private final Launcher mLauncher;
-    private final ItemInfo mAddInfo;
-
-    private final int[] mOutlineSize;
-
-    public ExternalDragPreviewProvider(Launcher launcher, ItemInfo addInfo) {
-        super(null, launcher);
-        mLauncher = launcher;
-        mAddInfo = addInfo;
-
-        mOutlineSize = mLauncher.getWorkspace().estimateItemSize(mAddInfo, false, false);
-    }
-
-    public Rect getPreviewBounds() {
-        Rect rect = new Rect();
-        DeviceProfile dp = mLauncher.getDeviceProfile();
-        rect.left = blurSizeOutline / 2;
-        rect.top = (mOutlineSize[1] - dp.cellHeightPx) / 2;
-        rect.right = rect.left + dp.iconSizePx;
-        rect.bottom = rect.top + dp.iconSizePx;
-        return rect;
-    }
-
-    @Override
-    public Bitmap createDragOutline(Canvas canvas) {
-        final Bitmap b = Bitmap.createBitmap(mOutlineSize[0], mOutlineSize[1], Bitmap.Config.ALPHA_8);
-        canvas.setBitmap(b);
-
-        Paint paint = new Paint();
-        paint.setColor(Color.WHITE);
-        paint.setStyle(Paint.Style.FILL);
-
-        // Use 0.9f times the radius for the actual circle to account for icon normalization.
-        float radius = getPreviewBounds().width() * 0.5f;
-        canvas.drawCircle(blurSizeOutline / 2 + radius,
-                blurSizeOutline / 2 + radius, radius * 0.9f, paint);
-
-        HolographicOutlineHelper.getInstance(mLauncher).applyExpensiveOutlineWithBlur(b, canvas);
-        canvas.setBitmap(null);
-        return b;
-    }
-}
diff --git a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
index a2aa27d..e794744 100644
--- a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
+++ b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
@@ -17,6 +17,8 @@
 package com.android.launcher3.dragndrop;
 
 import android.graphics.PointF;
+import android.os.SystemClock;
+import android.view.DragEvent;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
@@ -53,6 +55,31 @@
         mVelocityTracker.addMovement(ev);
     }
 
+    /**
+     * Same as {@link #recordMotionEvent}. It creates a temporary {@link MotionEvent} object
+     * using {@param event} for tracking velocity.
+     */
+    public void recordDragEvent(long dragStartTime, DragEvent event) {
+        final int motionAction;
+        switch (event.getAction()) {
+            case DragEvent.ACTION_DRAG_STARTED:
+                motionAction = MotionEvent.ACTION_DOWN;
+                break;
+            case DragEvent.ACTION_DRAG_LOCATION:
+                motionAction = MotionEvent.ACTION_MOVE;
+                break;
+            case DragEvent.ACTION_DRAG_ENDED:
+                motionAction = MotionEvent.ACTION_UP;
+                break;
+            default:
+                return;
+        }
+        MotionEvent emulatedEvent = MotionEvent.obtain(dragStartTime, SystemClock.uptimeMillis(),
+                motionAction, event.getX(), event.getY(), 0);
+        recordMotionEvent(emulatedEvent);
+        emulatedEvent.recycle();
+    }
+
     public void releaseVelocityTracker() {
         if (mVelocityTracker != null) {
             mVelocityTracker.recycle();
diff --git a/src/com/android/launcher3/dragndrop/PinItemDragListener.java b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
new file mode 100644
index 0000000..1a99cc8
--- /dev/null
+++ b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
@@ -0,0 +1,259 @@
+/*
+ * 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.dragndrop;
+
+import android.content.ClipDescription;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.DragEvent;
+import android.view.View;
+
+import com.android.launcher3.DeleteDropTarget;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.PendingAddItemInfo;
+import com.android.launcher3.compat.PinItemRequestCompat;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.graphics.LauncherIcons;
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.widget.PendingAddShortcutInfo;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
+import com.android.launcher3.widget.PendingItemPreviewProvider;
+
+import java.util.UUID;
+
+/**
+ * {@link DragSource} for handling drop from from a different window. This object is initialized
+ * in the source window and is passed on to the Launcher activity as an Intent extra.
+ */
+public class PinItemDragListener implements Parcelable, View.OnDragListener, DragSource {
+
+    private static final String TAG = "PinItemDragListener";
+
+    private static final String MIME_TYPE_PREFIX = "com.android.launcher3.drag_and_drop/";
+    public static final String EXTRA_PIN_ITEM_DRAG_LISTENER = "pin_item_drag_listener";
+
+    private final PinItemRequestCompat mRequest;
+
+    // Position of preview relative to the touch location
+    private final Rect mPreviewRect;
+
+    // Randomly generated id used to verify the drag event.
+    private final String mId;
+
+    private Launcher mLauncher;
+    private DragController mDragController;
+    private long mDragStartTime;
+
+    public PinItemDragListener(PinItemRequestCompat request, Rect previewRect) {
+        mRequest = request;
+        mPreviewRect = previewRect;
+        mId = UUID.randomUUID().toString();
+    }
+
+    private PinItemDragListener(Parcel parcel) {
+        mRequest = PinItemRequestCompat.CREATOR.createFromParcel(parcel);
+        mPreviewRect = Rect.CREATOR.createFromParcel(parcel);
+        mId = parcel.readString();
+    }
+
+    public String getMimeType() {
+        return MIME_TYPE_PREFIX + mId;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int i) {
+        mRequest.writeToParcel(parcel, i);
+        mPreviewRect.writeToParcel(parcel, i);
+        parcel.writeString(mId);
+    }
+
+    public void setLauncher(Launcher launcher) {
+        mLauncher = launcher;
+        mDragController = launcher.getDragController();
+    }
+
+    @Override
+    public boolean onDrag(View view, DragEvent event) {
+        if (mLauncher == null || mDragController == null) {
+            postCleanup();
+            return false;
+        }
+        if (event.getAction() == DragEvent.ACTION_DRAG_STARTED) {
+            if (onDragStart(event)) {
+                return true;
+            } else {
+                postCleanup();
+                return false;
+            }
+        }
+        return mDragController.onDragEvent(mDragStartTime, event);
+    }
+
+    private boolean onDragStart(DragEvent event) {
+        if (!mRequest.isValid()) {
+            return false;
+        }
+        ClipDescription desc =  event.getClipDescription();
+        if (desc == null || !desc.hasMimeType(getMimeType())) {
+            Log.e(TAG, "Someone started a dragAndDrop before us.");
+            return false;
+        }
+
+        if (mLauncher.isWorkspaceLocked()) {
+            // TODO: implement wait
+            return false;
+        }
+
+        final PendingAddItemInfo item;
+        final Bitmap preview;
+
+        Point dragShift = new Point(mPreviewRect.left, mPreviewRect.top);
+        if (mRequest.getRequestType() == PinItemRequestCompat.REQUEST_TYPE_SHORTCUT) {
+            item = new PendingAddShortcutInfo(
+                    new PinShortcutRequestActivityInfo(mRequest, mLauncher));
+
+            ShortcutInfoCompat compat = new ShortcutInfoCompat(mRequest.getShortcutInfo());
+            Bitmap icon = LauncherIcons.createShortcutIcon(compat, mLauncher, false /* badged */);
+
+            // Create a preview same as the workspace cell size and draw the icon at the
+            // appropriate position.
+            int[] size = mLauncher.getWorkspace().estimateItemSize(item, true, false);
+            preview = Bitmap.createBitmap(size[0], size[1], Bitmap.Config.ARGB_8888);
+            Canvas c = new Canvas(preview);
+            DeviceProfile dp = mLauncher.getDeviceProfile();
+            c.drawBitmap(icon, (size[0] - icon.getWidth()) / 2,
+                    (size[1] - icon.getHeight() - dp.iconTextSizePx - dp.iconDrawablePaddingPx) / 2,
+                    new Paint(Paint.FILTER_BITMAP_FLAG));
+        } else {
+            PendingAddWidgetInfo info = new PendingAddWidgetInfo(
+                    LauncherAppWidgetProviderInfo.fromProviderInfo(
+                            mLauncher, mRequest.getAppWidgetProviderInfo(mLauncher)));
+            int[] size = mLauncher.getWorkspace().estimateItemSize(info, true, false);
+
+            float minScale = 1.25f;
+            int maxWidth = Math.min((int) (mPreviewRect.width() * minScale), size[0]);
+            int[] previewSizeBeforeScale = new int[1];
+            preview = LauncherAppState.getInstance(mLauncher).getWidgetCache()
+                    .generateWidgetPreview(mLauncher, info.info, maxWidth, null,
+                            previewSizeBeforeScale);
+
+            dragShift.offset(
+                    (mPreviewRect.width() - preview.getWidth()) / 2,
+                    (mPreviewRect.height() - preview.getHeight()) / 2);
+            item = info;
+        }
+
+        PendingItemPreviewProvider previewProvider =
+                new PendingItemPreviewProvider(new View(mLauncher), item, preview);
+
+        // Since we are not going through the workspace for starting the drag, set drag related
+        // information on the workspace before starting the drag.
+        mLauncher.getWorkspace().prepareDragWithProvider(previewProvider);
+
+        Point downPos = new Point((int) event.getX(), (int) event.getY());
+        DragOptions options = new DragOptions();
+        options.systemDndStartPoint = downPos;
+
+        int x = downPos.x + dragShift.x;
+        int y = downPos.y + dragShift.y;
+        mDragController.startDrag(
+                preview, x, y, this, item, null, null, 1f, options);
+        mDragStartTime = SystemClock.uptimeMillis();
+        return true;
+    }
+
+    @Override
+    public boolean supportsAppInfoDropTarget() {
+        return false;
+    }
+
+    @Override
+    public boolean supportsDeleteDropTarget() {
+        return false;
+    }
+
+    @Override
+    public float getIntrinsicIconScaleFactor() {
+        return 1f;
+    }
+
+    @Override
+    public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
+            boolean success) {
+        if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() &&
+                !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) {
+            // Exit spring loaded mode if we have not successfully dropped or have not handled the
+            // drop in Workspace
+            mLauncher.exitSpringLoadedDragModeDelayed(true,
+                    Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
+        }
+        postCleanup();
+    }
+
+    @Override
+    public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
+            LauncherLogProto.Target targetParent) {
+        // TODO: We should probably log something
+    }
+
+    private void postCleanup() {
+        new Handler(Looper.getMainLooper()).post(new Runnable() {
+            @Override
+            public void run() {
+                removeListener();
+            }
+        });
+    }
+
+    public void removeListener() {
+        if (mLauncher != null) {
+            mLauncher.getDragLayer().setOnDragListener(null);
+        }
+    }
+
+    public static final Parcelable.Creator<PinItemDragListener> CREATOR =
+            new Parcelable.Creator<PinItemDragListener>() {
+                public PinItemDragListener createFromParcel(Parcel source) {
+                    return new PinItemDragListener(source);
+                }
+
+                public PinItemDragListener[] newArray(int size) {
+                    return new PinItemDragListener[size];
+                }
+            };
+}
diff --git a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
index d1f878a..2121b43 100644
--- a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
+++ b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
@@ -26,7 +26,9 @@
 import android.os.Build;
 
 import com.android.launcher3.IconCache;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.compat.PinItemRequestCompat;
 import com.android.launcher3.compat.ShortcutConfigActivityInfo;
 
 /**
@@ -40,12 +42,15 @@
     // actual existing class.
     private static final String DUMMY_COMPONENT_CLASS = "pinned-shortcut";
 
+    private final PinItemRequestCompat mRequest;
     private final ShortcutInfo mInfo;
     private final Context mContext;
 
-    public PinShortcutRequestActivityInfo(ShortcutInfo info, Context context) {
-        super(new ComponentName(info.getPackage(), DUMMY_COMPONENT_CLASS), info.getUserHandle());
-        mInfo = info;
+    public PinShortcutRequestActivityInfo(PinItemRequestCompat request, Context context) {
+        super(new ComponentName(request.getShortcutInfo().getPackage(), DUMMY_COMPONENT_CLASS),
+                request.getShortcutInfo().getUserHandle());
+        mRequest = request;
+        mInfo = request.getShortcutInfo();
         mContext = context;
     }
 
@@ -61,8 +66,9 @@
     }
 
     @Override
-    public boolean startConfigActivity(Activity activity, int requestCode) {
-        throw new RuntimeException("Not supported");
+    public boolean startConfigActivity(Launcher activity, int requestCode) {
+        activity.onActivityResult(requestCode, Activity.RESULT_OK, mRequest.toIntent());
+        return true;
     }
 
     @Override
diff --git a/src/com/android/launcher3/util/LooperExecuter.java b/src/com/android/launcher3/util/LooperExecuter.java
new file mode 100644
index 0000000..4db999b
--- /dev/null
+++ b/src/com/android/launcher3/util/LooperExecuter.java
@@ -0,0 +1,81 @@
+/*
+ * 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.os.Handler;
+import android.os.Looper;
+
+import java.util.List;
+import java.util.concurrent.AbstractExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Extension of {@link AbstractExecutorService} which executed on a provided looper.
+ */
+public class LooperExecuter extends AbstractExecutorService {
+
+    private final Handler mHandler;
+
+    public LooperExecuter(Looper looper) {
+        mHandler = new Handler(looper);
+    }
+
+    @Override
+    public void execute(Runnable runnable) {
+        if (mHandler.getLooper() == Looper.myLooper()) {
+            runnable.run();
+        } else {
+            mHandler.post(runnable);
+        }
+    }
+
+    /**
+     * Not supported and throws an exception when used.
+     */
+    @Override
+    @Deprecated
+    public void shutdown() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Not supported and throws an exception when used.
+     */
+    @Override
+    @Deprecated
+    public List<Runnable> shutdownNow() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isShutdown() {
+        return false;
+    }
+
+    @Override
+    public boolean isTerminated() {
+        return false;
+    }
+
+    /**
+     * Not supported and throws an exception when used.
+     */
+    @Override
+    @Deprecated
+    public boolean awaitTermination(long l, TimeUnit timeUnit) throws InterruptedException {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/src/com/android/launcher3/util/PendingRequestArgs.java b/src/com/android/launcher3/util/PendingRequestArgs.java
index 9452fbd..538e1df 100644
--- a/src/com/android/launcher3/util/PendingRequestArgs.java
+++ b/src/com/android/launcher3/util/PendingRequestArgs.java
@@ -15,15 +15,13 @@
  */
 package com.android.launcher3.util;
 
-import android.appwidget.AppWidgetProviderInfo;
 import android.content.ContentValues;
-import android.content.Context;
 import android.content.Intent;
 import android.os.Parcel;
 import android.os.Parcelable;
 
 import com.android.launcher3.ItemInfo;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.WidgetAddFlowHandler;
 
 /**
  * Utility class to store information regarding a pending request made by launcher. This information
@@ -58,12 +56,7 @@
 
         mArg1 = parcel.readInt();
         mObjectType = parcel.readInt();
-        if (parcel.readInt() != 0) {
-            mObject = (mObjectType == TYPE_INTENT ? Intent.CREATOR : AppWidgetProviderInfo.CREATOR)
-                    .createFromParcel(parcel);
-        } else {
-            mObject = null;
-        }
+        mObject = parcel.readParcelable(null);
     }
 
     @Override
@@ -79,18 +72,11 @@
 
         dest.writeInt(mArg1);
         dest.writeInt(mObjectType);
-        if (mObject != null) {
-            dest.writeInt(1);
-            mObject.writeToParcel(dest, flags);
-        } else {
-            dest.writeInt(0);
-        }
+        dest.writeParcelable(mObject, flags);
     }
 
-    public LauncherAppWidgetProviderInfo getWidgetProvider(Context context) {
-        return mObjectType == TYPE_APP_WIDGET ?
-                LauncherAppWidgetProviderInfo.fromProviderInfo(
-                        context, (AppWidgetProviderInfo) mObject) : null;
+    public WidgetAddFlowHandler getWidgetHandler() {
+        return mObjectType == TYPE_APP_WIDGET ? (WidgetAddFlowHandler) mObject : null;
     }
 
     public int getWidgetId() {
@@ -106,8 +92,9 @@
     }
 
     public static PendingRequestArgs forWidgetInfo(
-            int appWidgetId, AppWidgetProviderInfo widgetInfo, ItemInfo info) {
-        PendingRequestArgs args = new PendingRequestArgs(appWidgetId, TYPE_APP_WIDGET, widgetInfo);
+            int appWidgetId, WidgetAddFlowHandler widgetHandler, ItemInfo info) {
+        PendingRequestArgs args =
+                new PendingRequestArgs(appWidgetId, TYPE_APP_WIDGET, widgetHandler);
         args.copyFrom(info);
         return args;
     }
diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
index 7968684..23e2f92 100644
--- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
+++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
@@ -51,4 +51,8 @@
         minSpanX = i.minSpanX;
         minSpanY = i.minSpanY;
     }
+
+    public WidgetAddFlowHandler getHander() {
+        return new WidgetAddFlowHandler(info);
+    }
 }
diff --git a/src/com/android/launcher3/widget/WidgetAddFlowHandler.java b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
new file mode 100644
index 0000000..f44e56c
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
@@ -0,0 +1,108 @@
+/*
+ * 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.widget;
+
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppWidgetInfo;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.compat.AppWidgetManagerCompat;
+import com.android.launcher3.util.PendingRequestArgs;
+
+/**
+ * Utility class to handle app widget add flow.
+ */
+public class WidgetAddFlowHandler implements Parcelable {
+
+    private final AppWidgetProviderInfo mProviderInfo;
+
+    public WidgetAddFlowHandler(AppWidgetProviderInfo providerInfo) {
+        mProviderInfo = providerInfo;
+    }
+
+    private WidgetAddFlowHandler(Parcel parcel) {
+        mProviderInfo = AppWidgetProviderInfo.CREATOR.createFromParcel(parcel);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int i) {
+        mProviderInfo.writeToParcel(parcel, i);
+    }
+
+    public void startBindFlow(Launcher launcher, int appWidgetId, ItemInfo info, int requestCode) {
+        launcher.setWaitingForResult(PendingRequestArgs.forWidgetInfo(appWidgetId, this, info));
+
+        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND);
+        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, mProviderInfo.provider);
+        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE,
+                mProviderInfo.getProfile());
+        // TODO: we need to make sure that this accounts for the options bundle.
+        // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options);
+        launcher.startActivityForResult(intent, requestCode);
+    }
+
+    /**
+     * @see #startConfigActivity(Launcher, int, ItemInfo, int)
+     */
+    public boolean startConfigActivity(Launcher launcher, LauncherAppWidgetInfo info,
+            int requestCode) {
+        return startConfigActivity(launcher, info.appWidgetId, info, requestCode);
+    }
+
+    /**
+     * Starts the widget configuration flow if needed.
+     * @return true if the configuration flow was started, false otherwise.
+     */
+    public boolean startConfigActivity(Launcher launcher, int appWidgetId, ItemInfo info,
+            int requestCode) {
+        if (mProviderInfo.configure == null) {
+            return false;
+        }
+        launcher.setWaitingForResult(PendingRequestArgs.forWidgetInfo(appWidgetId, this, info));
+
+        AppWidgetManagerCompat.getInstance(launcher).startConfigActivity(
+                mProviderInfo, appWidgetId, launcher, launcher.getAppWidgetHost(), requestCode);
+        return true;
+    }
+
+    public LauncherAppWidgetProviderInfo getProviderInfo(Context context) {
+        return LauncherAppWidgetProviderInfo.fromProviderInfo(context, mProviderInfo);
+    }
+
+    public static final Parcelable.Creator<WidgetAddFlowHandler> CREATOR =
+            new Parcelable.Creator<WidgetAddFlowHandler>() {
+                public WidgetAddFlowHandler createFromParcel(Parcel source) {
+                    return new WidgetAddFlowHandler(source);
+                }
+
+                public WidgetAddFlowHandler[] newArray(int size) {
+                    return new WidgetAddFlowHandler[size];
+                }
+            };
+}
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 87247f4..455ec4e 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -144,12 +144,8 @@
         }
     }
 
-    public int[] getPreviewSize() {
-        int[] maxSize = new int[2];
-
-        maxSize[0] = mPresetPreviewSize;
-        maxSize[1] = mPresetPreviewSize;
-        return maxSize;
+    public WidgetImageView getWidgetView() {
+        return mWidgetImage;
     }
 
     public void applyPreview(Bitmap bitmap) {
@@ -166,12 +162,8 @@
         if (mActiveRequest != null) {
             return;
         }
-        int[] size = getPreviewSize();
-        if (DEBUG) {
-            Log.d(TAG, String.format("[tag=%s] ensurePreview (%d, %d):",
-                    getTagToString(), size[0], size[1]));
-        }
-        mActiveRequest = mWidgetPreviewLoader.getPreview(mItem, size[0], size[1], this);
+        mActiveRequest = mWidgetPreviewLoader.getPreview(
+                mItem, mPresetPreviewSize, mPresetPreviewSize, this);
     }
 
     @Override
diff --git a/tests/Android.mk b/tests/Android.mk
index 5103ced..e8797a7 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -20,6 +20,7 @@
 LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ub-uiautomator 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
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
new file mode 100644
index 0000000..763481a
--- /dev/null
+++ b/tests/AndroidManifest-common.xml
@@ -0,0 +1,47 @@
+<?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">
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+
+        <receiver android:name="com.android.launcher3.testcomponent.AppWidgetNoConfig">
+            <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">
+            <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>
+    </application>
+</manifest>
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/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/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/WidgetConfigActivity.java b/tests/src/com/android/launcher3/testcomponent/WidgetConfigActivity.java
new file mode 100644
index 0000000..c0509bc
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/WidgetConfigActivity.java
@@ -0,0 +1,64 @@
+/*
+ * 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;
+
+/**
+ * Simple activity for widget configuration
+ */
+public class WidgetConfigActivity extends Activity {
+
+    public static final String SUFFIX_FINISH = "-finish";
+    public static final String EXTRA_CODE = "code";
+    public static final String EXTRA_INTENT = "intent";
+
+    private final BroadcastReceiver mFinishReceiver = new BroadcastReceiver() {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            WidgetConfigActivity.this.setResult(
+                    intent.getIntExtra(EXTRA_CODE, RESULT_CANCELED),
+                    (Intent) intent.getParcelableExtra(EXTRA_INTENT));
+            WidgetConfigActivity.this.finish();
+        }
+    };
+
+    private final String mAction = this.getClass().getName();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        registerReceiver(mFinishReceiver, new IntentFilter(mAction + SUFFIX_FINISH));
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        sendBroadcast(new Intent(mAction).putExtra(Intent.EXTRA_INTENT, getIntent()));
+    }
+
+    @Override
+    protected void onDestroy() {
+        unregisterReceiver(mFinishReceiver);
+        super.onDestroy();
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java b/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
index 42c6cd7..4bc40c6 100644
--- a/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
+++ b/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
@@ -1,13 +1,27 @@
+/*
+ * 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.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 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,22 +33,23 @@
 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.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.FileInputStream;
 import java.io.IOException;
 import java.util.Locale;
 import java.util.concurrent.Callable;
-import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Base class for all instrumentation tests providing various utility methods.
@@ -42,6 +57,7 @@
 public class LauncherInstrumentationTestCase extends InstrumentationTestCase {
 
     public static final long DEFAULT_UI_TIMEOUT = 3000;
+    public static final long DEFAULT_WORKER_TIMEOUT_SECS = 5;
 
     protected UiDevice mDevice;
     protected Context mTargetContext;
@@ -233,18 +249,11 @@
      * 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);
+        }
     }
 
     /**
@@ -252,35 +261,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.getIDP(mTargetContext);
-
-                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) {
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..7cbd292
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import android.test.suitebuilder.annotation.LargeTest;
+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;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test to verify widget configuration is properly shown.
+ */
+@LargeTest
+public class AddConfigWidgetTest extends LauncherInstrumentationTestCase {
+
+    public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
+    public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5;
+
+    private LauncherAppWidgetProviderInfo mWidgetInfo;
+    private SimpleActivityMonitor mActivityMonitor;
+    private MainThreadExecutor mMainThreadExecutor;
+    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);
+    }
+
+    @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));
+
+        if (acceptConfig) {
+            setResult(Activity.RESULT_OK);
+            assertTrue(Wait.atMost(new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT));
+            assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
+        } else {
+            setResult(Activity.RESULT_CANCELED);
+            // Verify that the widget id is deleted.
+            assertTrue(Wait.atMost(new Condition() {
+                @Override
+                public boolean isTrue() throws Throwable {
+                    return mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null;
+                }
+            }, DEFAULT_ACTIVITY_TIMEOUT));
+        }
+    }
+
+    private void setResult(int resultCode) {
+        String action = WidgetConfigActivity.class.getName() + WidgetConfigActivity.SUFFIX_FINISH;
+        getInstrumentation().getTargetContext().sendBroadcast(
+                new Intent(action).putExtra(WidgetConfigActivity.EXTRA_CODE, resultCode));
+    }
+
+    /**
+     * 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 BroadcastReceiver {
+
+        private final CountDownLatch latch = new CountDownLatch(1);
+        private Intent mIntent;
+
+        WidgetConfigStartupMonitor() {
+            getInstrumentation().getTargetContext().registerReceiver(this,
+                    new IntentFilter(WidgetConfigActivity.class.getName()));
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT);
+            latch.countDown();
+        }
+
+        public int getWidgetId() throws InterruptedException {
+            latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS);
+            getInstrumentation().getTargetContext().unregisterReceiver(this);
+            assertNotNull(mIntent);
+            assertEquals(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, mIntent.getAction());
+            int widgetId = mIntent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
+                    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 74%
rename from tests/src/com/android/launcher3/ui/AddWidgetTest.java
rename to tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index d536af2..b7e1ca9 100644
--- a/tests/src/com/android/launcher3/ui/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -1,4 +1,19 @@
-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;
@@ -10,6 +25,7 @@
 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;
diff --git a/tests/src/com/android/launcher3/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
similarity index 90%
rename from tests/src/com/android/launcher3/BindWidgetTest.java
rename to tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index 575b42b..df2b662 100644
--- a/tests/src/com/android/launcher3/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -1,4 +1,19 @@
-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.appwidget.AppWidgetHost;
 import android.content.ComponentName;
@@ -12,10 +27,19 @@
 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.ContentWriter;
+import com.android.launcher3.util.LooperExecuter;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.WidgetHostViewLoader;
 
@@ -315,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/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);
+    }
+}