Making folders scrollable

Change-Id: Id6c9ec62acc6d86dc627d20abad3e2d92010f539
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 842037c..e115e43 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -106,6 +106,10 @@
     private Rect mForegroundRect;
     private int mForegroundPadding;
 
+    // These values allow a fixed measurement to be set on the CellLayout.
+    private int mFixedWidth = -1;
+    private int mFixedHeight = -1;
+
     // If we're actively dragging something over this screen, mIsDragOverlapping is true
     private boolean mIsDragOverlapping = false;
     private final Point mDragCenter = new Point();
@@ -972,6 +976,11 @@
         metrics.set(cellWidth, cellHeight, widthGap, heightGap);
     }
 
+    public void setFixedSize(int width, int height) {
+        mFixedWidth = width;
+        mFixedHeight = height;
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
@@ -980,7 +989,12 @@
         int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
         int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
 
-        if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
+        int newWidth = widthSpecSize;
+        int newHeight = heightSpecSize;
+        if (mFixedWidth > 0 && mFixedHeight > 0) {
+            newWidth = mFixedWidth;
+            newHeight = mFixedHeight;
+        } else if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
             throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions");
         }
 
@@ -1002,8 +1016,6 @@
         }
 
         // Initial values correspond to widthSpecMode == MeasureSpec.EXACTLY
-        int newWidth = widthSpecSize;
-        int newHeight = heightSpecSize;
         if (widthSpecMode == MeasureSpec.AT_MOST) {
             newWidth = getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth) +
                 ((mCountX - 1) * mWidthGap);
diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java
index a7b5c5c..347a1d4 100644
--- a/src/com/android/launcher3/Folder.java
+++ b/src/com/android/launcher3/Folder.java
@@ -20,6 +20,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.animation.PropertyValuesHolder;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.PointF;
@@ -42,6 +43,7 @@
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.LinearLayout;
+import android.widget.ScrollView;
 import android.widget.TextView;
 
 import com.android.launcher3.R;
@@ -70,15 +72,20 @@
 
     private int mExpandDuration;
     protected CellLayout mContent;
+    private ScrollView mScrollView;
     private final LayoutInflater mInflater;
     private final IconCache mIconCache;
     private int mState = STATE_NONE;
     private static final int REORDER_ANIMATION_DURATION = 230;
+    private static final int REORDER_DELAY = 250;
     private static final int ON_EXIT_CLOSE_DELAY = 800;
     private boolean mRearrangeOnClose = false;
     private FolderIcon mFolderIcon;
     private int mMaxCountX;
     private int mMaxCountY;
+    private int mMaxVisibleX;
+    private int mMaxVisibleY;
+    private int mMaxContentAreaHeight = 0;
     private int mMaxNumItems;
     private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
     private Drawable mIconDrawable;
@@ -101,12 +108,21 @@
     private float mFolderIconPivotX;
     private float mFolderIconPivotY;
 
+    private static final int SCROLL_CUT_OFF_AMOUNT = 60;
+    private static final int SCROLL_BAND_HEIGHT = 110;
+
     private boolean mIsEditingName = false;
     private InputMethodManager mInputMethodManager;
 
     private static String sDefaultFolderName;
     private static String sHintText;
 
+    private int DRAG_MODE_NONE = 0;
+    private int DRAG_MODE_REORDER = 1;
+    private int DRAG_MODE_SCROLL_UP = 2;
+    private int DRAG_MODE_SCROLL_DOWN = 3;
+    private int mDragMode = DRAG_MODE_NONE;
+
     private boolean mDestroyed;
 
     /**
@@ -122,13 +138,33 @@
         mIconCache = ((LauncherApplication)context.getApplicationContext()).getIconCache();
 
         Resources res = getResources();
-        mMaxCountX = res.getInteger(R.integer.folder_max_count_x);
-        mMaxCountY = res.getInteger(R.integer.folder_max_count_y);
+        mMaxCountX = mMaxVisibleX = res.getInteger(R.integer.folder_max_count_x);
+        mMaxCountY = mMaxVisibleY = res.getInteger(R.integer.folder_max_count_y);
         mMaxNumItems = res.getInteger(R.integer.folder_max_num_items);
-        if (mMaxCountX < 0 || mMaxCountY < 0 || mMaxNumItems < 0) {
-            mMaxCountX = LauncherModel.getCellCountX();
-            mMaxCountY = LauncherModel.getCellCountY();
+
+        if (mMaxCountY == -1) {
+            // -2 indicates unlimited
+            mMaxCountY = Integer.MAX_VALUE;
+            mMaxVisibleX = LauncherModel.getCellCountX() + 1;
+        }
+        if (mMaxNumItems == -1) {
+            // -2 indicates unlimited
+            mMaxNumItems = Integer.MAX_VALUE;
+            mMaxVisibleY = LauncherModel.getCellCountY() + 1;
+        }
+        if (mMaxCountX == 0) {
+            mMaxCountX = mMaxVisibleX = LauncherModel.getCellCountX();
+            mMaxVisibleX++;
+        }
+        if (mMaxCountY == 0) {
+            mMaxCountY = mMaxVisibleY = LauncherModel.getCellCountY();
+            mMaxVisibleY++;
+        }
+        if (mMaxNumItems == 0) {
             mMaxNumItems = mMaxCountX * mMaxCountY;
+            if (mMaxNumItems < 0) {
+                mMaxNumItems = Integer.MAX_VALUE;
+            }
         }
 
         mInputMethodManager = (InputMethodManager)
@@ -152,7 +188,13 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
+        mScrollView = (ScrollView) findViewById(R.id.scroll_view);
         mContent = (CellLayout) findViewById(R.id.folder_content);
+
+        // Beyond this height, the area scrolls
+        mContent.setGridSize(mMaxVisibleX, mMaxVisibleY);
+        mMaxContentAreaHeight = mContent.getDesiredHeight() - SCROLL_CUT_OFF_AMOUNT;
+
         mContent.setGridSize(0, 0);
         mContent.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false);
         mContent.setInvertIfRtl(true);
@@ -617,19 +659,72 @@
     }
 
     public void onDragOver(DragObject d) {
+        int scrollOffset = mScrollView.getScrollY();
+
         float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, null);
-        mTargetCell = mContent.findNearestArea((int) r[0], (int) r[1], 1, 1, mTargetCell);
+        r[0] -= getPaddingLeft();
+        r[1] -= getPaddingTop();
+
+        mTargetCell = mContent.findNearestArea((int) r[0], (int) r[1] + scrollOffset,
+                1, 1, mTargetCell);
 
         if (isLayoutRtl()) {
             mTargetCell[0] = mContent.getCountX() - mTargetCell[0] - 1;
         }
 
-        if (mTargetCell[0] != mPreviousTargetCell[0] || mTargetCell[1] != mPreviousTargetCell[1]) {
+        if (r[1] < SCROLL_BAND_HEIGHT && mScrollView.getScrollY() > 0) {
+            // Scroll up
+            if (mDragMode != DRAG_MODE_SCROLL_UP) {
+                mDragMode = DRAG_MODE_SCROLL_UP;
+                scrollUp();
+            }
+            mReorderAlarm.cancelAlarm();
+        } else if (r[1] > (getFolderHeight() - SCROLL_BAND_HEIGHT) && mScrollView.getScrollY() <
+                (mContent.getMeasuredHeight() - mScrollView.getMeasuredHeight())) {
+            if (mDragMode != DRAG_MODE_SCROLL_DOWN) {
+                mDragMode = DRAG_MODE_SCROLL_DOWN;
+                scrollDown();
+            }
+            mReorderAlarm.cancelAlarm();
+        } else if (mTargetCell[0] != mPreviousTargetCell[0] || mTargetCell[1] != mPreviousTargetCell[1]) {
             mReorderAlarm.cancelAlarm();
             mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
-            mReorderAlarm.setAlarm(150);
+            mReorderAlarm.setAlarm(REORDER_DELAY);
             mPreviousTargetCell[0] = mTargetCell[0];
             mPreviousTargetCell[1] = mTargetCell[1];
+            mDragMode = DRAG_MODE_REORDER;
+        } else {
+            mDragMode = DRAG_MODE_NONE;
+        }
+    }
+
+    Runnable mScrollUpRunnable = new Runnable() {
+        @Override
+        public void run() {
+            scrollUp();
+        }
+    };
+
+    Runnable mScrollDownRunnable = new Runnable() {
+        @Override
+        public void run() {
+            scrollDown();
+        }
+    };
+
+    private void scrollUp() {
+        if (mDragMode == DRAG_MODE_SCROLL_UP) {
+            mScrollView.setScrollY(mScrollView.getScrollY() - 7);
+            invalidate();
+            post(mScrollUpRunnable);
+        }
+    }
+
+    private void scrollDown() {
+        if (mDragMode == DRAG_MODE_SCROLL_DOWN) {
+            mScrollView.setScrollY(mScrollView.getScrollY() + 7);
+            invalidate();
+            post(mScrollDownRunnable);
         }
     }
 
@@ -681,6 +776,7 @@
             mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY);
         }
         mReorderAlarm.cancelAlarm();
+        mDragMode = DRAG_MODE_NONE;
     }
 
     public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete,
@@ -704,7 +800,6 @@
                 completeDragExit();
             }
         }
-
         mDeleteFolderOnDropCompleted = false;
         mDragInProgress = false;
         mItemAddedBackToSelfViaIcon = false;
@@ -714,7 +809,7 @@
 
         // Reordering may have occured, and we need to save the new item locations. We do this once
         // at the end to prevent unnecessary database operations.
-        updateItemLocationsInDatabase();
+        updateItemLocationsInDatabaseBatch();
     }
 
     @Override
@@ -741,6 +836,18 @@
         }
     }
 
+    private void updateItemLocationsInDatabaseBatch() {
+        ArrayList<View> list = getItemsInReadingOrder();
+        ArrayList<ItemInfo> items = new ArrayList<ItemInfo>();
+        for (int i = 0; i < list.size(); i++) {
+            View v = list.get(i);
+            ItemInfo info = (ItemInfo) v.getTag();
+            items.add(info);
+        }
+
+        LauncherModel.moveItemsInDatabase(mLauncher, items, mInfo.id, 0);
+    }
+
     public void notifyDrop() {
         if (mDragInProgress) {
             mItemAddedBackToSelfViaIcon = true;
@@ -792,8 +899,7 @@
         DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
 
         int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
-        int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight()
-                + mFolderNameHeight;
+        int height = getFolderHeight();
         DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer);
 
         float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, mTempRect);
@@ -862,18 +968,35 @@
         centerAboutIcon();
     }
 
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
-        int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight()
+    private int getFolderHeight() {
+        int contentAreaHeight = mContent.getDesiredHeight();
+        if (contentAreaHeight >= mMaxContentAreaHeight) {
+            // Subtract a bit so the user can see that it's scrollable.
+            contentAreaHeight = mMaxContentAreaHeight;
+        }
+        int height = getPaddingTop() + getPaddingBottom() + contentAreaHeight
                 + mFolderNameHeight;
+        return height;
+    }
 
-        int contentWidthSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredWidth(),
-                MeasureSpec.EXACTLY);
-        int contentHeightSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredHeight(),
-                MeasureSpec.EXACTLY);
-        mContent.measure(contentWidthSpec, contentHeightSpec);
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 
-        mFolderName.measure(contentWidthSpec,
+        int contentAreaHeight = mContent.getDesiredHeight();
+        if (contentAreaHeight >= mMaxContentAreaHeight) {
+            // Subtract a bit so the user can see that it's scrollable.
+            contentAreaHeight = mMaxContentAreaHeight;
+        }
+
+        int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
+        int height = getFolderHeight();
+        int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredWidth(),
+                MeasureSpec.EXACTLY);
+        int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(contentAreaHeight,
+                MeasureSpec.EXACTLY);
+
+        mContent.setFixedSize(mContent.getDesiredWidth(), mContent.getDesiredHeight());
+        mScrollView.measure(contentAreaWidthSpec, contentAreaHeightSpec);
+        mFolderName.measure(contentAreaWidthSpec,
                 MeasureSpec.makeMeasureSpec(mFolderNameHeight, MeasureSpec.EXACTLY));
         setMeasuredDimension(width, height);
     }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 5459af2..bbdff9e 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -22,6 +22,7 @@
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentProviderClient;
+import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
@@ -332,50 +333,83 @@
         Runnable r = new Runnable() {
             public void run() {
                 cr.update(uri, values, null, null);
+                updateItemArrays(item, itemId, stackTrace);
+            }
+        };
+        runOnWorkerThread(r);
+    }
 
-                // Lock on mBgLock *after* the db operation
-                synchronized (sBgLock) {
-                    checkItemInfoLocked(itemId, item, stackTrace);
+    static void updateItemsInDatabaseHelper(Context context, final ArrayList<ContentValues> valuesList,
+            final ArrayList<ItemInfo> items, final String callingFunction) {
+        final ContentResolver cr = context.getContentResolver();
 
-                    if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP &&
-                            item.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-                        // Item is in a folder, make sure this folder exists
-                        if (!sBgFolders.containsKey(item.container)) {
-                            // An items container is being set to a that of an item which is not in
-                            // the list of Folders.
-                            String msg = "item: " + item + " container being set to: " +
-                                    item.container + ", not in the list of folders";
-                            Log.e(TAG, msg);
-                            Launcher.dumpDebugLogsToConsole();
-                        }
-                    }
+        final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
+        Runnable r = new Runnable() {
+            public void run() {
+                ArrayList<ContentProviderOperation> ops =
+                        new ArrayList<ContentProviderOperation>();
+                int count = items.size();
+                for (int i = 0; i < count; i++) {
+                    ItemInfo item = items.get(i);
+                    final long itemId = item.id;
+                    final Uri uri = LauncherSettings.Favorites.getContentUri(itemId, false);
+                    ContentValues values = valuesList.get(i);
 
-                    // Items are added/removed from the corresponding FolderInfo elsewhere, such
-                    // as in Workspace.onDrop. Here, we just add/remove them from the list of items
-                    // that are on the desktop, as appropriate
-                    ItemInfo modelItem = sBgItemsIdMap.get(itemId);
-                    if (modelItem.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
-                            modelItem.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-                        switch (modelItem.itemType) {
-                            case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-                            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
-                            case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
-                                if (!sBgWorkspaceItems.contains(modelItem)) {
-                                    sBgWorkspaceItems.add(modelItem);
-                                }
-                                break;
-                            default:
-                                break;
-                        }
-                    } else {
-                        sBgWorkspaceItems.remove(modelItem);
-                    }
+                    ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
+                    updateItemArrays(item, itemId, stackTrace);
+
+                }
+                try {
+                    cr.applyBatch(LauncherProvider.AUTHORITY, ops);
+                } catch (Exception e) {
+                    e.printStackTrace();
                 }
             }
         };
         runOnWorkerThread(r);
     }
 
+    static void updateItemArrays(ItemInfo item, long itemId, StackTraceElement[] stackTrace) {
+        // Lock on mBgLock *after* the db operation
+        synchronized (sBgLock) {
+            checkItemInfoLocked(itemId, item, stackTrace);
+
+            if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP &&
+                    item.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+                // Item is in a folder, make sure this folder exists
+                if (!sBgFolders.containsKey(item.container)) {
+                    // An items container is being set to a that of an item which is not in
+                    // the list of Folders.
+                    String msg = "item: " + item + " container being set to: " +
+                            item.container + ", not in the list of folders";
+                    Log.e(TAG, msg);
+                    Launcher.dumpDebugLogsToConsole();
+                }
+            }
+
+            // Items are added/removed from the corresponding FolderInfo elsewhere, such
+            // as in Workspace.onDrop. Here, we just add/remove them from the list of items
+            // that are on the desktop, as appropriate
+            ItemInfo modelItem = sBgItemsIdMap.get(itemId);
+            if (modelItem.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
+                    modelItem.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+                switch (modelItem.itemType) {
+                    case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
+                    case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                    case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+                        if (!sBgWorkspaceItems.contains(modelItem)) {
+                            sBgWorkspaceItems.add(modelItem);
+                        }
+                        break;
+                    default:
+                        break;
+                }
+            } else {
+                sBgWorkspaceItems.remove(modelItem);
+            }
+        }
+    }
+
     public void flushWorkerThread() {
         mFlushingWorkerThread = true;
         Runnable waiter = new Runnable() {
@@ -438,6 +472,46 @@
     }
 
     /**
+     * Move items in the DB to a new <container, screen, cellX, cellY>. We assume that the
+     * cellX, cellY have already been updated on the ItemInfos.
+     */
+    static void moveItemsInDatabase(Context context, final ArrayList<ItemInfo> items,
+            final long container, final int screen) {
+
+        ArrayList<ContentValues> contentValues = new ArrayList<ContentValues>();
+        int count = items.size();
+
+        for (int i = 0; i < count; i++) {
+            ItemInfo item = items.get(i);
+            String transaction = "DbDebug    Modify item (" + item.title + ") in db, id: "
+                    + item.id + " (" + item.container + ", " + item.screen + ", " + item.cellX
+                    + ", " + item.cellY + ") --> " + "(" + container + ", " + screen + ", "
+                    + item.cellX + ", " + item.cellY + ")";
+            Launcher.sDumpLogs.add(transaction);
+            item.container = container;
+
+            // We store hotseat items in canonical form which is this orientation invariant position
+            // in the hotseat
+            if (context instanceof Launcher && screen < 0 &&
+                    container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+                item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(item.cellX,
+                        item.cellY);
+            } else {
+                item.screen = screen;
+            }
+
+            final ContentValues values = new ContentValues();
+            values.put(LauncherSettings.Favorites.CONTAINER, item.container);
+            values.put(LauncherSettings.Favorites.CELLX, item.cellX);
+            values.put(LauncherSettings.Favorites.CELLY, item.cellY);
+            values.put(LauncherSettings.Favorites.SCREEN, item.screen);
+
+            contentValues.add(values);
+        }
+        updateItemsInDatabaseHelper(context, contentValues, items, "moveItemInDatabase");
+    }
+
+    /**
      * Move and/or resize item in the DB to a new <container, screen, cellX, cellY, spanX, spanY>
      */
     static void modifyItemInDatabase(Context context, final ItemInfo item, final long container,