Merge "Add feature flag for all apps pull up work." into ub-launcher3-calgary
diff --git a/res/layout/widgets_view.xml b/res/layout/widgets_view.xml
index e55f6f0..0503466 100644
--- a/res/layout/widgets_view.xml
+++ b/res/layout/widgets_view.xml
@@ -49,6 +49,12 @@
             android:theme="@style/CustomOverscroll.Dark"
             android:layout_width="match_parent"
             android:layout_height="match_parent" />
+
+        <ProgressBar
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:id="@+id/loader"
+            android:layout_gravity="center" />
     </FrameLayout>
 
 
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 578e86f..50aa03d 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -72,7 +72,7 @@
     <string name="wallpaper_button_text" msgid="8404103075899945851">"Wallpaper"</string>
     <string name="settings_button_text" msgid="8119458837558863227">"Setelan"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Dinonaktifkan oleh admin"</string>
-    <string name="allow_rotation_title" msgid="3132336367556833843">"Izinkan putaran layar utama"</string>
+    <string name="allow_rotation_title" msgid="3132336367556833843">"Izinkan layar utama diputar"</string>
     <string name="allow_rotation_desc" msgid="7635719920854330492">"Saat perangkat diputar"</string>
     <string name="allow_rotation_blocked_desc" msgid="3212602545192996253">"Setelan Tampilan Saat Ini tidak memungkinkan putaran"</string>
     <string name="package_state_unknown" msgid="7592128424511031410">"Tidak dikenal"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 161227d..14d0aa6 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -73,7 +73,7 @@
     <string name="settings_button_text" msgid="8119458837558863227">"הגדרות"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"הושבת על ידי מנהל המערכת שלך"</string>
     <string name="allow_rotation_title" msgid="3132336367556833843">"אפשרות סיבוב של מסך דף הבית"</string>
-    <string name="allow_rotation_desc" msgid="7635719920854330492">"בעת סיבוב המכשיר"</string>
+    <string name="allow_rotation_desc" msgid="7635719920854330492">"בסיבוב המכשיר"</string>
     <string name="allow_rotation_blocked_desc" msgid="3212602545192996253">"הגדרת התצוגה הנוכחית אינה מאפשרת סיבוב"</string>
     <string name="package_state_unknown" msgid="7592128424511031410">"לא ידוע"</string>
     <string name="abandoned_clean_this" msgid="7610119707847920412">"הסר"</string>
diff --git a/res/values-ky-rKG/strings.xml b/res/values-ky-rKG/strings.xml
index 7f03a0b..2edd170 100644
--- a/res/values-ky-rKG/strings.xml
+++ b/res/values-ky-rKG/strings.xml
@@ -74,7 +74,7 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Администраторуңуз өчүрүп койгон"</string>
     <string name="allow_rotation_title" msgid="3132336367556833843">"Башкы экранды айлантууга уруксат берүү"</string>
     <string name="allow_rotation_desc" msgid="7635719920854330492">"Түзмөк айланганда"</string>
-    <string name="allow_rotation_blocked_desc" msgid="3212602545192996253">"Учурдагы дисплей жөндөөсү айлантууга уруксат бербейт"</string>
+    <string name="allow_rotation_blocked_desc" msgid="3212602545192996253">"Экранды айлантуу параметри өчүрүлгөн"</string>
     <string name="package_state_unknown" msgid="7592128424511031410">"Белгисиз"</string>
     <string name="abandoned_clean_this" msgid="7610119707847920412">"Алып салуу"</string>
     <string name="abandoned_search" msgid="891119232568284442">"Издөө"</string>
diff --git a/res/values-ne-rNP/strings.xml b/res/values-ne-rNP/strings.xml
index e4d1409..169f3de 100644
--- a/res/values-ne-rNP/strings.xml
+++ b/res/values-ne-rNP/strings.xml
@@ -72,7 +72,7 @@
     <string name="wallpaper_button_text" msgid="8404103075899945851">"वालपेपरहरु"</string>
     <string name="settings_button_text" msgid="8119458837558863227">"सेटिंङहरू"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"तपाईँको प्रशासकद्वारा असक्षम गरिएको"</string>
-    <string name="allow_rotation_title" msgid="3132336367556833843">"गृहस्क्रिन घुम्ने सुविधालाई अनुमति दिनुहोस्"</string>
+    <string name="allow_rotation_title" msgid="3132336367556833843">"गृहस्क्रिनलाई घुम्ने अनुमति दिनुहोस्"</string>
     <string name="allow_rotation_desc" msgid="7635719920854330492">"यन्त्रलाई घुमाइँदा"</string>
     <string name="allow_rotation_blocked_desc" msgid="3212602545192996253">"हालको प्रदर्शन सम्बन्धी सेटिङले घुमाउने सुविधालाई अनुमति दिँदैन"</string>
     <string name="package_state_unknown" msgid="7592128424511031410">"अज्ञात"</string>
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index eaa96c9..e3bb5fa 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -32,7 +32,6 @@
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.TransitionDrawable;
 import android.os.Build;
 import android.os.Parcelable;
@@ -55,6 +54,8 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.config.ProviderConfig;
 import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.util.CellAndSpan;
+import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.ParcelableSparseArray;
 import com.android.launcher3.util.Thunk;
 
@@ -81,9 +82,9 @@
     private int mFixedCellHeight;
 
     @ViewDebug.ExportedProperty(category = "launcher")
-    @Thunk int mCountX;
+    private int mCountX;
     @ViewDebug.ExportedProperty(category = "launcher")
-    @Thunk int mCountY;
+    private int mCountY;
 
     private int mOriginalWidthGap;
     private int mOriginalHeightGap;
@@ -101,8 +102,8 @@
     @Thunk final int[] mTmpPoint = new int[2];
     @Thunk final int[] mTempLocation = new int[2];
 
-    boolean[][] mOccupied;
-    boolean[][] mTmpOccupied;
+    private GridOccupancy mOccupied;
+    private GridOccupancy mTmpOccupied;
 
     private OnTouchListener mInterceptTouchListener;
     private StylusEventHelper mStylusEventHelper;
@@ -203,10 +204,12 @@
         mWidthGap = mOriginalWidthGap = 0;
         mHeightGap = mOriginalHeightGap = 0;
         mMaxGap = Integer.MAX_VALUE;
-        mCountX = (int) grid.inv.numColumns;
-        mCountY = (int) grid.inv.numRows;
-        mOccupied = new boolean[mCountX][mCountY];
-        mTmpOccupied = new boolean[mCountX][mCountY];
+
+        mCountX = grid.inv.numColumns;
+        mCountY = grid.inv.numRows;
+        mOccupied =  new GridOccupancy(mCountX, mCountY);
+        mTmpOccupied = new GridOccupancy(mCountX, mCountY);
+
         mPreviousReorderDirection[0] = INVALID_DIRECTION;
         mPreviousReorderDirection[1] = INVALID_DIRECTION;
 
@@ -375,8 +378,8 @@
     public void setGridSize(int x, int y) {
         mCountX = x;
         mCountY = y;
-        mOccupied = new boolean[mCountX][mCountY];
-        mTmpOccupied = new boolean[mCountX][mCountY];
+        mOccupied = new GridOccupancy(mCountX, mCountY);
+        mTmpOccupied = new GridOccupancy(mCountX, mCountY);
         mTempRectStack.clear();
         mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap,
                 mCountX, mCountY);
@@ -494,7 +497,7 @@
             cd.setBounds(0, 0,  mCellWidth, mCellHeight);
             for (int i = 0; i < mCountX; i++) {
                 for (int j = 0; j < mCountY; j++) {
-                    if (mOccupied[i][j]) {
+                    if (mOccupied.cells[i][j]) {
                         cellToPoint(i, j, pt);
                         canvas.save();
                         canvas.translate(pt[0], pt[1]);
@@ -655,14 +658,14 @@
 
     @Override
     public void removeAllViews() {
-        clearOccupiedCells();
+        mOccupied.clear();
         mShortcutsAndWidgets.removeAllViews();
     }
 
     @Override
     public void removeAllViewsInLayout() {
         if (mShortcutsAndWidgets.getChildCount() > 0) {
-            clearOccupiedCells();
+            mOccupied.clear();
             mShortcutsAndWidgets.removeAllViewsInLayout();
         }
     }
@@ -963,10 +966,6 @@
     public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration,
             int delay, boolean permanent, boolean adjustOccupied) {
         ShortcutAndWidgetContainer clc = getShortcutsAndWidgets();
-        boolean[][] occupied = mOccupied;
-        if (!permanent) {
-            occupied = mTmpOccupied;
-        }
 
         if (clc.indexOfChild(child) != -1) {
             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
@@ -981,8 +980,9 @@
             final int oldX = lp.x;
             final int oldY = lp.y;
             if (adjustOccupied) {
-                occupied[lp.cellX][lp.cellY] = false;
-                occupied[cellX][cellY] = true;
+                GridOccupancy occupied = permanent ? mOccupied : mTmpOccupied;
+                occupied.markCells(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, false);
+                occupied.markCells(cellX, cellY, lp.cellHSpan, lp.cellVSpan, true);
             }
             lp.isLockedToGrid = true;
             if (permanent) {
@@ -1218,7 +1218,7 @@
                     // First, let's see if this thing fits anywhere
                     for (int i = 0; i < minSpanX; i++) {
                         for (int j = 0; j < minSpanY; j++) {
-                            if (mOccupied[x + i][y + j]) {
+                            if (mOccupied.cells[x + i][y + j]) {
                                 continue inner;
                             }
                         }
@@ -1235,7 +1235,7 @@
                     while (!(hitMaxX && hitMaxY)) {
                         if (incX && !hitMaxX) {
                             for (int j = 0; j < ySize; j++) {
-                                if (x + xSize > countX -1 || mOccupied[x + xSize][y + j]) {
+                                if (x + xSize > countX -1 || mOccupied.cells[x + xSize][y + j]) {
                                     // We can't move out horizontally
                                     hitMaxX = true;
                                 }
@@ -1245,7 +1245,7 @@
                             }
                         } else if (!hitMaxY) {
                             for (int i = 0; i < xSize; i++) {
-                                if (y + ySize > countY - 1 || mOccupied[x + i][y + ySize]) {
+                                if (y + ySize > countY - 1 || mOccupied.cells[x + i][y + ySize]) {
                                     // We can't move out vertically
                                     hitMaxY = true;
                                 }
@@ -1303,7 +1303,7 @@
         return bestXY;
     }
 
-     /**
+    /**
      * Find a vacant area that will fit the given bounds nearest the requested
      * cell location, and will also weigh in a suggested direction vector of the
      * desired location. This method computers distance based on unit grid distances,
@@ -1377,17 +1377,18 @@
             int[] direction, ItemConfiguration currentState) {
         CellAndSpan c = currentState.map.get(v);
         boolean success = false;
-        markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false);
-        markCellsForRect(rectOccupiedByPotentialDrop, mTmpOccupied, true);
+        mTmpOccupied.markCells(c, false);
+        mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
 
-        findNearestArea(c.x, c.y, c.spanX, c.spanY, direction, mTmpOccupied, null, mTempLocation);
+        findNearestArea(c.cellX, c.cellY, c.spanX, c.spanY, direction,
+                mTmpOccupied.cells, null, mTempLocation);
 
         if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
-            c.x = mTempLocation[0];
-            c.y = mTempLocation[1];
+            c.cellX = mTempLocation[0];
+            c.cellY = mTempLocation[1];
             success = true;
         }
-        markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true);
+        mTmpOccupied.markCells(c, true);
         return success;
     }
 
@@ -1440,32 +1441,32 @@
                 CellAndSpan cs = config.map.get(views.get(i));
                 switch (which) {
                     case LEFT:
-                        int left = cs.x;
-                        for (int j = cs.y; j < cs.y + cs.spanY; j++) {
+                        int left = cs.cellX;
+                        for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) {
                             if (left < leftEdge[j] || leftEdge[j] < 0) {
                                 leftEdge[j] = left;
                             }
                         }
                         break;
                     case RIGHT:
-                        int right = cs.x + cs.spanX;
-                        for (int j = cs.y; j < cs.y + cs.spanY; j++) {
+                        int right = cs.cellX + cs.spanX;
+                        for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) {
                             if (right > rightEdge[j]) {
                                 rightEdge[j] = right;
                             }
                         }
                         break;
                     case TOP:
-                        int top = cs.y;
-                        for (int j = cs.x; j < cs.x + cs.spanX; j++) {
+                        int top = cs.cellY;
+                        for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) {
                             if (top < topEdge[j] || topEdge[j] < 0) {
                                 topEdge[j] = top;
                             }
                         }
                         break;
                     case BOTTOM:
-                        int bottom = cs.y + cs.spanY;
-                        for (int j = cs.x; j < cs.x + cs.spanX; j++) {
+                        int bottom = cs.cellY + cs.spanY;
+                        for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) {
                             if (bottom > bottomEdge[j]) {
                                 bottomEdge[j] = bottom;
                             }
@@ -1485,29 +1486,29 @@
 
             switch (whichEdge) {
                 case LEFT:
-                    for (int i = cs.y; i < cs.y + cs.spanY; i++) {
-                        if (leftEdge[i] == cs.x + cs.spanX) {
+                    for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) {
+                        if (leftEdge[i] == cs.cellX + cs.spanX) {
                             return true;
                         }
                     }
                     break;
                 case RIGHT:
-                    for (int i = cs.y; i < cs.y + cs.spanY; i++) {
-                        if (rightEdge[i] == cs.x) {
+                    for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) {
+                        if (rightEdge[i] == cs.cellX) {
                             return true;
                         }
                     }
                     break;
                 case TOP:
-                    for (int i = cs.x; i < cs.x + cs.spanX; i++) {
-                        if (topEdge[i] == cs.y + cs.spanY) {
+                    for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) {
+                        if (topEdge[i] == cs.cellY + cs.spanY) {
                             return true;
                         }
                     }
                     break;
                 case BOTTOM:
-                    for (int i = cs.x; i < cs.x + cs.spanX; i++) {
-                        if (bottomEdge[i] == cs.y) {
+                    for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) {
+                        if (bottomEdge[i] == cs.cellY) {
                             return true;
                         }
                     }
@@ -1521,17 +1522,17 @@
                 CellAndSpan c = config.map.get(v);
                 switch (whichEdge) {
                     case LEFT:
-                        c.x -= delta;
+                        c.cellX -= delta;
                         break;
                     case RIGHT:
-                        c.x += delta;
+                        c.cellX += delta;
                         break;
                     case TOP:
-                        c.y -= delta;
+                        c.cellY -= delta;
                         break;
                     case BOTTOM:
                     default:
-                        c.y += delta;
+                        c.cellY += delta;
                         break;
                 }
             }
@@ -1545,16 +1546,7 @@
 
         public Rect getBoundingRect() {
             if (boundingRectDirty) {
-                boolean first = true;
-                for (View v: views) {
-                    CellAndSpan c = config.map.get(v);
-                    if (first) {
-                        boundingRect.set(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
-                        first = false;
-                    } else {
-                        boundingRect.union(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
-                    }
-                }
+                config.getBoundingRectForViews(views, boundingRect);
             }
             return boundingRect;
         }
@@ -1567,14 +1559,14 @@
                 CellAndSpan r = config.map.get(right);
                 switch (whichEdge) {
                     case LEFT:
-                        return (r.x + r.spanX) - (l.x + l.spanX);
+                        return (r.cellX + r.spanX) - (l.cellX + l.spanX);
                     case RIGHT:
-                        return l.x - r.x;
+                        return l.cellX - r.cellX;
                     case TOP:
-                        return (r.y + r.spanY) - (l.y + l.spanY);
+                        return (r.cellY + r.spanY) - (l.cellY + l.spanY);
                     case BOTTOM:
                     default:
-                        return l.y - r.y;
+                        return l.cellY - r.cellY;
                 }
             }
         }
@@ -1618,7 +1610,7 @@
         // Mark the occupied state as false for the group of views we want to move.
         for (View v: views) {
             CellAndSpan c = currentState.map.get(v);
-            markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false);
+            mTmpOccupied.markCells(c, false);
         }
 
         // We save the current configuration -- if we fail to find a solution we will revert
@@ -1648,7 +1640,7 @@
                         CellAndSpan c = currentState.map.get(v);
 
                         // Adding view to cluster, mark it as not occupied.
-                        markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false);
+                        mTmpOccupied.markCells(c, false);
                     }
                 }
             }
@@ -1674,7 +1666,7 @@
         // In either case, we set the occupied array as marked for the location of the views
         for (View v: cluster.views) {
             CellAndSpan c = currentState.map.get(v);
-            markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true);
+            mTmpOccupied.markCells(c, true);
         }
 
         return foundSolution;
@@ -1685,37 +1677,31 @@
         if (views.size() == 0) return true;
 
         boolean success = false;
-        Rect boundingRect = null;
+        Rect boundingRect = new Rect();
         // We construct a rect which represents the entire group of views passed in
-        for (View v: views) {
-            CellAndSpan c = currentState.map.get(v);
-            if (boundingRect == null) {
-                boundingRect = new Rect(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
-            } else {
-                boundingRect.union(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
-            }
-        }
+        currentState.getBoundingRectForViews(views, boundingRect);
 
         // Mark the occupied state as false for the group of views we want to move.
         for (View v: views) {
             CellAndSpan c = currentState.map.get(v);
-            markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false);
+            mTmpOccupied.markCells(c, false);
         }
 
-        boolean[][] blockOccupied = new boolean[boundingRect.width()][boundingRect.height()];
+        GridOccupancy blockOccupied = new GridOccupancy(boundingRect.width(), boundingRect.height());
         int top = boundingRect.top;
         int left = boundingRect.left;
         // We mark more precisely which parts of the bounding rect are truly occupied, allowing
         // for interlocking.
         for (View v: views) {
             CellAndSpan c = currentState.map.get(v);
-            markCellsForView(c.x - left, c.y - top, c.spanX, c.spanY, blockOccupied, true);
+            blockOccupied.markCells(c.cellX - left, c.cellY - top, c.spanX, c.spanY, true);
         }
 
-        markCellsForRect(rectOccupiedByPotentialDrop, mTmpOccupied, true);
+        mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
 
         findNearestArea(boundingRect.left, boundingRect.top, boundingRect.width(),
-                boundingRect.height(), direction, mTmpOccupied, blockOccupied, mTempLocation);
+                boundingRect.height(), direction,
+                mTmpOccupied.cells, blockOccupied.cells, mTempLocation);
 
         // If we successfuly found a location by pushing the block of views, we commit it
         if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
@@ -1723,8 +1709,8 @@
             int deltaY = mTempLocation[1] - boundingRect.top;
             for (View v: views) {
                 CellAndSpan c = currentState.map.get(v);
-                c.x += deltaX;
-                c.y += deltaY;
+                c.cellX += deltaX;
+                c.cellY += deltaY;
             }
             success = true;
         }
@@ -1732,15 +1718,11 @@
         // In either case, we set the occupied array as marked for the location of the views
         for (View v: views) {
             CellAndSpan c = currentState.map.get(v);
-            markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true);
+            mTmpOccupied.markCells(c, true);
         }
         return success;
     }
 
-    private void markCellsForRect(Rect r, boolean[][] occupied, boolean value) {
-        markCellsForView(r.left, r.top, r.width(), r.height(), occupied, value);
-    }
-
     // This method tries to find a reordering solution which satisfies the push mechanic by trying
     // to push items in each of the cardinal directions, in an order based on the direction vector
     // passed.
@@ -1850,8 +1832,8 @@
         if (ignoreView != null) {
             CellAndSpan c = solution.map.get(ignoreView);
             if (c != null) {
-                c.x = cellX;
-                c.y = cellY;
+                c.cellX = cellX;
+                c.cellY = cellY;
             }
         }
         Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
@@ -1860,7 +1842,7 @@
             if (child == ignoreView) continue;
             CellAndSpan c = solution.map.get(child);
             LayoutParams lp = (LayoutParams) child.getLayoutParams();
-            r1.set(c.x, c.y, c.x + c.spanX, c.y + c.spanY);
+            r1.set(c.cellX, c.cellY, c.cellX + c.spanX, c.cellY + c.spanY);
             if (Rect.intersects(r0, r1)) {
                 if (!lp.canReorder) {
                     return false;
@@ -1911,14 +1893,6 @@
         }
     }
 
-    private void copyOccupiedArray(boolean[][] occupied) {
-        for (int i = 0; i < mCountX; i++) {
-            for (int j = 0; j < mCountY; j++) {
-                occupied[i][j] = mOccupied[i][j];
-            }
-        }
-    }
-
     private ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY,
             int spanX, int spanY, int[] direction, View dragView, boolean decX,
             ItemConfiguration solution) {
@@ -1926,7 +1900,7 @@
         copyCurrentStateToSolution(solution, false);
         // Copy the current occupied array into the temporary occupied array. This array will be
         // manipulated as necessary to find a solution.
-        copyOccupiedArray(mTmpOccupied);
+        mOccupied.copyTo(mTmpOccupied);
 
         // We find the nearest cell into which we would place the dragged item, assuming there's
         // nothing in its way.
@@ -1952,10 +1926,10 @@
             solution.isSolution = false;
         } else {
             solution.isSolution = true;
-            solution.dragViewX = result[0];
-            solution.dragViewY = result[1];
-            solution.dragViewSpanX = spanX;
-            solution.dragViewSpanY = spanY;
+            solution.cellX = result[0];
+            solution.cellY = result[1];
+            solution.spanX = spanX;
+            solution.spanY = spanY;
         }
         return solution;
     }
@@ -1976,11 +1950,7 @@
     }
 
     private void copySolutionToTempState(ItemConfiguration solution, View dragView) {
-        for (int i = 0; i < mCountX; i++) {
-            for (int j = 0; j < mCountY; j++) {
-                mTmpOccupied[i][j] = false;
-            }
-        }
+        mTmpOccupied.clear();
 
         int childCount = mShortcutsAndWidgets.getChildCount();
         for (int i = 0; i < childCount; i++) {
@@ -1989,26 +1959,21 @@
             LayoutParams lp = (LayoutParams) child.getLayoutParams();
             CellAndSpan c = solution.map.get(child);
             if (c != null) {
-                lp.tmpCellX = c.x;
-                lp.tmpCellY = c.y;
+                lp.tmpCellX = c.cellX;
+                lp.tmpCellY = c.cellY;
                 lp.cellHSpan = c.spanX;
                 lp.cellVSpan = c.spanY;
-                markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true);
+                mTmpOccupied.markCells(c, true);
             }
         }
-        markCellsForView(solution.dragViewX, solution.dragViewY, solution.dragViewSpanX,
-                solution.dragViewSpanY, mTmpOccupied, true);
+        mTmpOccupied.markCells(solution, true);
     }
 
     private void animateItemsToSolution(ItemConfiguration solution, View dragView, boolean
             commitDragView) {
 
-        boolean[][] occupied = DESTRUCTIVE_REORDER ? mOccupied : mTmpOccupied;
-        for (int i = 0; i < mCountX; i++) {
-            for (int j = 0; j < mCountY; j++) {
-                occupied[i][j] = false;
-            }
-        }
+        GridOccupancy occupied = DESTRUCTIVE_REORDER ? mOccupied : mTmpOccupied;
+        occupied.clear();
 
         int childCount = mShortcutsAndWidgets.getChildCount();
         for (int i = 0; i < childCount; i++) {
@@ -2016,14 +1981,13 @@
             if (child == dragView) continue;
             CellAndSpan c = solution.map.get(child);
             if (c != null) {
-                animateChildToPosition(child, c.x, c.y, REORDER_ANIMATION_DURATION, 0,
+                animateChildToPosition(child, c.cellX, c.cellY, REORDER_ANIMATION_DURATION, 0,
                         DESTRUCTIVE_REORDER, false);
-                markCellsForView(c.x, c.y, c.spanX, c.spanY, occupied, true);
+                occupied.markCells(c, true);
             }
         }
         if (commitDragView) {
-            markCellsForView(solution.dragViewX, solution.dragViewY, solution.dragViewSpanX,
-                    solution.dragViewSpanY, occupied, true);
+            occupied.markCells(solution, true);
         }
     }
 
@@ -2042,7 +2006,7 @@
             LayoutParams lp = (LayoutParams) child.getLayoutParams();
             if (c != null && !skip) {
                 ReorderPreviewAnimation rha = new ReorderPreviewAnimation(child, mode, lp.cellX,
-                        lp.cellY, c.x, c.y, c.spanX, c.spanY);
+                        lp.cellY, c.cellX, c.cellY, c.spanX, c.spanY);
                 rha.animate();
             }
         }
@@ -2186,11 +2150,7 @@
     }
 
     private void commitTempPlacement() {
-        for (int i = 0; i < mCountX; i++) {
-            for (int j = 0; j < mCountY; j++) {
-                mOccupied[i][j] = mTmpOccupied[i][j];
-            }
-        }
+        mTmpOccupied.copyTo(mOccupied);
 
         long screenId = mLauncher.getWorkspace().getIdForScreen(this);
         int container = Favorites.CONTAINER_DESKTOP;
@@ -2241,10 +2201,10 @@
                 resultSpan);
         if (result[0] >= 0 && result[1] >= 0) {
             copyCurrentStateToSolution(solution, false);
-            solution.dragViewX = result[0];
-            solution.dragViewY = result[1];
-            solution.dragViewSpanX = resultSpan[0];
-            solution.dragViewSpanY = resultSpan[1];
+            solution.cellX = result[0];
+            solution.cellY = result[1];
+            solution.spanX = resultSpan[0];
+            solution.spanY = resultSpan[1];
             solution.isSolution = true;
         } else {
             solution.isSolution = false;
@@ -2432,10 +2392,10 @@
             if (finalSolution != null) {
                 beginOrAdjustReorderPreviewAnimations(finalSolution, dragView, 0,
                         ReorderPreviewAnimation.MODE_HINT);
-                result[0] = finalSolution.dragViewX;
-                result[1] = finalSolution.dragViewY;
-                resultSpan[0] = finalSolution.dragViewSpanX;
-                resultSpan[1] = finalSolution.dragViewSpanY;
+                result[0] = finalSolution.cellX;
+                result[1] = finalSolution.cellY;
+                resultSpan[0] = finalSolution.spanX;
+                resultSpan[1] = finalSolution.spanY;
             } else {
                 result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1;
             }
@@ -2448,10 +2408,10 @@
         }
 
         if (finalSolution != null) {
-            result[0] = finalSolution.dragViewX;
-            result[1] = finalSolution.dragViewY;
-            resultSpan[0] = finalSolution.dragViewSpanX;
-            resultSpan[1] = finalSolution.dragViewSpanY;
+            result[0] = finalSolution.cellX;
+            result[1] = finalSolution.cellY;
+            resultSpan[0] = finalSolution.spanX;
+            resultSpan[1] = finalSolution.spanY;
 
             // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
             // committing anything or animating anything as we just want to determine if a solution
@@ -2493,25 +2453,24 @@
         return mItemPlacementDirty;
     }
 
-    @Thunk class ItemConfiguration {
+    private static class ItemConfiguration extends CellAndSpan {
         HashMap<View, CellAndSpan> map = new HashMap<View, CellAndSpan>();
         private HashMap<View, CellAndSpan> savedMap = new HashMap<View, CellAndSpan>();
         ArrayList<View> sortedViews = new ArrayList<View>();
         ArrayList<View> intersectingViews;
         boolean isSolution = false;
-        int dragViewX, dragViewY, dragViewSpanX, dragViewSpanY;
 
         void save() {
             // Copy current state into savedMap
             for (View v: map.keySet()) {
-                map.get(v).copy(savedMap.get(v));
+                savedMap.get(v).copyFrom(map.get(v));
             }
         }
 
         void restore() {
             // Restore current state from savedMap
             for (View v: savedMap.keySet()) {
-                savedMap.get(v).copy(map.get(v));
+                map.get(v).copyFrom(savedMap.get(v));
             }
         }
 
@@ -2522,35 +2481,21 @@
         }
 
         int area() {
-            return dragViewSpanX * dragViewSpanY;
-        }
-    }
-
-    private class CellAndSpan {
-        int x, y;
-        int spanX, spanY;
-
-        public CellAndSpan() {
+            return spanX * spanY;
         }
 
-        public void copy(CellAndSpan copy) {
-            copy.x = x;
-            copy.y = y;
-            copy.spanX = spanX;
-            copy.spanY = spanY;
+        void getBoundingRectForViews(ArrayList<View> views, Rect outRect) {
+            boolean first = true;
+            for (View v: views) {
+                CellAndSpan c = map.get(v);
+                if (first) {
+                    outRect.set(c.cellX, c.cellY, c.cellX + c.spanX, c.cellY + c.spanY);
+                    first = false;
+                } else {
+                    outRect.union(c.cellX, c.cellY, c.cellX + c.spanX, c.cellY + c.spanY);
+                }
+            }
         }
-
-        public CellAndSpan(int x, int y, int spanX, int spanY) {
-            this.x = x;
-            this.y = y;
-            this.spanX = spanX;
-            this.spanY = spanY;
-        }
-
-        public String toString() {
-            return "(" + x + ", " + y + ": " + spanX + ", " + spanY + ")";
-        }
-
     }
 
     /**
@@ -2588,33 +2533,10 @@
      * @return True if a vacant cell of the specified dimension was found, false otherwise.
      */
     public boolean findCellForSpan(int[] cellXY, int spanX, int spanY) {
-        boolean foundCell = false;
-        final int endX = mCountX - (spanX - 1);
-        final int endY = mCountY - (spanY - 1);
-
-        for (int y = 0; y < endY && !foundCell; y++) {
-            inner:
-            for (int x = 0; x < endX; x++) {
-                for (int i = 0; i < spanX; i++) {
-                    for (int j = 0; j < spanY; j++) {
-                        if (mOccupied[x + i][y + j]) {
-                            // small optimization: we can skip to after the column we just found
-                            // an occupied cell
-                            x += i;
-                            continue inner;
-                        }
-                    }
-                }
-                if (cellXY != null) {
-                    cellXY[0] = x;
-                    cellXY[1] = y;
-                }
-                foundCell = true;
-                break;
-            }
+        if (cellXY == null) {
+            cellXY = new int[2];
         }
-
-        return foundCell;
+        return mOccupied.findVacantCell(cellXY, spanX, spanY);
     }
 
     /**
@@ -2688,34 +2610,16 @@
         resultRect.set(x, y, x + width, y + height);
     }
 
-    private void clearOccupiedCells() {
-        for (int x = 0; x < mCountX; x++) {
-            for (int y = 0; y < mCountY; y++) {
-                mOccupied[x][y] = false;
-            }
-        }
-    }
-
     public void markCellsAsOccupiedForView(View view) {
         if (view == null || view.getParent() != mShortcutsAndWidgets) return;
         LayoutParams lp = (LayoutParams) view.getLayoutParams();
-        markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, mOccupied, true);
+        mOccupied.markCells(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, true);
     }
 
     public void markCellsAsUnoccupiedForView(View view) {
         if (view == null || view.getParent() != mShortcutsAndWidgets) return;
         LayoutParams lp = (LayoutParams) view.getLayoutParams();
-        markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, mOccupied, false);
-    }
-
-    private void markCellsForView(int cellX, int cellY, int spanX, int spanY, boolean[][] occupied,
-            boolean value) {
-        if (cellX < 0 || cellY < 0) return;
-        for (int x = cellX; x < cellX + spanX && x < mCountX; x++) {
-            for (int y = cellY; y < cellY + spanY && y < mCountY; y++) {
-                occupied[x][y] = value;
-            }
-        }
+        mOccupied.markCells(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, false);
     }
 
     public int getDesiredWidth() {
@@ -2730,7 +2634,7 @@
 
     public boolean isOccupied(int x, int y) {
         if (x < mCountX && y < mCountY) {
-            return mOccupied[x][y];
+            return mOccupied.cells[x][y];
         } else {
             throw new RuntimeException("Position exceeds the bound of this CellLayout");
         }
@@ -2909,21 +2813,17 @@
     // 2. When long clicking on an empty cell in a CellLayout, we save information about the
     //    cellX and cellY coordinates and which page was clicked. We then set this as a tag on
     //    the CellLayout that was long clicked
-    public static final class CellInfo {
+    public static final class CellInfo extends CellAndSpan {
         public View cell;
-        int cellX = -1;
-        int cellY = -1;
-        int spanX;
-        int spanY;
         long screenId;
         long container;
 
         public CellInfo(View v, ItemInfo info) {
-            cell = v;
             cellX = info.cellX;
             cellY = info.cellY;
             spanX = info.spanX;
             spanY = info.spanY;
+            cell = v;
             screenId = info.screenId;
             container = info.container;
         }
@@ -2935,10 +2835,6 @@
         }
     }
 
-    public boolean findVacantCell(int spanX, int spanY, int[] outXY) {
-        return Utilities.findVacantCell(outXY, spanX, spanY, mCountX, mCountY, mOccupied);
-    }
-
     /**
      * Returns whether an item can be placed in this CellLayout (after rearranging and/or resizing
      * if necessary).
@@ -2960,19 +2856,6 @@
     }
 
     public boolean isRegionVacant(int x, int y, int spanX, int spanY) {
-        int x2 = x + spanX - 1;
-        int y2 = y + spanY - 1;
-        if (x < 0 || y < 0 || x2 >= mCountX || y2 >= mCountY) {
-            return false;
-        }
-        for (int i = x; i <= x2; i++) {
-            for (int j = y; j <= y2; j++) {
-                if (mOccupied[i][j]) {
-                    return false;
-                }
-            }
-        }
-
-        return true;
+        return mOccupied.isRegionVacant(x, y, spanX, spanY);
     }
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index ec09cf1..13e451a 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -158,6 +158,7 @@
     private static final int REQUEST_PICK_WALLPAPER = 10;
 
     private static final int REQUEST_BIND_APPWIDGET = 11;
+    private static final int REQUEST_BIND_PENDING_APPWIDGET = 14;
     private static final int REQUEST_RECONFIGURE_APPWIDGET = 12;
 
     private static final int REQUEST_PERMISSION_CALL_PHONE = 13;
@@ -688,8 +689,22 @@
                 completeAddAppWidget(args.appWidgetId, args.container, screenId, null, null);
                 break;
             case REQUEST_RECONFIGURE_APPWIDGET:
-                completeRestoreAppWidget(args.appWidgetId);
+                completeRestoreAppWidget(args.appWidgetId, LauncherAppWidgetInfo.RESTORE_COMPLETED);
                 break;
+            case REQUEST_BIND_PENDING_APPWIDGET: {
+                int widgetId = args.appWidgetId;
+                LauncherAppWidgetInfo info =
+                        completeRestoreAppWidget(widgetId, LauncherAppWidgetInfo.FLAG_UI_NOT_READY);
+                if (info != null) {
+                    // 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, info);
+                    }
+                }
+                break;
+            }
         }
         // Before adding this resetAddInfo(), after a shortcut was added to a workspace screen,
         // if you turned the screen off and then back while in All Apps, Launcher would not
@@ -804,7 +819,8 @@
             return;
         }
 
-        if (requestCode == REQUEST_RECONFIGURE_APPWIDGET) {
+        if (requestCode == REQUEST_RECONFIGURE_APPWIDGET
+                || requestCode == REQUEST_BIND_PENDING_APPWIDGET) {
             if (resultCode == RESULT_OK) {
                 // Update the widget view.
                 PendingAddArguments args = preparePendingAddArgs(requestCode, data,
@@ -2380,7 +2396,7 @@
      */
     private void deleteWidgetInfo(final LauncherAppWidgetInfo widgetInfo) {
         final LauncherAppWidgetHost appWidgetHost = getAppWidgetHost();
-        if (appWidgetHost != null && !widgetInfo.isCustomWidget() && widgetInfo.isWidgetIdValid()) {
+        if (appWidgetHost != null && !widgetInfo.isCustomWidget() && widgetInfo.isWidgetIdAllocated()) {
             // Deleting an app widget ID is a void call but writes to disk before returning
             // to the caller...
             new AsyncTask<Void, Void, Void>() {
@@ -2526,16 +2542,31 @@
 
         final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag();
         if (v.isReadyForClickSetup()) {
-            int widgetId = info.appWidgetId;
-            LauncherAppWidgetProviderInfo appWidgetInfo =
-                    mAppWidgetManager.getLauncherAppWidgetInfo(widgetId);
-            if (appWidgetInfo != null) {
-                mPendingAddWidgetInfo = appWidgetInfo;
-                mPendingAddInfo.copyFrom(info);
-                mPendingAddWidgetId = widgetId;
+            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) {
+                    mPendingAddWidgetId = info.appWidgetId;
+                    mPendingAddInfo.copyFrom(info);
+                    mPendingAddWidgetInfo = appWidgetInfo;
 
-                AppWidgetManagerCompat.getInstance(this).startConfigActivity(appWidgetInfo,
-                        info.appWidgetId, this, mAppWidgetHost, REQUEST_RECONFIGURE_APPWIDGET);
+                    Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND);
+                    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mPendingAddWidgetId);
+                    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, appWidgetInfo.provider);
+                    mAppWidgetManager.getUser(mPendingAddWidgetInfo)
+                            .addToIntent(intent, AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE);
+                    startActivityForResult(intent, REQUEST_BIND_PENDING_APPWIDGET);
+                }
+            } else {
+                LauncherAppWidgetProviderInfo appWidgetInfo =
+                        mAppWidgetManager.getLauncherAppWidgetInfo(info.appWidgetId);
+                if (appWidgetInfo != null) {
+                    startRestoredWidgetReconfigActivity(appWidgetInfo, info);
+                }
             }
         } else if (info.installProgress < 0) {
             // The install has not been queued
@@ -2553,6 +2584,15 @@
         }
     }
 
+    private void startRestoredWidgetReconfigActivity(
+            LauncherAppWidgetProviderInfo provider, LauncherAppWidgetInfo info) {
+        mPendingAddWidgetInfo = provider;
+        mPendingAddInfo.copyFrom(info);
+        mPendingAddWidgetId = info.appWidgetId;
+        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.
@@ -3919,41 +3959,33 @@
 
             // If we do not have a valid id, try to bind an id.
             if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
-                // Note: This assumes that the id remap broadcast is received before this step.
-                // If that is not the case, the id remap will be ignored and user may see the
-                // click to setup view.
-                PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(this, appWidgetInfo);
-                pendingInfo.spanX = item.spanX;
-                pendingInfo.spanY = item.spanY;
-                pendingInfo.minSpanX = item.minSpanX;
-                pendingInfo.minSpanY = item.minSpanY;
-                Bundle options = WidgetHostViewLoader.getDefaultOptionsForWidget(this, pendingInfo);
+                if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) {
+                    // Id has not been allocated yet. Allocate a new id.
+                    item.appWidgetId = mAppWidgetHost.allocateAppWidgetId();
+                    item.restoreStatus |= LauncherAppWidgetInfo.FLAG_ID_ALLOCATED;
 
-                int newWidgetId = mAppWidgetHost.allocateAppWidgetId();
-                boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed(
-                        newWidgetId, appWidgetInfo, options);
+                    // Also try to bind the widget. If the bind fails, the user will be shown
+                    // a click to setup UI, which will ask for the bind permission.
+                    PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(this, appWidgetInfo);
+                    pendingInfo.spanX = item.spanX;
+                    pendingInfo.spanY = item.spanY;
+                    pendingInfo.minSpanX = item.minSpanX;
+                    pendingInfo.minSpanY = item.minSpanY;
+                    Bundle options = WidgetHostViewLoader.getDefaultOptionsForWidget(this, pendingInfo);
+                    boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed(
+                            item.appWidgetId, appWidgetInfo, options);
 
-                // TODO consider showing a permission dialog when the widget is clicked.
-                if (!success) {
-                    mAppWidgetHost.deleteAppWidgetId(newWidgetId);
-                    if (DEBUG_WIDGETS) {
-                        Log.d(TAG, "Removing restored widget: id=" + item.appWidgetId
-                                + " belongs to component " + item.providerName
-                                + ", as the launcher is unable to bing a new widget id");
+                    // Bind succeeded
+                    if (success) {
+                        // If the widget has a configure activity, it is still needs to set it up,
+                        // otherwise the widget is ready to go.
+                        item.restoreStatus = (appWidgetInfo.configure == null)
+                                ? LauncherAppWidgetInfo.RESTORE_COMPLETED
+                                : LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
                     }
-                    LauncherModel.deleteItemFromDatabase(this, item);
-                    return;
+
+                    LauncherModel.updateItemInDatabase(this, item);
                 }
-
-                item.appWidgetId = newWidgetId;
-
-                // If the widget has a configure activity, it is still needs to set it up, otherwise
-                // the widget is ready to go.
-                item.restoreStatus = (appWidgetInfo.configure == null)
-                        ? LauncherAppWidgetInfo.RESTORE_COMPLETED
-                        : LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
-
-                LauncherModel.updateItemInDatabase(this, item);
             } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY)
                     && (appWidgetInfo.configure == null)) {
                 // The widget was marked as UI not ready, but there is no configure activity to
@@ -4001,18 +4033,19 @@
      *
      * @param appWidgetId The app widget id
      */
-    private void completeRestoreAppWidget(final int appWidgetId) {
+    private LauncherAppWidgetInfo completeRestoreAppWidget(int appWidgetId, int finalRestoreFlag) {
         LauncherAppWidgetHostView view = mWorkspace.getWidgetForAppWidgetId(appWidgetId);
         if ((view == null) || !(view instanceof PendingAppWidgetHostView)) {
             Log.e(TAG, "Widget update called, when the widget no longer exists.");
-            return;
+            return null;
         }
 
         LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) view.getTag();
-        info.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED;
+        info.restoreStatus = finalRestoreFlag;
 
         mWorkspace.reinflateWidgetsIfNecessary();
         LauncherModel.updateItemInDatabase(this, info);
+        return info;
     }
 
     public void onPageBoundSynchronously(int page) {
diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/LauncherAppWidgetInfo.java
index 42d6468..99210fd 100644
--- a/src/com/android/launcher3/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/LauncherAppWidgetInfo.java
@@ -51,6 +51,12 @@
     public static final int FLAG_RESTORE_STARTED = 8;
 
     /**
+     * Indicates that the widget has been allocated an Id. The id is still not valid, as it has
+     * not been bound yet.
+     */
+    public static final int FLAG_ID_ALLOCATED = 16;
+
+    /**
      * Indicates that the widget hasn't been instantiated yet.
      */
     static final int NO_ID = -1;
@@ -66,7 +72,7 @@
      */
     int appWidgetId = NO_ID;
 
-    ComponentName providerName;
+    public ComponentName providerName;
 
     /**
      * Indicates the restore status of the widget.
@@ -127,8 +133,9 @@
         return "AppWidget(id=" + Integer.toString(appWidgetId) + ")";
     }
 
-    public final boolean isWidgetIdValid() {
-        return (restoreStatus & FLAG_ID_NOT_VALID) == 0;
+    public final boolean isWidgetIdAllocated() {
+        return (restoreStatus & FLAG_ID_NOT_VALID) == 0 ||
+                (restoreStatus & FLAG_ID_ALLOCATED) == FLAG_ID_ALLOCATED;
     }
 
     public final boolean hasRestoreFlag(int flag) {
diff --git a/src/com/android/launcher3/LauncherClings.java b/src/com/android/launcher3/LauncherClings.java
index c44969f..f0e9593 100644
--- a/src/com/android/launcher3/LauncherClings.java
+++ b/src/com/android/launcher3/LauncherClings.java
@@ -38,7 +38,7 @@
 
 import com.android.launcher3.util.Thunk;
 
-class LauncherClings implements OnClickListener, OnKeyListener {
+public class LauncherClings implements OnClickListener, OnKeyListener {
     private static final String WORKSPACE_CLING_DISMISSED_KEY = "cling_gel.workspace.dismissed";
 
     private static final String TAG_CROP_TOP_AND_SIDES = "crop_bg_top_and_sides";
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index f9cb9ed..649f42d 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -61,6 +61,7 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.CursorIconInfo;
 import com.android.launcher3.util.FlagOp;
+import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.LongArrayMap;
 import com.android.launcher3.util.ManagedProfileHeuristic;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -374,21 +375,14 @@
             int[] xy, int spanX, int spanY) {
         LauncherAppState app = LauncherAppState.getInstance();
         InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
-        final int xCount = (int) profile.numColumns;
-        final int yCount = (int) profile.numRows;
-        boolean[][] occupied = new boolean[xCount][yCount];
+
+        GridOccupancy occupied = new GridOccupancy(profile.numColumns, profile.numRows);
         if (occupiedPos != null) {
             for (ItemInfo r : occupiedPos) {
-                int right = r.cellX + r.spanX;
-                int bottom = r.cellY + r.spanY;
-                for (int x = r.cellX; 0 <= x && x < right && x < xCount; x++) {
-                    for (int y = r.cellY; 0 <= y && y < bottom && y < yCount; y++) {
-                        occupied[x][y] = true;
-                    }
-                }
+                occupied.markCells(r, true);
             }
         }
-        return Utilities.findVacantCell(xy, spanX, spanY, xCount, yCount, occupied);
+        return occupied.findVacantCell(xy, spanX, spanY);
     }
 
     /**
@@ -1468,12 +1462,10 @@
         }
 
         // check & update map of what's occupied; used to discard overlapping/invalid items
-        private boolean checkItemPlacement(LongArrayMap<ItemInfo[][]> occupied, ItemInfo item,
+        private boolean checkItemPlacement(LongArrayMap<GridOccupancy> occupied, ItemInfo item,
                    ArrayList<Long> workspaceScreens) {
             LauncherAppState app = LauncherAppState.getInstance();
             InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
-            final int countX = profile.numColumns;
-            final int countY = profile.numRows;
 
             long containerIndex = item.screenId;
             if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
@@ -1486,7 +1478,7 @@
                     return false;
                 }
 
-                final ItemInfo[][] hotseatItems =
+                final GridOccupancy hotseatOccupancy =
                         occupied.get((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT);
 
                 if (item.screenId >= profile.numHotseatIcons) {
@@ -1497,22 +1489,20 @@
                     return false;
                 }
 
-                if (hotseatItems != null) {
-                    if (hotseatItems[(int) item.screenId][0] != null) {
+                if (hotseatOccupancy != null) {
+                    if (hotseatOccupancy.cells[(int) item.screenId][0]) {
                         Log.e(TAG, "Error loading shortcut into hotseat " + item
                                 + " into position (" + item.screenId + ":" + item.cellX + ","
-                                + item.cellY + ") occupied by "
-                                + occupied.get(LauncherSettings.Favorites.CONTAINER_HOTSEAT)
-                                [(int) item.screenId][0]);
+                                + item.cellY + ") already occupied");
                             return false;
                     } else {
-                        hotseatItems[(int) item.screenId][0] = item;
+                        hotseatOccupancy.cells[(int) item.screenId][0] = true;
                         return true;
                     }
                 } else {
-                    final ItemInfo[][] items = new ItemInfo[(int) profile.numHotseatIcons][1];
-                    items[(int) item.screenId][0] = item;
-                    occupied.put((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT, items);
+                    final GridOccupancy occupancy = new GridOccupancy(profile.numHotseatIcons, 1);
+                    occupancy.cells[(int) item.screenId][0] = true;
+                    occupied.put((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT, occupancy);
                     return true;
                 }
             } else if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
@@ -1525,12 +1515,8 @@
                 return true;
             }
 
-            if (!occupied.containsKey(item.screenId)) {
-                ItemInfo[][] items = new ItemInfo[countX + 1][countY + 1];
-                occupied.put(item.screenId, items);
-            }
-
-            final ItemInfo[][] screens = occupied.get(item.screenId);
+            final int countX = profile.numColumns;
+            final int countY = profile.numRows;
             if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
                     item.cellX < 0 || item.cellY < 0 ||
                     item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) {
@@ -1541,26 +1527,22 @@
                 return false;
             }
 
-            // Check if any workspace icons overlap with each other
-            for (int x = item.cellX; x < (item.cellX+item.spanX); x++) {
-                for (int y = item.cellY; y < (item.cellY+item.spanY); y++) {
-                    if (screens[x][y] != null) {
-                        Log.e(TAG, "Error loading shortcut " + item
-                            + " into cell (" + containerIndex + "-" + item.screenId + ":"
-                            + x + "," + y
-                            + ") occupied by "
-                            + screens[x][y]);
-                        return false;
-                    }
-                }
+            if (!occupied.containsKey(item.screenId)) {
+                occupied.put(item.screenId, new GridOccupancy(countX + 1, countY + 1));
             }
-            for (int x = item.cellX; x < (item.cellX+item.spanX); x++) {
-                for (int y = item.cellY; y < (item.cellY+item.spanY); y++) {
-                    screens[x][y] = item;
-                }
-            }
+            final GridOccupancy occupancy = occupied.get(item.screenId);
 
-            return true;
+            // Check if any workspace icons overlap with each other
+            if (occupancy.isRegionVacant(item.cellX, item.cellY, item.spanX, item.spanY)) {
+                occupancy.markCells(item, true);
+                return true;
+            } else {
+                Log.e(TAG, "Error loading shortcut " + item
+                        + " into cell (" + containerIndex + "-" + item.screenId + ":"
+                        + item.cellX + "," + item.cellX + "," + item.spanX + "," + item.spanY
+                        + ") already occupied");
+                return false;
+            }
         }
 
         /** Clears all the sBg data structures */
@@ -1621,7 +1603,7 @@
                 // +1 for the hotseat (it can be larger than the workspace)
                 // Load workspace in reverse order to ensure that latest items are loaded first (and
                 // before any earlier duplicates)
-                final LongArrayMap<ItemInfo[][]> occupied = new LongArrayMap<>();
+                final LongArrayMap<GridOccupancy> occupied = new LongArrayMap<>();
                 HashMap<ComponentKey, AppWidgetProviderInfo> widgetProvidersMap = null;
 
                 try {
@@ -2185,14 +2167,6 @@
                             if (screenId > 0) {
                                 line += " | ";
                             }
-                            ItemInfo[][] screen = occupied.valueAt(i);
-                            for (int x = 0; x < countX; x++) {
-                                if (x < screen.length && y < screen[x].length) {
-                                    line += (screen[x][y] != null) ? "#" : ".";
-                                } else {
-                                    line += "!";
-                                }
-                            }
                         }
                         Log.d(TAG, "[ " + line + " ]");
                     }
diff --git a/src/com/android/launcher3/PendingAppWidgetHostView.java b/src/com/android/launcher3/PendingAppWidgetHostView.java
index 1c02904..a455026 100644
--- a/src/com/android/launcher3/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/PendingAppWidgetHostView.java
@@ -189,9 +189,19 @@
         }
     }
 
+    /**
+     * A pending widget is ready for setup after the provider is installed and
+     *   1) Widget id is not valid: the widget id is not yet bound to the provider, probably
+     *                              because the launcher doesn't have appropriate permissions.
+     *                              Note that we would still have an allocated id as that does not
+     *                              require any permissions and can be done during view inflation.
+     *   2) UI is not ready: the id is valid and the bound. But the widget has a configure activity
+     *                       which needs to be called once.
+     */
     public boolean isReadyForClickSetup() {
-        return (mInfo.restoreStatus & LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) == 0
-                && (mInfo.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0;
+        return !mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
+                && (mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY)
+                || mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID));
     }
 
     private void updateDrawableBounds() {
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 00ee387..c9d8cce 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -655,39 +655,6 @@
     }
 
     /**
-     * Find the first vacant cell, if there is one.
-     *
-     * @param vacant Holds the x and y coordinate of the vacant cell
-     * @param spanX Horizontal cell span.
-     * @param spanY Vertical cell span.
-     *
-     * @return true if a vacant cell was found
-     */
-    public static boolean findVacantCell(int[] vacant, int spanX, int spanY,
-            int xCount, int yCount, boolean[][] occupied) {
-
-        for (int y = 0; (y + spanY) <= yCount; y++) {
-            for (int x = 0; (x + spanX) <= xCount; x++) {
-                boolean available = !occupied[x][y];
-                out:            for (int i = x; i < x + spanX; i++) {
-                    for (int j = y; j < y + spanY; j++) {
-                        available = available && !occupied[i][j];
-                        if (!available) break out;
-                    }
-                }
-
-                if (available) {
-                    vacant[0] = x;
-                    vacant[1] = y;
-                    return true;
-                }
-            }
-        }
-
-        return false;
-    }
-
-    /**
      * Trims the string, removing all whitespace at the beginning and end of the string.
      * Non-breaking whitespaces are also removed.
      */
@@ -849,7 +816,7 @@
         if (isNycOrAbove()) {
             try {
                 WallpaperManager wm = context.getSystemService(WallpaperManager.class);
-                return (Boolean) wm.getClass().getDeclaredMethod("isWallpaperSettingAllowed")
+                return (Boolean) wm.getClass().getDeclaredMethod("isSetWallpaperAllowed")
                         .invoke(wm);
             } catch (Exception e) { }
         }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 62c1bc8..0f0673a 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -3938,7 +3938,7 @@
         });
     }
 
-    private View getFirstMatch(final ItemOperator operator) {
+    public View getFirstMatch(final ItemOperator operator) {
         final View[] value = new View[1];
         mapOverItems(MAP_NO_RECURSE, new ItemOperator() {
             @Override
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java
index e054734..f900790 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTask.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.backup.nano.BackupProtos;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.PackageInstallerCompat;
+import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.LongArrayMap;
 
 import java.util.ArrayList;
@@ -220,7 +221,7 @@
                 // {@link #mCarryOver}, to prevent an infinite loop. If no item could be removed,
                 // break the loop and abort migration by throwing an exception.
                 OptimalPlacementSolution placement = new OptimalPlacementSolution(
-                        new boolean[mTrgX][mTrgY], deepCopy(mCarryOver), true);
+                        new GridOccupancy(mTrgX, mTrgY), deepCopy(mCarryOver), true);
                 placement.find();
                 if (placement.finalPlacedItems.size() > 0) {
                     long newScreenId = LauncherSettings.Settings.call(
@@ -336,9 +337,9 @@
 
         if (!mCarryOver.isEmpty() && removeWt == 0) {
             // No new items were removed in this step. Try placing all the items on this screen.
-            boolean[][] occupied = new boolean[mTrgX][mTrgY];
+            GridOccupancy occupied = new GridOccupancy(mTrgX, mTrgY);
             for (DbEntry item : finalItems) {
-                markCells(occupied, item, true);
+                occupied.markCells(item, true);
             }
 
             OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied,
@@ -376,7 +377,7 @@
      */
     private ArrayList<DbEntry> tryRemove(int col, int row, ArrayList<DbEntry> items,
             float[] outLoss) {
-        boolean[][] occupied = new boolean[mTrgX][mTrgY];
+        GridOccupancy occupied = new GridOccupancy(mTrgX, mTrgY);
 
         col = mShouldRemoveX ? col : Integer.MAX_VALUE;
         row = mShouldRemoveY ? row : Integer.MAX_VALUE;
@@ -394,7 +395,7 @@
                 if (item.cellX > col) item.cellX --;
                 if (item.cellY > row) item.cellY --;
                 finalItems.add(item);
-                markCells(occupied, item, true);
+                occupied.markCells(item, true);
             }
         }
 
@@ -406,31 +407,9 @@
         return finalItems;
     }
 
-    private void markCells(boolean[][] occupied, DbEntry item, boolean val) {
-        for (int i = item.cellX; i < (item.cellX + item.spanX); i++) {
-            for (int j = item.cellY; j < (item.cellY + item.spanY); j++) {
-                occupied[i][j] = val;
-            }
-        }
-    }
-
-    private boolean isVacant(boolean[][] occupied, int x, int y, int w, int h) {
-        if (x + w > mTrgX) return false;
-        if (y + h > mTrgY) return false;
-
-        for (int i = 0; i < w; i++) {
-            for (int j = 0; j < h; j++) {
-                if (occupied[i + x][j + y]) {
-                    return false;
-                }
-            }
-        }
-        return true;
-    }
-
     private class OptimalPlacementSolution {
         private final ArrayList<DbEntry> itemsToPlace;
-        private final boolean[][] occupied;
+        private final GridOccupancy occupied;
 
         // If set to true, item movement are not considered in move cost, leading to a more
         // linear placement.
@@ -440,11 +419,11 @@
         float lowestMoveCost = Float.MAX_VALUE;
         ArrayList<DbEntry> finalPlacedItems;
 
-        public OptimalPlacementSolution(boolean[][] occupied, ArrayList<DbEntry> itemsToPlace) {
+        public OptimalPlacementSolution(GridOccupancy occupied, ArrayList<DbEntry> itemsToPlace) {
             this(occupied, itemsToPlace, false);
         }
 
-        public OptimalPlacementSolution(boolean[][] occupied, ArrayList<DbEntry> itemsToPlace,
+        public OptimalPlacementSolution(GridOccupancy occupied, ArrayList<DbEntry> itemsToPlace,
                 boolean ignoreMove) {
             this.occupied = occupied;
             this.itemsToPlace = itemsToPlace;
@@ -513,42 +492,42 @@
                             newMoveCost = moveCost;
                         }
 
-                        if (isVacant(occupied, x, y, myW, myH)) {
+                        if (occupied.isRegionVacant(x, y, myW, myH)) {
                             // place at this position and continue search.
-                            markCells(occupied, me, true);
+                            occupied.markCells(me, true);
                             find(index + 1, weightLoss, newMoveCost, itemsIncludingMe);
-                            markCells(occupied, me, false);
+                            occupied.markCells(me, false);
                         }
 
                         // Try resizing horizontally
-                        if (myW > me.minSpanX && isVacant(occupied, x, y, myW - 1, myH)) {
+                        if (myW > me.minSpanX && occupied.isRegionVacant(x, y, myW - 1, myH)) {
                             me.spanX --;
-                            markCells(occupied, me, true);
+                            occupied.markCells(me, true);
                             // 1 extra move cost
                             find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
-                            markCells(occupied, me, false);
+                            occupied.markCells(me, false);
                             me.spanX ++;
                         }
 
                         // Try resizing vertically
-                        if (myH > me.minSpanY && isVacant(occupied, x, y, myW, myH - 1)) {
+                        if (myH > me.minSpanY && occupied.isRegionVacant(x, y, myW, myH - 1)) {
                             me.spanY --;
-                            markCells(occupied, me, true);
+                            occupied.markCells(me, true);
                             // 1 extra move cost
                             find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
-                            markCells(occupied, me, false);
+                            occupied.markCells(me, false);
                             me.spanY ++;
                         }
 
                         // Try resizing horizontally & vertically
                         if (myH > me.minSpanY && myW > me.minSpanX &&
-                                isVacant(occupied, x, y, myW - 1, myH - 1)) {
+                                occupied.isRegionVacant(x, y, myW - 1, myH - 1)) {
                             me.spanX --;
                             me.spanY --;
-                            markCells(occupied, me, true);
+                            occupied.markCells(me, true);
                             // 2 extra move cost
                             find(index + 1, weightLoss, newMoveCost + 2, itemsIncludingMe);
-                            markCells(occupied, me, false);
+                            occupied.markCells(me, false);
                             me.spanX ++;
                             me.spanY ++;
                         }
@@ -570,7 +549,7 @@
 
                 for (int y = 0; y < mTrgY; y++) {
                     for (int x = 0; x < mTrgX; x++) {
-                        if (!occupied[x][y]) {
+                        if (!occupied.cells[x][y]) {
                             int dist = ignoreMove ? 0 :
                                 ((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY - y));
                             if (dist < newDistance) {
@@ -595,9 +574,9 @@
                     if (ignoreMove) {
                         newMoveCost = moveCost;
                     }
-                    markCells(occupied, me, true);
+                    occupied.markCells(me, true);
                     find(index + 1, weightLoss, newMoveCost, itemsIncludingMe);
-                    markCells(occupied, me, false);
+                    occupied.markCells(me, false);
                     me.cellX = myX;
                     me.cellY = myY;
 
diff --git a/src/com/android/launcher3/util/CellAndSpan.java b/src/com/android/launcher3/util/CellAndSpan.java
new file mode 100644
index 0000000..3a0a6cd
--- /dev/null
+++ b/src/com/android/launcher3/util/CellAndSpan.java
@@ -0,0 +1,48 @@
+package com.android.launcher3.util;
+
+/**
+ * Base class which represents an area on the grid.
+ */
+public class CellAndSpan {
+
+    /**
+     * Indicates the X position of the associated cell.
+     */
+    public int cellX = -1;
+
+    /**
+     * Indicates the Y position of the associated cell.
+     */
+    public int cellY = -1;
+
+    /**
+     * Indicates the X cell span.
+     */
+    public int spanX = 1;
+
+    /**
+     * Indicates the Y cell span.
+     */
+    public int spanY = 1;
+
+    public CellAndSpan() {
+    }
+
+    public void copyFrom(CellAndSpan copy) {
+        cellX = copy.cellX;
+        cellY = copy.cellY;
+        spanX = copy.spanX;
+        spanY = copy.spanY;
+    }
+
+    public CellAndSpan(int cellX, int cellY, int spanX, int spanY) {
+        this.cellX = cellX;
+        this.cellY = cellY;
+        this.spanX = spanX;
+        this.spanY = spanY;
+    }
+
+    public String toString() {
+        return "(" + cellX + ", " + cellY + ": " + spanX + ", " + spanY + ")";
+    }
+}
diff --git a/src/com/android/launcher3/util/GridOccupancy.java b/src/com/android/launcher3/util/GridOccupancy.java
new file mode 100644
index 0000000..3f5f0b4
--- /dev/null
+++ b/src/com/android/launcher3/util/GridOccupancy.java
@@ -0,0 +1,101 @@
+package com.android.launcher3.util;
+
+import android.graphics.Rect;
+
+import com.android.launcher3.ItemInfo;
+
+/**
+ * Utility object to manage the occupancy in a grid.
+ */
+public class GridOccupancy {
+
+    private final int mCountX;
+    private final int mCountY;
+
+    public final boolean[][] cells;
+
+    public GridOccupancy(int countX, int countY) {
+        mCountX = countX;
+        mCountY = countY;
+        cells = new boolean[countX][countY];
+    }
+
+    /**
+     * Find the first vacant cell, if there is one.
+     *
+     * @param vacantOut Holds the x and y coordinate of the vacant cell
+     * @param spanX Horizontal cell span.
+     * @param spanY Vertical cell span.
+     *
+     * @return true if a vacant cell was found
+     */
+    public boolean findVacantCell(int[] vacantOut, int spanX, int spanY) {
+        for (int y = 0; (y + spanY) <= mCountY; y++) {
+            for (int x = 0; (x + spanX) <= mCountX; x++) {
+                boolean available = !cells[x][y];
+                out:
+                for (int i = x; i < x + spanX; i++) {
+                    for (int j = y; j < y + spanY; j++) {
+                        available = available && !cells[i][j];
+                        if (!available) break out;
+                    }
+                }
+                if (available) {
+                    vacantOut[0] = x;
+                    vacantOut[1] = y;
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public void copyTo(GridOccupancy dest) {
+        for (int i = 0; i < mCountX; i++) {
+            for (int j = 0; j < mCountY; j++) {
+                dest.cells[i][j] = cells[i][j];
+            }
+        }
+    }
+
+    public boolean isRegionVacant(int x, int y, int spanX, int spanY) {
+        int x2 = x + spanX - 1;
+        int y2 = y + spanY - 1;
+        if (x < 0 || y < 0 || x2 >= mCountX || y2 >= mCountY) {
+            return false;
+        }
+        for (int i = x; i <= x2; i++) {
+            for (int j = y; j <= y2; j++) {
+                if (cells[i][j]) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    public void markCells(int cellX, int cellY, int spanX, int spanY, boolean value) {
+        if (cellX < 0 || cellY < 0) return;
+        for (int x = cellX; x < cellX + spanX && x < mCountX; x++) {
+            for (int y = cellY; y < cellY + spanY && y < mCountX; y++) {
+                cells[x][y] = value;
+            }
+        }
+    }
+
+    public void markCells(Rect r, boolean value) {
+        markCells(r.left, r.top, r.width(), r.height(), value);
+    }
+
+    public void markCells(CellAndSpan cell, boolean value) {
+        markCells(cell.cellX, cell.cellY, cell.spanX, cell.spanY, value);
+    }
+
+    public void markCells(ItemInfo item, boolean value) {
+        markCells(item.cellX, item.cellY, item.spanX, item.spanY, value);
+    }
+
+    public void clear() {
+        markCells(0, 0, mCountX, mCountY, false);
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 9ec0340..3ad03ae 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -207,4 +207,9 @@
         }
         return "";
     }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return WidgetCell.class.getName();
+    }
 }
diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java
index 23d0433..a76f0af 100644
--- a/src/com/android/launcher3/widget/WidgetsContainerView.java
+++ b/src/com/android/launcher3/widget/WidgetsContainerView.java
@@ -26,6 +26,7 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.Toast;
 
 import com.android.launcher3.BaseContainerView;
@@ -329,6 +330,11 @@
         mRecyclerView.setWidgets(model);
         mAdapter.setWidgetsModel(model);
         mAdapter.notifyDataSetChanged();
+
+        View loader = getContentView().findViewById(R.id.loader);
+        if (loader != null) {
+            ((ViewGroup) getContentView()).removeView(loader);
+        }
     }
 
     public boolean isEmpty() {
diff --git a/tests/src/com/android/launcher3/BindWidgetTest.java b/tests/src/com/android/launcher3/BindWidgetTest.java
index 34b1174..81cb8b5 100644
--- a/tests/src/com/android/launcher3/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/BindWidgetTest.java
@@ -1,38 +1,30 @@
 package com.android.launcher3;
 
 import android.annotation.TargetApi;
-import android.app.SearchManager;
 import android.appwidget.AppWidgetHost;
-import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionParams;
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.ParcelFileDescriptor;
-import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiSelector;
-import android.test.InstrumentationTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
 
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.PackageInstallerCompat;
+import com.android.launcher3.ui.LauncherInstrumentationTestCase;
 import com.android.launcher3.util.ManagedProfileHeuristic;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.WidgetHostViewLoader;
 
-import java.io.FileInputStream;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Tests for bind widget flow.
@@ -41,12 +33,8 @@
  */
 @LargeTest
 @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-public class BindWidgetTest extends InstrumentationTestCase {
+public class BindWidgetTest extends LauncherInstrumentationTestCase {
 
-    private static final long DEFAULT_TIMEOUT = 6000;
-
-    private UiDevice mDevice;
-    private Context mTargetContext;
     private ContentResolver mResolver;
     private AppWidgetManagerCompat mWidgetManager;
 
@@ -59,23 +47,9 @@
     protected void setUp() throws Exception {
         super.setUp();
 
-        mDevice = UiDevice.getInstance(getInstrumentation());
-        mTargetContext = getInstrumentation().getTargetContext();
         mResolver = mTargetContext.getContentResolver();
         mWidgetManager = AppWidgetManagerCompat.getInstance(mTargetContext);
-
-        // Check bind widget permission
-        String pkg = mTargetContext.getPackageName();
-        if (mTargetContext.getPackageManager().checkPermission(
-                pkg, android.Manifest.permission.BIND_APPWIDGET)
-                != PackageManager.PERMISSION_GRANTED) {
-            ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation().executeShellCommand(
-                    "appwidget grantbind --package " + pkg);
-            // Read the input stream fully.
-            FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
-            while (fis.read() != -1);
-            fis.close();
-        }
+        grantWidgetPermission();
 
         // Clear all existing data
         LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
@@ -264,61 +238,14 @@
             throw new IllegalArgumentException(t);
         }
         // Launch the home activity
-        getInstrumentation().getContext().startActivity(new Intent(Intent.ACTION_MAIN)
-                .addCategory(Intent.CATEGORY_HOME)
-                .setPackage(mTargetContext.getPackageName())
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
-
+        startLauncher();
         // Verify UI
         UiSelector selector = new UiSelector().packageName(mTargetContext.getPackageName())
                 .className(widgetClass);
         if (desc != null) {
             selector = selector.description(desc);
         }
-        assertTrue(mDevice.findObject(selector).waitForExists(DEFAULT_TIMEOUT));
-    }
-
-    /**
-     * Finds a widget provider which can fit on the home screen.
-     * @param hasConfigureScreen if true, a provider with a config screen is returned.
-     */
-    private LauncherAppWidgetProviderInfo findWidgetProvider(final boolean hasConfigureScreen) {
-        LauncherAppWidgetProviderInfo info = getOnUiThread(new Callable<LauncherAppWidgetProviderInfo>() {
-            @Override
-            public LauncherAppWidgetProviderInfo call() throws Exception {
-                InvariantDeviceProfile idv =
-                        LauncherAppState.getInstance().getInvariantDeviceProfile();
-
-                ComponentName searchComponent = ((SearchManager) mTargetContext
-                        .getSystemService(Context.SEARCH_SERVICE)).getGlobalSearchActivity();
-                String searchPackage = searchComponent == null
-                        ? null : searchComponent.getPackageName();
-
-                for (AppWidgetProviderInfo info :
-                        AppWidgetManagerCompat.getInstance(mTargetContext).getAllProviders()) {
-                    if ((info.configure != null) ^ hasConfigureScreen) {
-                        continue;
-                    }
-                    // Exclude the widgets in search package, as Launcher already binds them in
-                    // QSB, so they can cause conflicts.
-                    if (info.provider.getPackageName().equals(searchPackage)) {
-                        continue;
-                    }
-                    LauncherAppWidgetProviderInfo widgetInfo = LauncherAppWidgetProviderInfo
-                            .fromProviderInfo(mTargetContext, info);
-                    if (widgetInfo.minSpanX >= idv.numColumns
-                            || widgetInfo.minSpanY >= idv.numRows) {
-                        continue;
-                    }
-                    return widgetInfo;
-                }
-                return null;
-            }
-        });
-        if (info == null) {
-            throw new IllegalArgumentException("No valid widget provider");
-        }
-        return info;
+        assertTrue(mDevice.findObject(selector).waitForExists(DEFAULT_UI_TIMEOUT));
     }
 
     /**
@@ -398,24 +325,6 @@
     }
 
     /**
-     * Runs the callback on the UI thread and returns the result.
-     */
-    private <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();
-    }
-
-    /**
      * Blocks the current thread until all the jobs in the main worker thread are complete.
      */
     private void waitUntilLoaderIdle() throws InterruptedException {
diff --git a/tests/src/com/android/launcher3/QuickAddWidgetTest.java b/tests/src/com/android/launcher3/QuickAddWidgetTest.java
deleted file mode 100644
index 0078294..0000000
--- a/tests/src/com/android/launcher3/QuickAddWidgetTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.android.launcher3;
-
-import android.content.Intent;
-import android.graphics.Point;
-import android.os.SystemClock;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.Direction;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.Until;
-import android.test.InstrumentationTestCase;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.view.MotionEvent;
-import android.view.ViewConfiguration;
-
-import java.util.List;
-
-/**
- * Add an arbitrary widget from the widget picker very quickly to test potential race conditions.
- */
-@LargeTest
-public class QuickAddWidgetTest extends InstrumentationTestCase {
-    // Disabled because it's flaky and not particularly useful. But this class could still be useful
-    // as an example if we want other UI tests in the future.
-    private static final boolean DISABLED = true;
-
-    private UiDevice mDevice;
-    private String mTargetPackage;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        mDevice = UiDevice.getInstance(getInstrumentation());
-
-        // Set Launcher3 as home.
-        mTargetPackage = getInstrumentation().getTargetContext().getPackageName();
-        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
-                .addCategory(Intent.CATEGORY_HOME)
-                .setPackage(mTargetPackage)
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        getInstrumentation().getContext().startActivity(homeIntent);
-        mDevice.wait(Until.hasObject(By.pkg(mTargetPackage).depth(0)), 3000);
-    }
-
-    public void testAddWidgetQuickly() throws Exception {
-        if (DISABLED) return;
-        mDevice.pressMenu(); // Enter overview mode.
-        mDevice.wait(Until.findObject(By.text("Widgets")), 3000).click();
-        UiObject2 calendarWidget = getWidgetByName("Clock");
-        Point center = calendarWidget.getVisibleCenter();
-        // Touch widget just long enough to pick it up (longPressTimeout), then let go immediately.
-        getInstrumentation().sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(),
-                SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, center.x, center.y, 0));
-        Thread.sleep(ViewConfiguration.getLongPressTimeout() + 50);
-        getInstrumentation().sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(),
-                SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, center.x, center.y, 0));
-
-        assertTrue("Drag was never started", isOnHomescreen());
-    }
-
-    private UiObject2 getWidgetByName(String name) {
-        UiObject2 widgetsList = mDevice.wait(Until.findObject(By.res(mTargetPackage,
-                "widgets_list_view")), 3000);
-        do {
-            UiObject2 widget = getVisibleWidgetByName(name);
-            if (widget != null) {
-                return widget;
-            }
-        } while (widgetsList.scroll(Direction.DOWN, 1f));
-        return getVisibleWidgetByName(name);
-    }
-
-    private UiObject2 getVisibleWidgetByName(String name) {
-        List<UiObject2> visibleWidgets = mDevice.wait(Until.findObjects(By.clazz(
-                "android.widget.LinearLayout")), 3000);
-        for (UiObject2 widget : visibleWidgets) {
-            if (widget.hasObject(By.text(name))) {
-                return widget;
-            }
-        }
-        return null;
-    }
-
-    private boolean isOnHomescreen() {
-        return mDevice.wait(Until.hasObject(By.res(mTargetPackage, "hotseat")), 3000);
-    }
-}
diff --git a/tests/src/com/android/launcher3/ui/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/AddWidgetTest.java
new file mode 100644
index 0000000..a0ca60c
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/AddWidgetTest.java
@@ -0,0 +1,64 @@
+package com.android.launcher3.ui;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.View;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppWidgetInfo;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.Workspace.ItemOperator;
+import com.android.launcher3.util.Condition;
+import com.android.launcher3.util.Wait;
+import com.android.launcher3.widget.WidgetCell;
+
+/**
+ * Test to add widget from widget tray
+ */
+@LargeTest
+public class AddWidgetTest extends LauncherInstrumentationTestCase {
+
+    private LauncherAppWidgetProviderInfo widgetInfo;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        widgetInfo = findWidgetProvider(false /* hasConfigureScreen */);
+    }
+
+    public void testDragIcon_portrait() throws Throwable {
+        lockRotation(true);
+        performTest();
+    }
+
+    public void testDragIcon_landscape() throws Throwable {
+        lockRotation(false);
+        performTest();
+    }
+
+    private void performTest() throws Throwable {
+        clearHomescreen();
+        Launcher launcher = 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
+        UiObject2 widget = scrollAndFind(widgetContainer, By.clazz(WidgetCell.class)
+                .hasDescendant(By.text(widgetInfo.getLabel(mTargetContext.getPackageManager()))));
+        dragToWorkspace(widget);
+
+        assertNotNull(launcher.getWorkspace().getFirstMatch(new ItemOperator() {
+            @Override
+            public boolean evaluate(ItemInfo info, View view) {
+                return info instanceof LauncherAppWidgetInfo &&
+                        ((LauncherAppWidgetInfo) info).providerName.equals(widgetInfo.provider);
+            }
+        }));
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/AllAppsAppLaunchTest.java b/tests/src/com/android/launcher3/ui/AllAppsAppLaunchTest.java
new file mode 100644
index 0000000..abe6b95
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/AllAppsAppLaunchTest.java
@@ -0,0 +1,52 @@
+package com.android.launcher3.ui;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.launcher3.compat.LauncherActivityInfoCompat;
+import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.util.Condition;
+import com.android.launcher3.util.Wait;
+
+/**
+ * Test for verifying apps is launched from all-apps
+ */
+@LargeTest
+public class AllAppsAppLaunchTest extends LauncherInstrumentationTestCase {
+
+    private LauncherActivityInfoCompat mSettingsApp;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mSettingsApp = LauncherAppsCompat.getInstance(mTargetContext)
+                .getActivityList("com.android.settings", UserHandleCompat.myUserHandle()).get(0);
+    }
+
+    public void testAppLauncher_portrait() throws Exception {
+        lockRotation(true);
+        performTest();
+    }
+
+    public void testAppLauncher_landscape() throws Exception {
+        lockRotation(false);
+        performTest();
+    }
+
+    private void performTest() throws Exception {
+        startLauncher();
+
+        // Open all apps and wait for load complete
+        final UiObject2 appsContainer = openAllApps();
+        assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT));
+
+        // Open settings app and verify app launched
+        scrollAndFind(appsContainer, By.text(mSettingsApp.getLabel().toString())).click();
+        assertTrue(mDevice.wait(Until.hasObject(By.pkg(
+                mSettingsApp.getComponentName().getPackageName()).depth(0)), DEFAULT_UI_TIMEOUT));
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java b/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
new file mode 100644
index 0000000..56fc90a
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
@@ -0,0 +1,57 @@
+package com.android.launcher3.ui;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.launcher3.compat.LauncherActivityInfoCompat;
+import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.util.Condition;
+import com.android.launcher3.util.Wait;
+
+/**
+ * Test for dragging an icon from all-apps to homescreen.
+ */
+@LargeTest
+public class AllAppsIconToHomeTest extends LauncherInstrumentationTestCase {
+
+    private LauncherActivityInfoCompat mSettingsApp;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mSettingsApp = LauncherAppsCompat.getInstance(mTargetContext)
+                .getActivityList("com.android.settings", UserHandleCompat.myUserHandle()).get(0);
+    }
+
+    public void testDragIcon_portrait() throws Throwable {
+        lockRotation(true);
+        performTest();
+    }
+
+    public void testDragIcon_landscape() throws Throwable {
+        lockRotation(false);
+        performTest();
+    }
+
+    private void performTest() throws Throwable {
+        clearHomescreen();
+        startLauncher();
+
+        // Open all apps and wait for load complete.
+        final UiObject2 appsContainer = openAllApps();
+        assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT));
+
+        // Drag icon to homescreen.
+        UiObject2 icon = scrollAndFind(appsContainer, By.text(mSettingsApp.getLabel().toString()));
+        dragToWorkspace(icon);
+
+        // Verify that the icon works on homescreen.
+        mDevice.findObject(By.text(mSettingsApp.getLabel().toString())).click();
+        assertTrue(mDevice.wait(Until.hasObject(By.pkg(
+                mSettingsApp.getComponentName().getPackageName()).depth(0)), DEFAULT_UI_TIMEOUT));
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java b/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
new file mode 100644
index 0000000..a59f0ff
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
@@ -0,0 +1,266 @@
+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.RemoteException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.view.MotionEvent;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.LauncherClings;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.compat.AppWidgetManagerCompat;
+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.
+ */
+public class LauncherInstrumentationTestCase extends InstrumentationTestCase {
+
+    public static final long DEFAULT_UI_TIMEOUT = 3000;
+
+    protected UiDevice mDevice;
+    protected Context mTargetContext;
+    protected String mTargetPackage;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mTargetContext = getInstrumentation().getTargetContext();
+        mTargetPackage = mTargetContext.getPackageName();
+    }
+
+    protected void lockRotation(boolean naturalOrientation) throws RemoteException {
+        Utilities.getPrefs(mTargetContext)
+                .edit()
+                .putBoolean(Utilities.ALLOW_ROTATION_PREFERENCE_KEY, !naturalOrientation)
+                .commit();
+
+        if (naturalOrientation) {
+            mDevice.setOrientationNatural();
+        } else {
+            mDevice.setOrientationRight();
+        }
+    }
+
+    /**
+     * Starts the launcher activity in the target package and returns the Launcher instance.
+     */
+    protected Launcher startLauncher() {
+        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setPackage(mTargetPackage)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        return (Launcher) getInstrumentation().startActivitySync(homeIntent);
+    }
+
+    /**
+     * Grants the launcher permission to bind widgets.
+     */
+    protected void grantWidgetPermission() throws IOException {
+        // Check bind widget permission
+        if (mTargetContext.getPackageManager().checkPermission(
+                mTargetPackage, android.Manifest.permission.BIND_APPWIDGET)
+                != PackageManager.PERMISSION_GRANTED) {
+            ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation().executeShellCommand(
+                    "appwidget grantbind --package " + mTargetPackage);
+            // Read the input stream fully.
+            FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
+            while (fis.read() != -1);
+            fis.close();
+        }
+    }
+
+    /**
+     * Opens all apps and returns the recycler view
+     */
+    protected UiObject2 openAllApps() {
+        mDevice.wait(Until.findObject(
+                By.desc(mTargetContext.getString(R.string.all_apps_button_label))), DEFAULT_UI_TIMEOUT).click();
+        return findViewById(R.id.apps_list_view);
+    }
+
+    /**
+     * Opens widget tray and returns the recycler view.
+     */
+    protected UiObject2 openWidgetsTray() {
+        mDevice.pressMenu(); // Enter overview mode.
+        mDevice.wait(Until.findObject(
+                By.text(mTargetContext.getString(R.string.widget_button_text)
+                        .toUpperCase(Locale.getDefault()))), DEFAULT_UI_TIMEOUT).click();
+        return findViewById(R.id.widgets_list_view);
+    }
+
+    /**
+     * Scrolls the {@param container} until it finds an object matching {@param condition}.
+     * @return the matching object.
+     */
+    protected UiObject2 scrollAndFind(UiObject2 container, BySelector condition) {
+        do {
+            UiObject2 widget = container.findObject(condition);
+            if (widget != null) {
+                return widget;
+            }
+        } while (container.scroll(Direction.DOWN, 1f));
+        return container.findObject(condition);
+    }
+
+    /**
+     * Drags an icon to the center of homescreen.
+     */
+    protected void dragToWorkspace(UiObject2 icon) {
+        Point center = icon.getVisibleCenter();
+
+        // Action Down
+        sendPointer(MotionEvent.ACTION_DOWN, center);
+
+        // Wait until "Remove/Delete target is visible
+        assertNotNull(findViewById(R.id.delete_target_text));
+
+        Point moveLocation = findViewById(R.id.drag_layer).getVisibleCenter();
+
+        // Move to center
+        while(!moveLocation.equals(center)) {
+            center.x = getNextMoveValue(moveLocation.x, center.x);
+            center.y = getNextMoveValue(moveLocation.y, center.y);
+            sendPointer(MotionEvent.ACTION_MOVE, center);
+        }
+        sendPointer(MotionEvent.ACTION_UP, center);
+
+        // Wait until remove target is gone.
+        mDevice.wait(Until.gone(getSelectorForId(R.id.delete_target_text)), DEFAULT_UI_TIMEOUT);
+    }
+
+    private int getNextMoveValue(int targetValue, int oldValue) {
+        if (targetValue - oldValue > 10) {
+            return oldValue + 10;
+        } else if (targetValue - oldValue < -10) {
+            return oldValue - 10;
+        } else {
+            return targetValue;
+        }
+    }
+
+    private void sendPointer(int action, Point point) {
+        MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(),
+                SystemClock.uptimeMillis(), action, point.x, point.y, 0);
+        getInstrumentation().sendPointerSync(event);
+        event.recycle();
+    }
+
+    /**
+     * Removes all icons from homescreen and hotseat.
+     */
+    public void clearHomescreen() throws Throwable {
+        LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
+                LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
+        LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
+                LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
+        LauncherClings.markFirstRunClingDismissed(mTargetContext);
+        ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mTargetContext);
+
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // Reset the loader state
+                LauncherAppState.getInstance().getModel().resetLoadedState(true, true);
+            }
+        });
+    }
+
+    /**
+     * 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();
+    }
+
+    /**
+     * Finds a widget provider which can fit on the home screen.
+     * @param hasConfigureScreen if true, a provider with a config screen is returned.
+     */
+    protected LauncherAppWidgetProviderInfo findWidgetProvider(final boolean hasConfigureScreen) {
+        LauncherAppWidgetProviderInfo info = getOnUiThread(new Callable<LauncherAppWidgetProviderInfo>() {
+            @Override
+            public LauncherAppWidgetProviderInfo call() throws Exception {
+                InvariantDeviceProfile idv =
+                        LauncherAppState.getInstance().getInvariantDeviceProfile();
+
+                ComponentName searchComponent = ((SearchManager) mTargetContext
+                        .getSystemService(Context.SEARCH_SERVICE)).getGlobalSearchActivity();
+                String searchPackage = searchComponent == null
+                        ? null : searchComponent.getPackageName();
+
+                for (AppWidgetProviderInfo info :
+                        AppWidgetManagerCompat.getInstance(mTargetContext).getAllProviders()) {
+                    if ((info.configure != null) ^ hasConfigureScreen) {
+                        continue;
+                    }
+                    // Exclude the widgets in search package, as Launcher already binds them in
+                    // QSB, so they can cause conflicts.
+                    if (info.provider.getPackageName().equals(searchPackage)) {
+                        continue;
+                    }
+                    LauncherAppWidgetProviderInfo widgetInfo = LauncherAppWidgetProviderInfo
+                            .fromProviderInfo(mTargetContext, info);
+                    if (widgetInfo.minSpanX >= idv.numColumns
+                            || widgetInfo.minSpanY >= idv.numRows) {
+                        continue;
+                    }
+                    return widgetInfo;
+                }
+                return null;
+            }
+        });
+        if (info == null) {
+            throw new IllegalArgumentException("No valid widget provider");
+        }
+        return info;
+    }
+
+    protected UiObject2 findViewById(int id) {
+        return mDevice.wait(Until.findObject(getSelectorForId(id)), DEFAULT_UI_TIMEOUT);
+    }
+
+    protected BySelector getSelectorForId(int id) {
+        String name = mTargetContext.getResources().getResourceEntryName(id);
+        return By.res(mTargetPackage, name);
+    }
+}
diff --git a/tests/src/com/android/launcher3/RotationPreferenceTest.java b/tests/src/com/android/launcher3/ui/RotationPreferenceTest.java
similarity index 74%
rename from tests/src/com/android/launcher3/RotationPreferenceTest.java
rename to tests/src/com/android/launcher3/ui/RotationPreferenceTest.java
index 7259d35..e84ad04 100644
--- a/tests/src/com/android/launcher3/RotationPreferenceTest.java
+++ b/tests/src/com/android/launcher3/ui/RotationPreferenceTest.java
@@ -1,24 +1,20 @@
-package com.android.launcher3;
+package com.android.launcher3.ui;
 
-import android.content.Context;
-import android.content.Intent;
 import android.content.SharedPreferences;
 import android.graphics.Rect;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject;
 import android.support.test.uiautomator.UiSelector;
-import android.test.InstrumentationTestCase;
 import android.test.suitebuilder.annotation.MediumTest;
 
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+
 /**
  * Test for auto rotate preference.
  */
 @MediumTest
-public class RotationPreferenceTest extends InstrumentationTestCase {
-
-    private UiDevice mDevice;
-    private Context mTargetContext;
-    private String mTargetPackage;
+public class RotationPreferenceTest extends LauncherInstrumentationTestCase {
 
     private SharedPreferences mPrefs;
     private boolean mOriginalRotationValue;
@@ -48,7 +44,7 @@
 
         setRotationEnabled(false);
         mDevice.setOrientationRight();
-        goToLauncher();
+        startLauncher();
 
         Rect hotseat = getHotseatBounds();
         assertTrue(hotseat.width() > hotseat.height());
@@ -62,21 +58,12 @@
 
         setRotationEnabled(true);
         mDevice.setOrientationRight();
-        goToLauncher();
+        startLauncher();
 
         Rect hotseat = getHotseatBounds();
         assertTrue(hotseat.width() < hotseat.height());
     }
 
-    private void goToLauncher() {
-        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
-                .addCategory(Intent.CATEGORY_HOME)
-                .setPackage(mTargetPackage)
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        getInstrumentation().getContext().startActivity(homeIntent);
-        mDevice.findObject(new UiSelector().packageName(mTargetPackage)).waitForExists(6000);
-    }
-
     private void setRotationEnabled(boolean enabled) {
         mPrefs.edit().putBoolean(Utilities.ALLOW_ROTATION_PREFERENCE_KEY, enabled).commit();
     }
diff --git a/tests/src/com/android/launcher3/util/Condition.java b/tests/src/com/android/launcher3/util/Condition.java
new file mode 100644
index 0000000..e9ee67c
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/Condition.java
@@ -0,0 +1,54 @@
+package com.android.launcher3.util;
+
+import android.support.test.uiautomator.UiObject2;
+
+import com.android.launcher3.MainThreadExecutor;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public abstract class Condition {
+
+    public abstract boolean isTrue() throws Throwable;
+
+    /**
+     * Converts the condition to be run on UI thread.
+     */
+    public static Condition runOnUiThread(final Condition condition) {
+        final MainThreadExecutor executor = new MainThreadExecutor();
+        return new Condition() {
+            @Override
+            public boolean isTrue() throws Throwable {
+                final AtomicBoolean value = new AtomicBoolean(false);
+                final Throwable[] exceptions = new Throwable[1];
+                final CountDownLatch latch = new CountDownLatch(1);
+                executor.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            value.set(condition.isTrue());
+                        } catch (Throwable e) {
+                            exceptions[0] = e;
+                        }
+
+                    }
+                });
+                latch.await(1, TimeUnit.SECONDS);
+                if (exceptions[0] != null) {
+                    throw exceptions[0];
+                }
+                return value.get();
+            }
+        };
+    }
+
+    public static Condition minChildCount(final UiObject2 obj, final int childCount) {
+        return new Condition() {
+            @Override
+            public boolean isTrue() {
+                return obj.getChildCount() >= childCount;
+            }
+        };
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/FocusLogicTest.java b/tests/src/com/android/launcher3/util/FocusLogicTest.java
index 386f7dd..eee567f 100644
--- a/tests/src/com/android/launcher3/util/FocusLogicTest.java
+++ b/tests/src/com/android/launcher3/util/FocusLogicTest.java
@@ -14,7 +14,7 @@
  * the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.util;
 
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
diff --git a/tests/src/com/android/launcher3/util/GridOccupancyTest.java b/tests/src/com/android/launcher3/util/GridOccupancyTest.java
new file mode 100644
index 0000000..7d0fe71
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/GridOccupancyTest.java
@@ -0,0 +1,61 @@
+package com.android.launcher3.util;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link GridOccupancy}
+ */
+@SmallTest
+public class GridOccupancyTest extends TestCase {
+
+    public void testFindVacantCell() {
+        GridOccupancy grid = initGrid(4,
+                1, 1, 1, 0, 0,
+                0, 0, 1, 1, 0,
+                0, 0, 0, 0, 0,
+                1, 1, 0, 0, 0
+                );
+
+        int[] vacant = new int[2];
+        assertTrue(grid.findVacantCell(vacant, 2, 2));
+        assertEquals(vacant[0], 0);
+        assertEquals(vacant[1], 1);
+
+        assertTrue(grid.findVacantCell(vacant, 3, 2));
+        assertEquals(vacant[0], 2);
+        assertEquals(vacant[1], 2);
+
+        assertFalse(grid.findVacantCell(vacant, 3, 3));
+    }
+
+    public void testIsRegionVacant() {
+        GridOccupancy grid = initGrid(4,
+                1, 1, 1, 0, 0,
+                0, 0, 1, 1, 0,
+                0, 0, 0, 0, 0,
+                1, 1, 0, 0, 0
+        );
+
+        assertTrue(grid.isRegionVacant(4, 0, 1, 4));
+        assertTrue(grid.isRegionVacant(0, 1, 2, 2));
+        assertTrue(grid.isRegionVacant(2, 2, 3, 2));
+
+        assertFalse(grid.isRegionVacant(3, 0, 2, 4));
+        assertFalse(grid.isRegionVacant(0, 0, 2, 1));
+    }
+
+    private GridOccupancy initGrid(int rows, int... cells) {
+        int cols = cells.length / rows;
+        int i = 0;
+        GridOccupancy grid = new GridOccupancy(cols, rows);
+        for (int y = 0; y < rows; y++) {
+            for (int x = 0; x < cols; x++) {
+                grid.cells[x][y] = cells[i] != 0;
+                i++;
+            }
+        }
+        return grid;
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/Wait.java b/tests/src/com/android/launcher3/util/Wait.java
new file mode 100644
index 0000000..02a1913
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/Wait.java
@@ -0,0 +1,30 @@
+package com.android.launcher3.util;
+
+import android.os.SystemClock;
+
+/**
+ * A utility class for waiting for a condition to be true.
+ */
+public class Wait {
+
+    private static final long DEFAULT_SLEEP_MS = 200;
+
+    public static boolean atMost(Condition condition, long timeout) {
+        return atMost(condition, timeout, DEFAULT_SLEEP_MS);
+    }
+
+    public static boolean atMost(Condition condition, long timeout, long sleepMillis) {
+        long endTime = SystemClock.uptimeMillis() + timeout;
+        while (SystemClock.uptimeMillis() < endTime) {
+            try {
+                if (condition.isTrue()) {
+                    return true;
+                }
+            } catch (Throwable t) {
+                // Ignore
+            }
+            SystemClock.sleep(sleepMillis);
+        }
+        return false;
+    }
+}