Merge "Drag and drop into folders."
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index a6a6816..cf81f1e 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -23,7 +23,9 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.Animator.AnimatorListener;
+import android.content.ClipData;
 import android.content.Context;
+import android.content.ClipData.Item;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.graphics.Bitmap;
@@ -32,6 +34,7 @@
 import android.graphics.Color;
 import android.graphics.LinearGradient;
 import android.graphics.Paint;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.Shader;
 import android.graphics.Typeface;
@@ -54,6 +57,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
+import android.view.View.DragShadowBuilder;
 import android.view.View.MeasureSpec;
 import android.view.animation.DecelerateInterpolator;
 import android.widget.Checkable;
@@ -68,6 +72,7 @@
 import com.android.mail.providers.UIProvider;
 import com.android.mail.providers.UIProvider.ConversationColumns;
 import com.android.mail.ui.ConversationSelectionSet;
+import com.android.mail.ui.DragListener;
 import com.android.mail.ui.FolderDisplayer;
 import com.android.mail.ui.ViewMode;
 import com.android.mail.utils.Utils;
@@ -159,6 +164,9 @@
     private CheckForTap mPendingCheckForTap;
     private CheckForLongPress mPendingCheckForLongPress;
     private boolean mSwipeEnabled;
+    private int mLastTouchX;
+    private int mLastTouchY;
+    private DragListener mDragListener;
     private static Bitmap MORE_FOLDERS;
 
     static {
@@ -289,13 +297,13 @@
                 }
             }
         }
+    }
 
-        /**
-         * Helpers function to align an element in the center of a space.
-         */
-        private static int getPadding(int space, int length) {
-            return (space - length) / 2;
-        }
+    /**
+     * Helpers function to align an element in the center of a space.
+     */
+    private static int getPadding(int space, int length) {
+        return (space - length) / 2;
     }
 
     public ConversationItemView(Context context, String account) {
@@ -361,27 +369,29 @@
         }
     }
 
-    public void bind(Cursor cursor, ViewMode viewMode, ConversationSelectionSet set,
-            Folder folder, boolean checkboxesDisabled, boolean swipeEnabled) {
-        mViewMode = viewMode;
-        mHeader = ConversationItemViewModel.forCursor(cursor);
-        mSelectedConversationSet = set;
-        mDisplayedFolder = folder;
-        mCheckboxesEnabled = !checkboxesDisabled;
-        mSwipeEnabled = swipeEnabled;
-        setContentDescription(mHeader.getContentDescription(mContext));
-        requestLayout();
+    public void bind(Cursor cursor, ViewMode viewMode, ConversationSelectionSet set, Folder folder,
+            boolean checkboxesDisabled, boolean swipeEnabled, DragListener dragListener) {
+        bind(ConversationItemViewModel.forCursor(cursor), viewMode, set, folder,
+                checkboxesDisabled, swipeEnabled, dragListener);
     }
 
-
     public void bind(Conversation conversation, ViewMode viewMode, ConversationSelectionSet set,
-            Folder folder, boolean checkboxesDisabled, boolean swipeEnabled) {
+            Folder folder, boolean checkboxesDisabled, boolean swipeEnabled,
+            DragListener dragListener) {
+        bind(ConversationItemViewModel.forConversation(conversation), viewMode, set, folder,
+                checkboxesDisabled, swipeEnabled, dragListener);
+    }
+
+    private void bind(ConversationItemViewModel header, ViewMode viewMode,
+            ConversationSelectionSet set, Folder folder, boolean checkboxesDisabled,
+            boolean swipeEnabled, DragListener dragListener) {
         mViewMode = viewMode;
-        mHeader = ConversationItemViewModel.forConversation(conversation);
+        mHeader = header;
         mSelectedConversationSet = set;
         mDisplayedFolder = folder;
         mCheckboxesEnabled = !checkboxesDisabled;
         mSwipeEnabled = swipeEnabled;
+        mDragListener = dragListener;
         setContentDescription(mHeader.getContentDescription(mContext));
         requestLayout();
     }
@@ -1072,13 +1082,15 @@
      */
     @Override
     public boolean onTouchEvent(MotionEvent event) {
+        mLastTouchX = (int) event.getX();
+        mLastTouchY = (int) event.getY();
         if (!mSwipeEnabled) {
             return onTouchEventNoSwipe(event);
         }
         boolean handled = true;
 
-        int x = (int) event.getX();
-        int y = (int) event.getY();
+        int x = mLastTouchX;
+        int y = mLastTouchY;
         switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
                 mDownEvent = true;
@@ -1209,7 +1221,7 @@
 
     private class CheckForLongPress implements Runnable {
         public void run() {
-            ConversationItemView.this.toggleCheckMark();
+            ConversationItemView.this.toggleSelectionOrBeginDrag();
             performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
         }
     }
@@ -1257,4 +1269,87 @@
         return mHeader != null && mHeader.conversation != null ?
                 mHeader.conversation.position : -1;
     }
+
+    /**
+     * Select the current conversation.
+     */
+    private void selectConversation() {
+        if (!mSelectedConversationSet.containsKey(mHeader.conversation.id)) {
+            toggleCheckMark();
+        }
+    }
+
+    /**
+     * With two pane mode and mailboxes in one pane (tablet), add the conversation to the selected
+     * set and start drag mode.
+     * In two pane mode when viewing conversations (tablet), toggle selection.
+     * In one pane mode (phone, and portrait mode on tablet), toggle selection.
+     */
+    public void toggleSelectionOrBeginDrag() {
+        // If we are in one pane mode, or we are looking at conversations, drag and drop is
+        // meaningless.  Toggle checkmark and return early.
+        if (!Utils.useTabletUI(mContext) || mViewMode.getMode() != ViewMode.CONVERSATION_LIST) {
+            toggleCheckMark();
+            return;
+        }
+
+        // Begin drag mode. Keep the conversation selected (NOT toggle selection) and start drag.
+        selectConversation();
+        mDragListener.onStartDragMode();
+
+        // Clip data has form: [conversations_uri, conversationId1,
+        // maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...]
+        int count = mSelectedConversationSet.size();
+        String description = Utils.formatPlural(mContext, R.plurals.move_conversation, count);
+
+        ClipData data = ClipData.newUri(mContext.getContentResolver(), description,
+                Conversation.MOVE_CONVERSATIONS_URI);
+        for (Conversation conversation : mSelectedConversationSet.values()) {
+            data.addItem(new Item(String.valueOf(conversation.position)));
+        }
+
+        // Start drag mode
+        startDrag(data, new ShadowBuilder(this, count, mLastTouchX, mLastTouchY), null, 0);
+    }
+
+    private class ShadowBuilder extends DragShadowBuilder {
+        private final Drawable mBackground;
+
+        private final View mView;
+        private final String mDragDesc;
+        private final int mTouchX;
+        private final int mTouchY;
+        private int mDragDescX;
+        private int mDragDescY;
+
+        public ShadowBuilder(View view, int count, int touchX, int touchY) {
+            super(view);
+            mView = view;
+            mBackground = mView.getResources().getDrawable(R.drawable.list_pressed_holo);
+            mDragDesc = Utils.formatPlural(mView.getContext(), R.plurals.move_conversation, count);
+            mTouchX = touchX;
+            mTouchY = touchY;
+        }
+
+        @Override
+        public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
+            int width = mView.getWidth();
+            int height = mView.getHeight();
+            mDragDescX = mCoordinates.sendersX;
+            mDragDescY = getPadding(height, mCoordinates.subjectFontSize)
+                    - mCoordinates.subjectAscent;
+            shadowSize.set(width, height);
+            shadowTouchPoint.set(mTouchX, mTouchY);
+        }
+
+        @Override
+        public void onDrawShadow(Canvas canvas) {
+            super.onDrawShadow(canvas);
+
+            mBackground.setBounds(0, 0, mView.getWidth(), mView.getHeight());
+            mBackground.draw(canvas);
+            sPaint.setTextSize(mCoordinates.subjectFontSize);
+            canvas.drawText(mDragDesc, mDragDescX, mDragDescY, sPaint);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/mail/providers/Conversation.java b/src/com/android/mail/providers/Conversation.java
index f763498..052fcb7 100644
--- a/src/com/android/mail/providers/Conversation.java
+++ b/src/com/android/mail/providers/Conversation.java
@@ -138,6 +138,8 @@
 
     };
 
+    public static final Uri MOVE_CONVERSATIONS_URI = Uri.parse("content://moveconversations");
+
     public Conversation(Cursor cursor) {
         if (cursor != null) {
             id = cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN);
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index 8b55d20..7cc3286 100644
--- a/src/com/android/mail/ui/AbstractActivityController.java
+++ b/src/com/android/mail/ui/AbstractActivityController.java
@@ -24,6 +24,7 @@
 import android.app.Dialog;
 import android.app.LoaderManager;
 import android.app.SearchManager;
+import android.content.ClipData;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.CursorLoader;
@@ -35,6 +36,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.provider.SearchRecentSuggestions;
+import android.view.DragEvent;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -62,12 +64,19 @@
 import com.android.mail.providers.UIProvider.FolderCapabilities;
 import com.android.mail.utils.LogUtils;
 import com.android.mail.utils.Utils;
+import com.google.android.gm.ConversationInfo;
+import com.google.android.gm.LabelOperations;
+import com.google.android.gm.provider.Gmail;
+import com.google.android.gm.provider.Label;
+import com.google.android.gm.provider.LabelManager;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 
@@ -1419,4 +1428,57 @@
                     .getString(R.string.search_unsupported), Toast.LENGTH_SHORT).show();
         }
     }
+
+    /**
+     * Supports dragging conversations to a folder.
+     */
+    @Override
+    public boolean supportsDrag(DragEvent event, Folder folder) {
+        return (folder != null
+                && event != null
+                && event.getClipDescription() != null
+                && folder.supportsCapability
+                    (UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
+                && folder.supportsCapability
+                    (UIProvider.FolderCapabilities.CAN_HOLD_MAIL)
+                && !mFolder.uri.equals(folder.uri));
+    }
+
+    /**
+     * Handles dropping conversations to a label.
+     */
+    @Override
+    public void handleDrop(DragEvent event, final Folder folder) {
+        /*
+         * Expect clip data has form: [conversations_uri, conversationId1,
+         * maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...]
+         */
+        if (!supportsDrag(event, folder)) {
+            return;
+        }
+        ClipData data = event.getClipData();
+        ArrayList<Integer> conversationPositions = Lists.newArrayList();
+        for (int i = 1; i < data.getItemCount(); i += 3) {
+            int position = Integer.parseInt(data.getItemAt(i).getText().toString());
+            conversationPositions.add(position);
+        }
+        final Collection<Conversation> conversations = mSelectedSet.values();
+        mConversationListFragment.requestDelete(conversations,
+                new ActionCompleteListener() {
+                    @Override
+                    public void onActionComplete() {
+                        AbstractActivityController.this.onActionComplete();
+                        ArrayList<Folder> changes = new ArrayList<Folder>();
+                        changes.add(folder);
+                        Conversation.updateString(mContext, conversations,
+                                ConversationColumns.FOLDER_LIST, folder.uri.toString());
+                        Conversation.updateString(mContext, conversations,
+                                ConversationColumns.RAW_FOLDERS,
+                                Folder.getSerializedFolderString(mFolder, changes));
+                        mConversationListFragment.onUndoAvailable(new UndoOperation(conversations
+                                .size(), R.id.change_folder));
+                        mSelectedSet.clear();
+                    }
+                });
+    }
 }
diff --git a/src/com/android/mail/ui/ActivityController.java b/src/com/android/mail/ui/ActivityController.java
index 2b08ee7..3832266 100644
--- a/src/com/android/mail/ui/ActivityController.java
+++ b/src/com/android/mail/ui/ActivityController.java
@@ -22,6 +22,7 @@
 import android.content.Intent;
 import android.database.Cursor;
 import android.os.Bundle;
+import android.view.DragEvent;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -30,6 +31,7 @@
 import com.android.mail.ConversationListContext;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Conversation;
+import com.android.mail.providers.Folder;
 import com.android.mail.providers.Settings;
 import com.android.mail.ui.ViewMode.ModeChangeListener;
 import com.android.mail.ui.FoldersSelectionDialog.FolderChangeCommitListener;
@@ -39,11 +41,11 @@
  * ActivityControllers are delegates that implement methods by calling underlying views to modify,
  * or respond to user action.
  */
-public interface ActivityController extends MenuCallback, LayoutListener, SubjectDisplayChanger,
+public interface ActivityController extends DragListener, LayoutListener, SubjectDisplayChanger,
         ModeChangeListener, ConversationListCallbacks, FolderChangeCommitListener,
         FolderChangeListener, AccountChangeListener, LoaderManager.LoaderCallbacks<Cursor>,
         ActionCompleteListener, ConversationSetObserver,
-        FolderListFragment.FolderListSelectionListener {
+        FolderListFragment.FolderListSelectionListener, HelpCallback {
 
     // As far as possible, the methods here that correspond to Activity lifecycle have the same name
     // as their counterpart in the Activity lifecycle.
@@ -264,4 +266,14 @@
      * Start search mode if the account being view supports the search capability.
      */
     void startSearch();
+
+    /**
+     * Supports dragging conversations to a folder.
+     */
+    boolean supportsDrag(DragEvent event, Folder folder);
+
+    /**
+     * Handles dropping conversations to a folder.
+     */
+    void handleDrop(DragEvent event, Folder folder);
 }
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index 7b86070..c61f2ae 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -59,6 +59,7 @@
     private final SwipeableListView mListView;
     private Settings mCachedSettings;
     private boolean mSwipeEnabled;
+    private DragListener mDragListener;
 
     /**
      * Used only for debugging.
@@ -67,7 +68,7 @@
 
     public AnimatedAdapter(Context context, int textViewResourceId, ConversationCursor cursor,
             ConversationSelectionSet batch, Account account, Settings settings, ViewMode viewMode,
-            SwipeableListView listView) {
+            SwipeableListView listView, DragListener dragListener) {
         // Use FLAG_REGISTER_CONTENT_OBSERVER to ensure special
         // ConversationCursor notifications (triggered by UI actions) cause any
         // connected ListView to redraw.
@@ -80,6 +81,7 @@
         mShowFooter = false;
         mListView = listView;
         mCachedSettings = settings;
+        mDragListener = dragListener;
         mSwipeEnabled = account.supportsCapability(UIProvider.AccountCapabilities.ARCHIVE);
     }
 
@@ -116,7 +118,7 @@
         if (!isPositionAnimating(view) && !isPositionFooter(view)) {
             ((ConversationItemView) view).bind(cursor, mViewMode, mBatchConversations, mFolder,
                     mCachedSettings != null ? mCachedSettings.hideCheckboxes : false,
-                    mSwipeEnabled);
+                    mSwipeEnabled, mDragListener);
         }
     }
 
@@ -256,7 +258,7 @@
                     parent);
             convView.bind(conversation, mViewMode, mBatchConversations, mFolder,
                     mCachedSettings != null ? mCachedSettings.hideCheckboxes : false,
-                    mSwipeEnabled);
+                    mSwipeEnabled, mDragListener);
             convView.startUndoAnimation(mViewMode, this);
             return convView;
         } else {
diff --git a/src/com/android/mail/ui/ControllableActivity.java b/src/com/android/mail/ui/ControllableActivity.java
index 33fa2dd..60a538e 100644
--- a/src/com/android/mail/ui/ControllableActivity.java
+++ b/src/com/android/mail/ui/ControllableActivity.java
@@ -24,7 +24,8 @@
  * A controllable activity is an Activity that has a Controller attached. This activity must be
  * able to attach the various view fragments and delegate the method calls between them.
  */
-public interface ControllableActivity extends HelpCallback, RestrictedActivity {
+public interface ControllableActivity extends HelpCallback, RestrictedActivity,
+        FolderItemView.DropHandler {
     /**
      * Attaches the conversation list fragment to the activity controller. This callback is
      * currently required because the Activity Controller directly calls methods on the conversation
@@ -92,4 +93,6 @@
      * pushed for hierarchical folders.
      */
     FolderListFragment.FolderListSelectionListener getFolderListSelectionListener();
+
+    DragListener getDragListener();
 }
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index c892576..835829a 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -213,7 +213,8 @@
 
         mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), -1,
                 getConversationListCursor(), mActivity.getSelectedSet(), mAccount,
-                mActivity.getSettings(), mActivity.getViewMode(), mListView);
+                mActivity.getSettings(), mActivity.getViewMode(), mListView,
+                mActivity.getDragListener());
         mFooterView = (ConversationListFooterView) LayoutInflater.from(
                 mActivity.getActivityContext()).inflate(R.layout.conversation_list_footer_view,
                 null);
@@ -471,6 +472,15 @@
         requestDelete(listener);
     }
 
+    public void requestDelete(Collection<Conversation> conversations,
+            ActionCompleteListener listener) {
+        for (Conversation conv : conversations) {
+            conv.localDeleteOnUpdate = true;
+        }
+        // Delete the local delete items (all for now) and when done,
+        // update...
+        mListAdapter.delete(conversations, listener);
+    }
 
     public void onFolderUpdated(Folder folder) {
         mFolder = folder;
diff --git a/src/com/android/mail/ui/MenuCallback.java b/src/com/android/mail/ui/DragListener.java
similarity index 95%
rename from src/com/android/mail/ui/MenuCallback.java
rename to src/com/android/mail/ui/DragListener.java
index c992f70..1b693ba 100644
--- a/src/com/android/mail/ui/MenuCallback.java
+++ b/src/com/android/mail/ui/DragListener.java
@@ -25,7 +25,7 @@
  * the UI).
  */
 // Called MenuHandler.ActivityCallback in the previous code.
-public interface MenuCallback extends HelpCallback {
+public interface DragListener {
     /**
      * Invoked when user starts drag and drop mode.
      */
diff --git a/src/com/android/mail/ui/FolderListFragment.java b/src/com/android/mail/ui/FolderListFragment.java
index 63dae5a..b494bb7 100644
--- a/src/com/android/mail/ui/FolderListFragment.java
+++ b/src/com/android/mail/ui/FolderListFragment.java
@@ -18,8 +18,6 @@
 package com.android.mail.ui;
 
 import android.app.Activity;
-import android.app.Fragment;
-import android.app.FragmentTransaction;
 import android.app.ListFragment;
 import android.app.LoaderManager;
 import android.content.Context;
@@ -69,6 +67,7 @@
     public static final int MODE_PICK = 1;
     private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
     private static final String ARG_FOLDER_URI = "arg-folder-list-uri";
+    private FolderItemView.DropHandler mDropHandler;
 
     /**
      * Constructor needs to be public to handle orientation changes and activity lifecycle events.
@@ -107,6 +106,9 @@
                     "create it. Cannot proceed.");
         }
         mActivity = (ControllableActivity) activity;
+        if (activity instanceof FolderItemView.DropHandler) {
+            mDropHandler = (FolderItemView.DropHandler) activity;
+        }
         mActivity.attachFolderList(this);
         mListener = mActivity.getFolderListSelectionListener();
         if (mActivity.isFinishing()) {
@@ -206,7 +208,7 @@
             }
             getCursor().moveToPosition(position);
             Folder folder = new Folder(getCursor());
-            folderItemView.bind(folder, null);
+            folderItemView.bind(folder, mDropHandler);
             if (mSelectedFolder != null && folder.uri.equals(mSelectedFolder.uri)) {
                 getListView().setItemChecked(position, true);
             }
diff --git a/src/com/android/mail/ui/FolderSelectionActivity.java b/src/com/android/mail/ui/FolderSelectionActivity.java
index bdb956d..b2b8bad 100644
--- a/src/com/android/mail/ui/FolderSelectionActivity.java
+++ b/src/com/android/mail/ui/FolderSelectionActivity.java
@@ -28,6 +28,7 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
+import android.view.DragEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.Button;
@@ -301,4 +302,19 @@
     public FolderListSelectionListener getFolderListSelectionListener() {
         return this;
     }
+
+    @Override
+    public DragListener getDragListener() {
+        return null;
+    }
+
+    @Override
+    public boolean supportsDrag(DragEvent event, Folder folder) {
+        return false;
+    }
+
+    @Override
+    public void handleDrop(DragEvent event, Folder folder) {
+        // Do nothing.
+    }
 }
diff --git a/src/com/android/mail/ui/MailActivity.java b/src/com/android/mail/ui/MailActivity.java
index d156023..5b9e0fe 100644
--- a/src/com/android/mail/ui/MailActivity.java
+++ b/src/com/android/mail/ui/MailActivity.java
@@ -22,11 +22,13 @@
 import android.os.Bundle;
 import android.os.StrictMode;
 import android.view.ActionMode;
+import android.view.DragEvent;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MotionEvent;
 
+import com.android.mail.providers.Folder;
 import com.android.mail.providers.Settings;
 import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
 import com.android.mail.ui.ViewMode.ModeChangeListener;
@@ -270,4 +272,19 @@
     public ConversationSelectionSet getSelectedSet() {
         return mController.getSelectedSet();
     }
+
+    @Override
+    public DragListener getDragListener() {
+        return mController;
+    }
+
+    @Override
+    public boolean supportsDrag(DragEvent event, Folder folder) {
+        return mController.supportsDrag(event, folder);
+    }
+
+    @Override
+    public void handleDrop(DragEvent event, Folder folder) {
+        mController.handleDrop(event, folder);
+    }
 }