/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.email.activity; import com.android.email.Controller; import com.android.email.Email; import com.android.email.R; import com.android.email.RefreshManager; import com.android.email.provider.EmailProvider; import com.android.emailcommon.Logging; import com.android.emailcommon.provider.EmailContent.Account; import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.utility.EmailAsyncTask; import com.android.emailcommon.utility.Utility; import android.app.Activity; import android.app.ListFragment; import android.app.LoaderManager; import android.app.LoaderManager.LoaderCallbacks; import android.content.ClipData; import android.content.ClipDescription; import android.content.Loader; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.util.Log; import android.view.DragEvent; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnDragListener; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ListView; import java.util.Timer; import java.util.TimerTask; /** * This fragment presents a list of mailboxes for a given account. */ public class MailboxListFragment extends ListFragment implements OnItemClickListener, OnDragListener { private static final String TAG = "MailboxListFragment"; private static final String BUNDLE_KEY_SELECTED_MAILBOX_ID = "MailboxListFragment.state.selected_mailbox_id"; private static final String BUNDLE_LIST_STATE = "MailboxListFragment.state.listState"; private static final boolean DEBUG_DRAG_DROP = false; // MUST NOT SUBMIT SET TO TRUE /** While in drag-n-drop, amount of time before it auto expands; in ms */ private static final long AUTO_EXPAND_DELAY = 750L; /** No drop target is available where the user is currently hovering over */ private static final int NO_DROP_TARGET = -1; // Total height of the top and bottom scroll zones, in pixels private static final int SCROLL_ZONE_SIZE = 64; // The amount of time to scroll by one pixel, in ms private static final int SCROLL_SPEED = 4; /** Arbitrary number for use with the loader manager */ private static final int MAILBOX_LOADER_ID = 1; /** Argument name(s) */ private static final String ARG_ACCOUNT_ID = "accountId"; private static final String ARG_PARENT_MAILBOX_ID = "parentMailboxId"; /** Timer to auto-expand folder lists during drag-n-drop */ private static final Timer sDragTimer = new Timer(); /** Rectangle used for hit testing children */ private static final Rect sTouchFrame = new Rect(); private RefreshManager mRefreshManager; // UI Support private Activity mActivity; private MailboxesAdapter mListAdapter; private Callback mCallback = EmptyCallback.INSTANCE; private ListView mListView; private boolean mResumed; // Colors used for drop targets private static Integer sDropTrashColor; private static Drawable sDropActiveDrawable; /** ID of the mailbox to hightlight. */ private long mSelectedMailboxId = -1; // True if a drag is currently in progress private boolean mDragInProgress; /** Mailbox ID of the item being dragged. Used to determine valid drop targets. */ private long mDragItemMailboxId = -1; /** A unique identifier for the drop target. May be {@link #NO_DROP_TARGET}. */ private int mDropTargetId = NO_DROP_TARGET; // The mailbox list item view that the user's finger is hovering over private MailboxListItem mDropTargetView; // Lazily instantiated height of a mailbox list item (-1 is a sentinel for 'not initialized') private int mDragItemHeight = -1; /** Task that actually does the work to auto-expand folder lists during drag-n-drop */ private TimerTask mDragTimerTask; // True if we are currently scrolling under the drag item private boolean mTargetScrolling; private Parcelable mSavedListState; private final MailboxesAdapter.Callback mMailboxesAdapterCallback = new MailboxesAdapter.Callback() { @Override public void onBind(MailboxListItem listItem) { listItem.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); } }; /** * Callback interface that owning activities must implement */ public interface Callback { /** * Called when any mailbox (even a combined mailbox) is selected. * * @param accountId * The ID of the owner account of the selected mailbox. * Or {@link Account#ACCOUNT_ID_COMBINED_VIEW} if it's a combined mailbox. * @param mailboxId * The ID of the selected mailbox. This may be real mailbox ID [e.g. a number > 0], * or a combined mailbox ID [e.g. {@link Mailbox#QUERY_ALL_INBOXES}]. * @param navigate navigate to the mailbox. */ public void onMailboxSelected(long accountId, long mailboxId, boolean navigate); /** * Called when a mailbox is selected during D&D. */ public void onMailboxSelectedForDnD(long mailboxId); /** Called when an account is selected on the combined view. */ public void onAccountSelected(long accountId); /** * Called when the list updates to propagate the current mailbox name and the unread count * for it. * * Note the reason why it's separated from onMailboxSelected is because this needs to be * reported when the unread count changes without changing the current mailbox. * * @param mailboxId ID for the selected mailbox. It'll never be of a combined mailbox, * and the owner account ID is always the same as * {@link MailboxListFragment#getAccountId()}. */ public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, int unreadCount); } private static class EmptyCallback implements Callback { public static final Callback INSTANCE = new EmptyCallback(); @Override public void onMailboxSelected(long accountId, long mailboxId, boolean navigate) { } @Override public void onMailboxSelectedForDnD(long mailboxId) { } @Override public void onAccountSelected(long accountId) { } @Override public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, int unreadCount) { } } /** * Returns the index of the view located at the specified coordinates in the given list. * If the coordinates are outside of the list, {@code NO_DROP_TARGET} is returned. */ private static int pointToIndex(ListView list, int x, int y) { final int count = list.getChildCount(); for (int i = count - 1; i >= 0; i--) { final View child = list.getChildAt(i); if (child.getVisibility() == View.VISIBLE) { child.getHitRect(sTouchFrame); if (sTouchFrame.contains(x, y)) { return i; } } } return NO_DROP_TARGET; } /** * Create a new instance with initialization parameters. * * This fragment should be created only with this method. (Arguments should always be set.) * * @param accountId The ID of the account we want to view * @param parentMailboxId The ID of the parent mailbox. Use {@link Mailbox#NO_MAILBOX} * to open the root. */ public static MailboxListFragment newInstance(long accountId, long parentMailboxId) { if (accountId == Account.NO_ACCOUNT) { throw new IllegalArgumentException(); } final MailboxListFragment instance = new MailboxListFragment(); final Bundle args = new Bundle(); args.putLong(ARG_ACCOUNT_ID, accountId); args.putLong(ARG_PARENT_MAILBOX_ID, parentMailboxId); instance.setArguments(args); return instance; } // Cached arguments. DO NOT use them directly. ALWAYS use getXxxIdArg(). private boolean mArgCacheInitialized; private long mCachedAccountId; private long mCachedParentMailboxId; private void initializeArgCache() { if (!mArgCacheInitialized) { mArgCacheInitialized = true; mCachedAccountId = getArguments().getLong(ARG_ACCOUNT_ID); mCachedParentMailboxId = getArguments().getLong(ARG_PARENT_MAILBOX_ID); } } /** * @return the account ID passed to {@link #newInstance}. Safe to call even before onCreate. */ public long getAccountId() { initializeArgCache(); return mCachedAccountId; } /** * @return the mailbox ID passed to {@link #newInstance}. Safe to call even before onCreate. */ public long getParentMailboxId() { initializeArgCache(); return mCachedParentMailboxId; } /** * @return true if the top level mailboxes are shown. Safe to call even before onCreate. */ public boolean isRoot() { return getParentMailboxId() == Mailbox.NO_MAILBOX; } /** * Called to do initial creation of a fragment. This is called after * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}. */ @Override public void onCreate(Bundle savedInstanceState) { if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Logging.LOG_TAG, this + " onCreate"); } super.onCreate(savedInstanceState); mActivity = getActivity(); mRefreshManager = RefreshManager.getInstance(mActivity); mListAdapter = new MailboxFragmentAdapter(mActivity, mMailboxesAdapterCallback); if (savedInstanceState != null) { restoreInstanceState(savedInstanceState); } if (sDropTrashColor == null) { Resources res = getResources(); sDropTrashColor = res.getColor(R.color.mailbox_drop_destructive_bg_color); sDropActiveDrawable = res.getDrawable(R.drawable.list_activated_holo); } } @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.mailbox_list_fragment, container, false); } @Override public void onActivityCreated(Bundle savedInstanceState) { if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Logging.LOG_TAG, this + " onActivityCreated"); } super.onActivityCreated(savedInstanceState); mListView = getListView(); mListView.setOnItemClickListener(this); mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); mListView.setOnDragListener(this); registerForContextMenu(mListView); startLoading(); } public void setCallback(Callback callback) { mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback; } /** * Returns whether or not the specified mailbox can be navigated to. */ private boolean isNavigable(long mailboxId) { final int count = mListView.getCount(); for (int i = 0; i < count; i++) { final MailboxListItem item = (MailboxListItem) mListView.getChildAt(i); if (item.mMailboxId != mailboxId) { continue; } return item.isNavigable(); } return false; } /** * Sets the selected mailbox to the given ID. Sub-folders will not be loaded. * @param mailboxId The ID of the mailbox to select. */ public void setSelectedMailbox(long mailboxId) { mSelectedMailboxId = mailboxId; if (mResumed) { highlightSelectedMailbox(true); } } /** * Called when the Fragment is visible to the user. */ @Override public void onStart() { if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Logging.LOG_TAG, this + " onStart"); } super.onStart(); } /** * Called when the fragment is visible to the user and actively running. */ @Override public void onResume() { if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Logging.LOG_TAG, this + " onResume"); } super.onResume(); mResumed = true; // Fetch the latest mailbox list from the server here if stale so that the user always // sees the (reasonably) up-to-date mailbox list, without pressing "refresh". final long accountId = getAccountId(); if (mRefreshManager.isMailboxListStale(accountId)) { mRefreshManager.refreshMailboxList(accountId); } } @Override public void onPause() { if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Logging.LOG_TAG, this + " onPause"); } mResumed = false; super.onPause(); mSavedListState = getListView().onSaveInstanceState(); } /** * Called when the Fragment is no longer started. */ @Override public void onStop() { if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Logging.LOG_TAG, this + " onStop"); } super.onStop(); } /** * Called when the fragment is no longer in use. */ @Override public void onDestroy() { if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Logging.LOG_TAG, this + " onDestroy"); } super.onDestroy(); } @Override public void onSaveInstanceState(Bundle outState) { if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); } super.onSaveInstanceState(outState); outState.putLong(BUNDLE_KEY_SELECTED_MAILBOX_ID, mSelectedMailboxId); outState.putParcelable(BUNDLE_LIST_STATE, getListView().onSaveInstanceState()); } private void restoreInstanceState(Bundle savedInstanceState) { if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Logging.LOG_TAG, this + " restoreInstanceState"); } mSelectedMailboxId = savedInstanceState.getLong(BUNDLE_KEY_SELECTED_MAILBOX_ID); mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE); } private void startLoading() { if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Logging.LOG_TAG, this + " startLoading"); } // Clear the list. (ListFragment will show the "Loading" animation) setListShown(false); final LoaderManager lm = getLoaderManager(); lm.initLoader(MAILBOX_LOADER_ID, null, new MailboxListLoaderCallbacks()); } // TODO This class probably should be made static. There are many calls into the enclosing // class and we need to be cautious about what we call while in these callbacks private class MailboxListLoaderCallbacks implements LoaderCallbacks { private boolean mIsFirstLoad; @Override public Loader onCreateLoader(int id, Bundle args) { if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onCreateLoader"); } mIsFirstLoad = true; return MailboxFragmentAdapter.createLoader(getActivity(), getAccountId(), getParentMailboxId()); } @Override public void onLoadFinished(Loader loader, Cursor cursor) { if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onLoadFinished count=" + cursor.getCount()); } // Save list view state (primarily scroll position) final ListView lv = getListView(); final Parcelable listState; if (mSavedListState != null) { listState = mSavedListState; mSavedListState = null; } else { listState = lv.onSaveInstanceState(); } if (cursor.getCount() == 0) { // If there's no row, don't set it to the ListView. // Instead use setListShown(false) to make ListFragment show progress icon. mListAdapter.swapCursor(null); setListShown(false); } else { // Set the adapter. mListAdapter.swapCursor(cursor); setListAdapter(mListAdapter); setListShown(true); // We want to make visible the selection only for the first load. // Re-load caused by content changed events shouldn't scroll the list. highlightSelectedMailbox(mIsFirstLoad); } // List has been reloaded; clear any drop target information mDropTargetId = NO_DROP_TARGET; mDropTargetView = null; // Restore the state if (listState != null) { lv.onRestoreInstanceState(listState); } mIsFirstLoad = false; } @Override public void onLoaderReset(Loader loader) { if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onLoaderReset"); } mListAdapter.swapCursor(null); } } /** * {@inheritDoc} *

* @param doNotUse IMPORTANT: Do not use this parameter. The ID in the list widget * must be a positive value. However, we rely on negative IDs for special mailboxes. Instead, * we use the ID returned by {@link MailboxesAdapter#getId(int)}. */ @Override public void onItemClick(AdapterView parent, View view, int position, long doNotUse) { final long id = mListAdapter.getId(position); if (mListAdapter.isAccountRow(position)) { mCallback.onAccountSelected(id); } else { // STOPSHIP On phone, we need a way to open a message list without navigating to the // mailbox. mCallback.onMailboxSelected(mListAdapter.getAccountId(position), id, isNavigable(id)); } } /** * Highlight the selected mailbox. */ private void highlightSelectedMailbox(boolean ensureSelectionVisible) { String mailboxName = ""; int unreadCount = 0; if (mSelectedMailboxId == -1) { // No mailbox selected mListView.clearChoices(); } else { // TODO Don't mix list view & list adapter indices. This is a recipe for disaster. final int count = mListView.getCount(); for (int i = 0; i < count; i++) { if (mListAdapter.getId(i) != mSelectedMailboxId) { continue; } mListView.setItemChecked(i, true); if (ensureSelectionVisible) { Utility.listViewSmoothScrollToPosition(getActivity(), mListView, i); } mailboxName = mListAdapter.getDisplayName(mActivity, i); unreadCount = mListAdapter.getUnreadCount(i); break; } } mCallback.onCurrentMailboxUpdated(mSelectedMailboxId, mailboxName, unreadCount); } // Drag & Drop handling /** * Update all of the list's child views with the proper target background (for now, orange if * a valid target, except red if the trash; standard background otherwise) */ private void updateChildViews() { int itemCount = mListView.getChildCount(); // Lazily initialize the height of our list items if (itemCount > 0 && mDragItemHeight < 0) { mDragItemHeight = mListView.getChildAt(0).getHeight(); } for (int i = 0; i < itemCount; i++) { MailboxListItem item = (MailboxListItem)mListView.getChildAt(i); item.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); } } /** * Starts the timer responsible for auto-selecting mailbox items while in drag-n-drop. * If there is already an active task, we first try to cancel it. There are only two * reasons why a new timer may not be started. First, if we are unable to cancel a * previous timer, we must assume that a new mailbox has already been loaded. Second, * if the target item is not permitted to be auto selected. * @param newTarget The drag target that needs to be auto selected */ private void startDragTimer(final MailboxListItem newTarget) { boolean canceledInTime = mDragTimerTask == null || stopDragTimer(); if (canceledInTime && newTarget != null && newTarget.isNavigable() && newTarget.isDropTarget(mDragItemMailboxId)) { mDragTimerTask = new TimerTask() { @Override public void run() { mActivity.runOnUiThread(new Runnable() { @Override public void run() { stopDragTimer(); mCallback.onMailboxSelectedForDnD(newTarget.mMailboxId); } }); } }; sDragTimer.schedule(mDragTimerTask, AUTO_EXPAND_DELAY); } } /** * Stops the timer responsible for auto-selecting mailbox items while in drag-n-drop. * If the timer is not active, nothing will happen. * @return Whether or not the timer was interrupted. {@link TimerTask#cancel()}. */ private boolean stopDragTimer() { boolean timerInterrupted = false; synchronized (sDragTimer) { if (mDragTimerTask != null) { timerInterrupted = mDragTimerTask.cancel(); mDragTimerTask = null; } } return timerInterrupted; } /** * Called when the user has dragged outside of the mailbox list area. */ private void onDragExited() { // Reset the background of the current target if (mDropTargetView != null) { mDropTargetView.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); mDropTargetView = null; } mDropTargetId = NO_DROP_TARGET; stopDragTimer(); stopScrolling(); } /** * Called while dragging; highlight possible drop targets, and auto scroll the list. */ private void onDragLocation(DragEvent event) { // TODO The list may be changing while in drag-n-drop; temporarily suspend drag-n-drop // if the list is being updated [i.e. navigated to another mailbox] if (mDragItemHeight <= 0) { // This shouldn't be possible, but avoid NPE Log.w(TAG, "drag item height is not set"); return; } // Find out which item we're in and highlight as appropriate final int rawTouchX = (int) event.getX(); final int rawTouchY = (int) event.getY(); final int viewIndex = pointToIndex(mListView, rawTouchX, rawTouchY); int targetId = viewIndex; if (targetId != mDropTargetId) { if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Target changed; oldId: " + mDropTargetId + ", newId: " + targetId); } // Remove highlight the current target; if there was one if (mDropTargetView != null) { mDropTargetView.setDropTargetBackground(true, mDragItemMailboxId); mDropTargetView = null; } // Get the new target mailbox view final MailboxListItem newTarget = (MailboxListItem) mListView.getChildAt(viewIndex); if (newTarget == null) { // In any event, we're no longer dragging in the list view if newTarget is null if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Drag off the list"); } final int childCount = mListView.getChildCount(); if (viewIndex >= childCount) { // Touching beyond the end of the list; may happen for small lists onDragExited(); return; } else { // We should never get here Log.w(TAG, "null view; idx: " + viewIndex + ", cnt: " + childCount); } } else if (newTarget.mMailboxType == Mailbox.TYPE_TRASH) { if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Trash mailbox; id: " + newTarget.mMailboxId); } newTarget.setBackgroundColor(sDropTrashColor); } else if (newTarget.isDropTarget(mDragItemMailboxId)) { if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Target mailbox; id: " + newTarget.mMailboxId); } newTarget.setBackgroundDrawable(sDropActiveDrawable); } else { if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Non-droppable mailbox; id: " + newTarget.mMailboxId); } newTarget.setDropTargetBackground(true, mDragItemMailboxId); targetId = NO_DROP_TARGET; } // Save away our current position and view mDropTargetId = targetId; mDropTargetView = newTarget; startDragTimer(newTarget); } // This is a quick-and-dirty implementation of drag-under-scroll; something like this // should eventually find its way into the framework int scrollDiff = rawTouchY - (mListView.getHeight() - SCROLL_ZONE_SIZE); boolean scrollDown = (scrollDiff > 0); boolean scrollUp = (SCROLL_ZONE_SIZE > rawTouchY); if (!mTargetScrolling && scrollDown) { int itemsToScroll = mListView.getCount() - mListView.getLastVisiblePosition(); int pixelsToScroll = (itemsToScroll + 1) * mDragItemHeight; mListView.smoothScrollBy(pixelsToScroll, pixelsToScroll * SCROLL_SPEED); if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Start scrolling list down"); } mTargetScrolling = true; } else if (!mTargetScrolling && scrollUp) { int pixelsToScroll = (mListView.getFirstVisiblePosition() + 1) * mDragItemHeight; mListView.smoothScrollBy(-pixelsToScroll, pixelsToScroll * SCROLL_SPEED); if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Start scrolling list up"); } mTargetScrolling = true; } else if (!scrollUp && !scrollDown) { stopScrolling(); } } /** * Indicate that scrolling has stopped */ private void stopScrolling() { if (mTargetScrolling) { mTargetScrolling = false; if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Stop scrolling list"); } // Stop the scrolling mListView.smoothScrollBy(0, 0); } } private void onDragEnded() { stopDragTimer(); if (mDragInProgress) { mDragInProgress = false; // Reenable updates to the view and redraw (in case it changed) MailboxesAdapter.enableUpdates(true); mListAdapter.notifyDataSetChanged(); // Stop highlighting targets updateChildViews(); // Stop any scrolling that was going on stopScrolling(); } } private boolean onDragStarted(DragEvent event) { // We handle dropping of items with our email mime type // If the mime type has a mailbox id appended, that is the mailbox of the item // being draged ClipDescription description = event.getClipDescription(); int mimeTypeCount = description.getMimeTypeCount(); for (int i = 0; i < mimeTypeCount; i++) { String mimeType = description.getMimeType(i); if (mimeType.startsWith(EmailProvider.EMAIL_MESSAGE_MIME_TYPE)) { if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Drag started"); } mDragItemMailboxId = -1; // See if we find a mailbox id here int dash = mimeType.lastIndexOf('-'); if (dash > 0) { try { mDragItemMailboxId = Long.parseLong(mimeType.substring(dash + 1)); } catch (NumberFormatException e) { // Ignore; we just won't know the mailbox } } mDragInProgress = true; // Stop the list from updating MailboxesAdapter.enableUpdates(false); // Update the backgrounds of our child views to highlight drop targets updateChildViews(); return true; } } return false; } /** * Perform a "drop" action. If the user is not on top of a valid drop target, no action * is performed. * @return {@code true} if the drop action was performed. Otherwise {@code false}. */ private boolean onDrop(DragEvent event) { stopDragTimer(); stopScrolling(); // If we're not on a target, we're done if (mDropTargetId == NO_DROP_TARGET) { return false; } final Controller controller = Controller.getInstance(mActivity); ClipData clipData = event.getClipData(); int count = clipData.getItemCount(); if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Dropping " + count + " items."); } // Extract the messageId's to move from the ClipData (set up in MessageListItem) final long[] messageIds = new long[count]; for (int i = 0; i < count; i++) { Uri uri = clipData.getItemAt(i).getUri(); String msgNum = uri.getPathSegments().get(1); long id = Long.parseLong(msgNum); messageIds[i] = id; } final MailboxListItem targetItem = mDropTargetView; // Call either deleteMessage or moveMessage, depending on the target EmailAsyncTask.runAsyncSerial(new Runnable() { @Override public void run() { if (targetItem.mMailboxType == Mailbox.TYPE_TRASH) { for (long messageId: messageIds) { // TODO Get this off UI thread (put in clip) Message msg = Message.restoreMessageWithId(mActivity, messageId); if (msg != null) { controller.deleteMessage(messageId, msg.mAccountKey); } } } else { controller.moveMessages(messageIds, targetItem.mMailboxId); } } }); return true; } @Override public boolean onDrag(View view, DragEvent event) { boolean result = false; switch (event.getAction()) { case DragEvent.ACTION_DRAG_STARTED: result = onDragStarted(event); break; case DragEvent.ACTION_DRAG_ENTERED: // The drag has entered the ListView window if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Drag entered; targetId: " + mDropTargetId); } break; case DragEvent.ACTION_DRAG_EXITED: // The drag has left the building if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Drag exited; targetId: " + mDropTargetId); } onDragExited(); break; case DragEvent.ACTION_DRAG_ENDED: // The drag is over if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Drag ended"); } onDragEnded(); break; case DragEvent.ACTION_DRAG_LOCATION: // We're moving around within our window; handle scroll, if necessary onDragLocation(event); break; case DragEvent.ACTION_DROP: // The drag item was dropped if (DEBUG_DRAG_DROP) { Log.d(TAG, "=== Drop"); } result = onDrop(event); break; default: break; } return result; } }