Drag and drop into folders.
Change-Id: If80c43fb5643d328bf061c2bed87bd9acb6e4661
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);
+ }
}