MailboxListFragment.java revision 007d0be4eddad21776ba45db5186bb8a9157504c
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.email.activity; 18 19import com.android.email.Controller; 20import com.android.email.Email; 21import com.android.email.R; 22import com.android.email.RefreshManager; 23import com.android.email.provider.EmailProvider; 24import com.android.emailcommon.Logging; 25import com.android.emailcommon.provider.EmailContent.Mailbox; 26import com.android.emailcommon.provider.EmailContent.Message; 27import com.android.emailcommon.utility.EmailAsyncTask; 28import com.android.emailcommon.utility.Utility; 29 30import android.app.Activity; 31import android.app.ListFragment; 32import android.app.LoaderManager; 33import android.app.LoaderManager.LoaderCallbacks; 34import android.content.ClipData; 35import android.content.ClipDescription; 36import android.content.Loader; 37import android.content.res.Resources; 38import android.database.Cursor; 39import android.graphics.drawable.Drawable; 40import android.net.Uri; 41import android.os.Bundle; 42import android.util.Log; 43import android.view.DragEvent; 44import android.view.LayoutInflater; 45import android.view.View; 46import android.view.View.OnDragListener; 47import android.view.ViewGroup; 48import android.widget.AdapterView; 49import android.widget.ListView; 50import android.widget.AdapterView.OnItemClickListener; 51 52import java.security.InvalidParameterException; 53 54/** 55 * This fragment presents a list of mailboxes for a given account. The "API" includes the 56 * following elements which must be provided by the host Activity. 57 * 58 * - call bindActivityInfo() to provide the account ID and set callbacks 59 * - provide callbacks for onOpen and onRefresh 60 * - pass-through implementations of onCreateContextMenu() and onContextItemSelected() (temporary) 61 * 62 * TODO Restoring ListView state -- don't do this when changing accounts 63 */ 64public class MailboxListFragment extends ListFragment implements OnItemClickListener, 65 OnDragListener { 66 private static final String TAG = "MailboxListFragment"; 67 private static final String BUNDLE_KEY_SELECTED_MAILBOX_ID 68 = "MailboxListFragment.state.selected_mailbox_id"; 69 private static final String BUNDLE_LIST_STATE = "MailboxListFragment.state.listState"; 70 private static final boolean DEBUG_DRAG_DROP = false; // MUST NOT SUBMIT SET TO TRUE 71 72 private static final int NO_DROP_TARGET = -1; 73 // Total height of the top and bottom scroll zones, in pixels 74 private static final int SCROLL_ZONE_SIZE = 64; 75 // The amount of time to scroll by one pixel, in ms 76 private static final int SCROLL_SPEED = 4; 77 78 // TODO Clean up usage of mailbox ID. We use both '-1' and '0' to mean "not selected". To 79 // confuse matters, the database uses '-1' for "no mailbox" and '0' for "invalid mailbox". 80 // Once legacy accounts properly support nested folders, we need to make sure we're only 81 // ever using '-1'. 82 // STOPSHIP Change value to '-1' when legacy protocols support folders 83 private final static long DEFAULT_MAILBOX_ID = 0; 84 85 private RefreshManager mRefreshManager; 86 87 // UI Support 88 private Activity mActivity; 89 private MailboxesAdapter mListAdapter; 90 private Callback mCallback = EmptyCallback.INSTANCE; 91 92 private ListView mListView; 93 94 private boolean mResumed; 95 96 // Colors used for drop targets 97 private static Integer sDropTrashColor; 98 private static Drawable sDropActiveDrawable; 99 100 private long mLastLoadedAccountId = -1; 101 private long mAccountId = -1; 102 private long mSelectedMailboxId = DEFAULT_MAILBOX_ID; 103 /** The ID of the mailbox that we have been asked to load */ 104 private long mLoadedMailboxId = -1; 105 106 private boolean mOpenRequested; 107 108 // True if a drag is currently in progress 109 private boolean mDragInProgress = false; 110 // The mailbox id of the dragged item's mailbox. We use it to prevent that box from being a 111 // valid drop target 112 private long mDragItemMailboxId = -1; 113 // The adapter position that the user's finger is hovering over 114 private int mDropTargetAdapterPosition = NO_DROP_TARGET; 115 // The mailbox list item view that the user's finger is hovering over 116 private MailboxListItem mDropTargetView; 117 // Lazily instantiated height of a mailbox list item (-1 is a sentinel for 'not initialized') 118 private int mDragItemHeight = -1; 119 // True if we are currently scrolling under the drag item 120 private boolean mTargetScrolling; 121 122 private Utility.ListStateSaver mSavedListState; 123 124 private MailboxesAdapter.Callback mMailboxesAdapterCallback = new MailboxesAdapter.Callback() { 125 @Override 126 public void onBind(MailboxListItem listItem) { 127 listItem.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); 128 } 129 }; 130 131 /** 132 * Callback interface that owning activities must implement 133 */ 134 public interface Callback { 135 /** 136 * Called when any mailbox (even a combined mailbox) is selected. 137 * @param accountId 138 * The ID of the account for which a mailbox was selected 139 * @param mailboxId 140 * The ID of the selected mailbox. This may be real mailbox ID [e.g. a number > 0], 141 * or a special mailbox ID [e.g. {@link MessageListXLFragmentManager#NO_MAILBOX}, 142 * {@link Mailbox#QUERY_ALL_INBOXES}, etc...]. 143 */ 144 public void onMailboxSelected(long accountId, long mailboxId); 145 146 /** Called when an account is selected on the combined view. */ 147 public void onAccountSelected(long accountId); 148 149 /** 150 * Called when the list updates to propagate the current mailbox name and the unread count 151 * for it. 152 * 153 * Note the reason why it's separated from onMailboxSelected is because this needs to be 154 * reported when the unread count changes without changing the current mailbox. 155 */ 156 public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, int unreadCount); 157 } 158 159 private static class EmptyCallback implements Callback { 160 public static final Callback INSTANCE = new EmptyCallback(); 161 @Override public void onMailboxSelected(long accountId, long mailboxId) { } 162 @Override public void onAccountSelected(long accountId) { } 163 @Override public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, 164 int unreadCount) { } 165 } 166 167 /** 168 * Called to do initial creation of a fragment. This is called after 169 * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}. 170 */ 171 @Override 172 public void onCreate(Bundle savedInstanceState) { 173 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 174 Log.d(Logging.LOG_TAG, "MailboxListFragment onCreate"); 175 } 176 super.onCreate(savedInstanceState); 177 178 mActivity = getActivity(); 179 mRefreshManager = RefreshManager.getInstance(mActivity); 180 mListAdapter = new MailboxFragmentAdapter(mActivity, mMailboxesAdapterCallback); 181 if (savedInstanceState != null) { 182 restoreInstanceState(savedInstanceState); 183 } 184 if (sDropTrashColor == null) { 185 Resources res = getResources(); 186 sDropTrashColor = res.getColor(R.color.mailbox_drop_destructive_bg_color); 187 sDropActiveDrawable = res.getDrawable(R.drawable.list_activated_holo); 188 } 189 } 190 191 @Override 192 public View onCreateView( 193 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 194 return inflater.inflate(R.layout.mailbox_list_fragment, container, false); 195 } 196 197 @Override 198 public void onActivityCreated(Bundle savedInstanceState) { 199 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 200 Log.d(Logging.LOG_TAG, "MailboxListFragment onActivityCreated"); 201 } 202 super.onActivityCreated(savedInstanceState); 203 204 mListView = getListView(); 205 mListView.setOnItemClickListener(this); 206 mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 207 mListView.setOnDragListener(this); 208 registerForContextMenu(mListView); 209 } 210 211 public void setCallback(Callback callback) { 212 mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback; 213 } 214 215 private void clearContent() { 216 getLoaderManager().destroyLoader((int) mLoadedMailboxId); 217 218 mLastLoadedAccountId = -1; 219 mAccountId = -1; 220 mSelectedMailboxId = DEFAULT_MAILBOX_ID; 221 mLoadedMailboxId = -1; 222 223 mOpenRequested = false; 224 mDragInProgress = false; 225 226 if (mListAdapter != null) { 227 mListAdapter.swapCursor(null); 228 } 229 setListShownNoAnimation(false); 230 } 231 232 /** 233 * Opens the top-level mailboxes for the given account ID. If the account is currently 234 * loaded, no actions will be performed. To forcefully load the list of top-level 235 * mailboxes use {@link #openMailboxes(long, boolean)} 236 * @param accountId The ID of the account we want to view 237 */ 238 public void openMailboxes(long accountId) { 239 openMailboxes(accountId, false); 240 } 241 242 /** 243 * Opens the top-level mailboxes for the given account ID. If the account is currently 244 * loaded, the list of top-level mailbox will not be reloaded unless <code>forceReload</code> 245 * is <code>true</code>. 246 * @param accountId The ID of the account we want to view 247 * @param forceReload If <code>true</code>, always load the list of top-level mailboxes. 248 * Otherwise, only load the list of top-level mailboxes if the account changes. 249 */ 250 public void openMailboxes(long accountId, boolean forceReload) { 251 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 252 Log.d(Logging.LOG_TAG, "MailboxListFragment openMailboxes"); 253 } 254 if (accountId == -1) { 255 throw new InvalidParameterException(); 256 } 257 if (!forceReload && mAccountId == accountId) { 258 return; 259 } 260 clearContent(); 261 mOpenRequested = true; 262 mAccountId = accountId; 263 if (mResumed) { 264 startLoading(); 265 } 266 } 267 268 /** 269 * Selects the given mailbox ID and navigates to it. This loads any mailboxes contained 270 * within it. The mailbox is assumed to be associated with the account passed into 271 * {@link #openMailboxes(long)} 272 * @param mailboxId The ID of the mailbox to load. 273 */ 274 public void navigateToMailbox(long mailboxId) { 275 setSelectedMailbox(mailboxId); 276 if (mResumed) { 277 startLoading(); 278 } 279 } 280 281 /** 282 * Sets the selected mailbox to the given ID. Sub-folders will not be loaded. 283 * @param mailboxId The ID of the mailbox to select. 284 */ 285 public void setSelectedMailbox(long mailboxId) { 286 mSelectedMailboxId = mailboxId; 287 if (mResumed) { 288 highlightSelectedMailbox(true); 289 } 290 } 291 292 /** 293 * Called when the Fragment is visible to the user. 294 */ 295 @Override 296 public void onStart() { 297 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 298 Log.d(Logging.LOG_TAG, "MailboxListFragment onStart"); 299 } 300 super.onStart(); 301 } 302 303 /** 304 * Called when the fragment is visible to the user and actively running. 305 */ 306 @Override 307 public void onResume() { 308 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 309 Log.d(Logging.LOG_TAG, "MailboxListFragment onResume"); 310 } 311 super.onResume(); 312 mResumed = true; 313 314 // If we're recovering from the stopped state, we don't have to reload. 315 // (when mOpenRequested = false) 316 if (mAccountId != -1 && mOpenRequested) { 317 startLoading(); 318 } 319 } 320 321 @Override 322 public void onPause() { 323 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 324 Log.d(Logging.LOG_TAG, "MailboxListFragment onPause"); 325 } 326 mResumed = false; 327 super.onPause(); 328 mSavedListState = new Utility.ListStateSaver(getListView()); 329 } 330 331 /** 332 * Called when the Fragment is no longer started. 333 */ 334 @Override 335 public void onStop() { 336 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 337 Log.d(Logging.LOG_TAG, "MailboxListFragment onStop"); 338 } 339 super.onStop(); 340 } 341 342 /** 343 * Called when the fragment is no longer in use. 344 */ 345 @Override 346 public void onDestroy() { 347 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 348 Log.d(Logging.LOG_TAG, "MailboxListFragment onDestroy"); 349 } 350 super.onDestroy(); 351 } 352 353 @Override 354 public void onSaveInstanceState(Bundle outState) { 355 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 356 Log.d(Logging.LOG_TAG, "MailboxListFragment onSaveInstanceState"); 357 } 358 super.onSaveInstanceState(outState); 359 outState.putLong(BUNDLE_KEY_SELECTED_MAILBOX_ID, mSelectedMailboxId); 360 outState.putParcelable(BUNDLE_LIST_STATE, new Utility.ListStateSaver(getListView())); 361 } 362 363 private void restoreInstanceState(Bundle savedInstanceState) { 364 mSelectedMailboxId = savedInstanceState.getLong(BUNDLE_KEY_SELECTED_MAILBOX_ID); 365 mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE); 366 } 367 368 private void startLoading() { 369 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 370 Log.d(Logging.LOG_TAG, "MailboxListFragment startLoading"); 371 } 372 mOpenRequested = false; 373 // Clear the list. (ListFragment will show the "Loading" animation) 374 setListShown(false); 375 376 // If we've already loaded for a different account OR if we've loaded for a different 377 // mailbox, discard the previous result and load again. 378 boolean saveListState = true; 379 final LoaderManager lm = getLoaderManager(); 380 long lastLoadedMailboxId = mLoadedMailboxId; 381 mLoadedMailboxId = mSelectedMailboxId; 382 if ((lastLoadedMailboxId != mSelectedMailboxId) || 383 ((mLastLoadedAccountId != -1) && (mLastLoadedAccountId != mAccountId))) { 384 lm.destroyLoader((int) lastLoadedMailboxId); 385 saveListState = false; 386 refreshMailboxListIfStale(); 387 } 388 /** 389 * Don't use {@link LoaderManager#restartLoader(int, Bundle, LoaderCallbacks)}, because 390 * we want to reuse the previous result if the Loader has been retained. 391 */ 392 lm.initLoader((int)mLoadedMailboxId, null, 393 new MailboxListLoaderCallbacks(saveListState, mLoadedMailboxId)); 394 } 395 396 // TODO This class probably should be made static. There are many calls into the enclosing 397 // class and we need to be cautious about what we call while in these callbacks 398 private class MailboxListLoaderCallbacks implements LoaderCallbacks<Cursor> { 399 private boolean mSaveListState; 400 private final long mMailboxId; 401 402 public MailboxListLoaderCallbacks(boolean saveListState, long mailboxId) { 403 mSaveListState = saveListState; 404 mMailboxId = mailboxId; 405 } 406 407 @Override 408 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 409 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 410 Log.d(Logging.LOG_TAG, "MailboxListFragment onCreateLoader"); 411 } 412 return MailboxFragmentAdapter.createLoader(getActivity(), mAccountId, mMailboxId); 413 } 414 415 @Override 416 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 417 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 418 Log.d(Logging.LOG_TAG, "MailboxListFragment onLoadFinished"); 419 } 420 if (mMailboxId != mLoadedMailboxId) { 421 return; 422 } 423 mLastLoadedAccountId = mAccountId; 424 425 // Save list view state (primarily scroll position) 426 final ListView lv = getListView(); 427 final Utility.ListStateSaver lss; 428 if (!mSaveListState) { 429 lss = null; // Don't preserve list state 430 } else if (mSavedListState != null) { 431 lss = mSavedListState; 432 mSavedListState = null; 433 } else { 434 lss = new Utility.ListStateSaver(lv); 435 } 436 437 if (cursor.getCount() == 0) { 438 // If there's no row, don't set it to the ListView. 439 // Instead use setListShown(false) to make ListFragment show progress icon. 440 mListAdapter.swapCursor(null); 441 setListShown(false); 442 } else { 443 // Set the adapter. 444 mListAdapter.swapCursor(cursor); 445 setListAdapter(mListAdapter); 446 setListShown(true); 447 448 // We want to make selection visible only when account is changing.. 449 // i.e. Refresh caused by content changed events shouldn't scroll the list. 450 highlightSelectedMailbox(!mSaveListState); 451 } 452 453 // Restore the state 454 if (lss != null) { 455 lss.restore(lv); 456 } 457 458 // Clear this for next reload triggered by content changed events. 459 mSaveListState = true; 460 } 461 462 @Override 463 public void onLoaderReset(Loader<Cursor> loader) { 464 if (mMailboxId != mLoadedMailboxId) { 465 return; 466 } 467 mListAdapter.swapCursor(null); 468 } 469 } 470 471 public void onItemClick(AdapterView<?> parent, View view, int position, 472 long idDontUseIt /* see MailboxesAdapter */ ) { 473 final long id = mListAdapter.getId(position); 474 if (mListAdapter.isAccountRow(position)) { 475 mCallback.onAccountSelected(id); 476 } else { 477 mCallback.onMailboxSelected(mAccountId, id); 478 } 479 } 480 481 public void onRefresh() { 482 if (mAccountId != -1) { 483 mRefreshManager.refreshMailboxList(mAccountId); 484 } 485 } 486 487 private void refreshMailboxListIfStale() { 488 if (mRefreshManager.isMailboxListStale(mAccountId)) { 489 mRefreshManager.refreshMailboxList(mAccountId); 490 } 491 } 492 493 /** 494 * Highlight the selected mailbox. 495 */ 496 private void highlightSelectedMailbox(boolean ensureSelectionVisible) { 497 String mailboxName = ""; 498 int unreadCount = 0; 499 if (mSelectedMailboxId == DEFAULT_MAILBOX_ID) { 500 // No mailbox selected 501 mListView.clearChoices(); 502 } else { 503 final int count = mListView.getCount(); 504 for (int i = 0; i < count; i++) { 505 if (mListAdapter.getId(i) != mSelectedMailboxId) { 506 continue; 507 } 508 mListView.setItemChecked(i, true); 509 if (ensureSelectionVisible) { 510 Utility.listViewSmoothScrollToPosition(getActivity(), mListView, i); 511 } 512 mailboxName = mListAdapter.getDisplayName(mActivity, i); 513 unreadCount = mListAdapter.getUnreadCount(i); 514 break; 515 } 516 } 517 mCallback.onCurrentMailboxUpdated(mSelectedMailboxId, mailboxName, unreadCount); 518 } 519 520 // Drag & Drop handling 521 522 /** 523 * Update all of the list's child views with the proper target background (for now, orange if 524 * a valid target, except red if the trash; standard background otherwise) 525 */ 526 private void updateChildViews() { 527 int itemCount = mListView.getChildCount(); 528 // Lazily initialize the height of our list items 529 if (itemCount > 0 && mDragItemHeight < 0) { 530 mDragItemHeight = mListView.getChildAt(0).getHeight(); 531 } 532 for (int i = 0; i < itemCount; i++) { 533 MailboxListItem item = (MailboxListItem)mListView.getChildAt(i); 534 item.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); 535 } 536 } 537 538 /** 539 * Called when our ListView gets a DRAG_EXITED event 540 */ 541 private void onDragExited() { 542 // Reset the background of the current target 543 if (mDropTargetAdapterPosition != NO_DROP_TARGET) { 544 mDropTargetView.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); 545 mDropTargetAdapterPosition = NO_DROP_TARGET; 546 } 547 stopScrolling(); 548 } 549 550 /** 551 * Called while dragging; highlight possible drop targets, and autoscroll the list. 552 */ 553 private void onDragLocation(DragEvent event) { 554 // The drag is somewhere in the ListView 555 if (mDragItemHeight <= 0) { 556 // This shouldn't be possible, but avoid NPE 557 return; 558 } 559 // Find out which item we're in and highlight as appropriate 560 int rawTouchY = (int)event.getY(); 561 int offset = 0; 562 if (mListView.getCount() > 0) { 563 offset = mListView.getChildAt(0).getTop(); 564 } 565 int targetScreenPosition = (rawTouchY - offset) / mDragItemHeight; 566 int firstVisibleItem = mListView.getFirstVisiblePosition(); 567 int targetAdapterPosition = firstVisibleItem + targetScreenPosition; 568 if (targetAdapterPosition != mDropTargetAdapterPosition) { 569 if (DEBUG_DRAG_DROP) { 570 Log.d(TAG, "========== DROP TARGET " + mDropTargetAdapterPosition + " -> " + 571 targetAdapterPosition); 572 } 573 // Unhighlight the current target, if we've got one 574 if (mDropTargetAdapterPosition != NO_DROP_TARGET) { 575 mDropTargetView.setDropTargetBackground(true, mDragItemMailboxId); 576 } 577 // Get the new target mailbox view 578 MailboxListItem newTarget = 579 (MailboxListItem)mListView.getChildAt(targetScreenPosition); 580 // This can be null due to a bug in the framework (checking on that) 581 // In any event, we're no longer dragging in the list view if newTarget is null 582 if (newTarget == null) { 583 if (DEBUG_DRAG_DROP) { 584 Log.d(TAG, "========== WTF??? DRAG EXITED"); 585 } 586 onDragExited(); 587 return; 588 } else if (newTarget.mMailboxType == Mailbox.TYPE_TRASH) { 589 if (DEBUG_DRAG_DROP) { 590 Log.d("onDragLocation", "=== Mailbox " + newTarget.mMailboxId + " TRASH"); 591 } 592 newTarget.setBackgroundColor(sDropTrashColor); 593 } else if (newTarget.isDropTarget(mDragItemMailboxId)) { 594 if (DEBUG_DRAG_DROP) { 595 Log.d("onDragLocation", "=== Mailbox " + newTarget.mMailboxId + " TARGET"); 596 } 597 newTarget.setBackgroundDrawable(sDropActiveDrawable); 598 } else { 599 if (DEBUG_DRAG_DROP) { 600 Log.d("onDragLocation", "=== Mailbox " + newTarget.mMailboxId + " (CALL)"); 601 } 602 targetAdapterPosition = NO_DROP_TARGET; 603 newTarget.setDropTargetBackground(true, mDragItemMailboxId); 604 } 605 // Save away our current position and view 606 mDropTargetAdapterPosition = targetAdapterPosition; 607 mDropTargetView = newTarget; 608 } 609 610 // This is a quick-and-dirty implementation of drag-under-scroll; something like this 611 // should eventually find its way into the framework 612 int scrollDiff = rawTouchY - (mListView.getHeight() - SCROLL_ZONE_SIZE); 613 boolean scrollDown = (scrollDiff > 0); 614 boolean scrollUp = (SCROLL_ZONE_SIZE > rawTouchY); 615 if (!mTargetScrolling && scrollDown) { 616 int itemsToScroll = mListView.getCount() - targetAdapterPosition; 617 int pixelsToScroll = (itemsToScroll + 1) * mDragItemHeight; 618 mListView.smoothScrollBy(pixelsToScroll, pixelsToScroll * SCROLL_SPEED); 619 if (DEBUG_DRAG_DROP) { 620 Log.d(TAG, "========== START TARGET SCROLLING DOWN"); 621 } 622 mTargetScrolling = true; 623 } else if (!mTargetScrolling && scrollUp) { 624 int pixelsToScroll = (firstVisibleItem + 1) * mDragItemHeight; 625 mListView.smoothScrollBy(-pixelsToScroll, pixelsToScroll * SCROLL_SPEED); 626 if (DEBUG_DRAG_DROP) { 627 Log.d(TAG, "========== START TARGET SCROLLING UP"); 628 } 629 mTargetScrolling = true; 630 } else if (!scrollUp && !scrollDown) { 631 stopScrolling(); 632 } 633 } 634 635 /** 636 * Indicate that scrolling has stopped 637 */ 638 private void stopScrolling() { 639 if (mTargetScrolling) { 640 mTargetScrolling = false; 641 if (DEBUG_DRAG_DROP) { 642 Log.d(TAG, "========== STOP TARGET SCROLLING"); 643 } 644 // Stop the scrolling 645 mListView.smoothScrollBy(0, 0); 646 } 647 } 648 649 private void onDragEnded() { 650 if (mDragInProgress) { 651 mDragInProgress = false; 652 // Reenable updates to the view and redraw (in case it changed) 653 MailboxesAdapter.enableUpdates(true); 654 mListAdapter.notifyDataSetChanged(); 655 // Stop highlighting targets 656 updateChildViews(); 657 // Stop any scrolling that was going on 658 stopScrolling(); 659 } 660 } 661 662 private boolean onDragStarted(DragEvent event) { 663 // We handle dropping of items with our email mime type 664 // If the mime type has a mailbox id appended, that is the mailbox of the item 665 // being draged 666 ClipDescription description = event.getClipDescription(); 667 int mimeTypeCount = description.getMimeTypeCount(); 668 for (int i = 0; i < mimeTypeCount; i++) { 669 String mimeType = description.getMimeType(i); 670 if (mimeType.startsWith(EmailProvider.EMAIL_MESSAGE_MIME_TYPE)) { 671 if (DEBUG_DRAG_DROP) { 672 Log.d(TAG, "========== DRAG STARTED"); 673 } 674 mDragItemMailboxId = -1; 675 // See if we find a mailbox id here 676 int dash = mimeType.lastIndexOf('-'); 677 if (dash > 0) { 678 try { 679 mDragItemMailboxId = Long.parseLong(mimeType.substring(dash + 1)); 680 } catch (NumberFormatException e) { 681 // Ignore; we just won't know the mailbox 682 } 683 } 684 mDragInProgress = true; 685 // Stop the list from updating 686 MailboxesAdapter.enableUpdates(false); 687 // Update the backgrounds of our child views to highlight drop targets 688 updateChildViews(); 689 return true; 690 } 691 } 692 return false; 693 } 694 695 private boolean onDrop(DragEvent event) { 696 stopScrolling(); 697 // If we're not on a target, we're done 698 if (mDropTargetAdapterPosition == NO_DROP_TARGET) return false; 699 final Controller controller = Controller.getInstance(mActivity); 700 ClipData clipData = event.getClipData(); 701 int count = clipData.getItemCount(); 702 if (DEBUG_DRAG_DROP) { 703 Log.d(TAG, "Received a drop of " + count + " items."); 704 } 705 // Extract the messageId's to move from the ClipData (set up in MessageListItem) 706 final long[] messageIds = new long[count]; 707 for (int i = 0; i < count; i++) { 708 Uri uri = clipData.getItemAt(i).getUri(); 709 String msgNum = uri.getPathSegments().get(1); 710 long id = Long.parseLong(msgNum); 711 messageIds[i] = id; 712 } 713 // Call either deleteMessage or moveMessage, depending on the target 714 EmailAsyncTask.runAsyncSerial(new Runnable() { 715 @Override 716 public void run() { 717 if (mDropTargetView.mMailboxType == Mailbox.TYPE_TRASH) { 718 for (long messageId: messageIds) { 719 // TODO Get this off UI thread (put in clip) 720 Message msg = Message.restoreMessageWithId(mActivity, messageId); 721 if (msg != null) { 722 controller.deleteMessage(messageId, msg.mAccountKey); 723 } 724 } 725 } else { 726 controller.moveMessage(messageIds, mDropTargetView.mMailboxId); 727 } 728 } 729 }); 730 return true; 731 } 732 733 @Override 734 public boolean onDrag(View view, DragEvent event) { 735 boolean result = false; 736 switch (event.getAction()) { 737 case DragEvent.ACTION_DRAG_STARTED: 738 result = onDragStarted(event); 739 break; 740 case DragEvent.ACTION_DRAG_ENTERED: 741 // The drag has entered the ListView window 742 if (DEBUG_DRAG_DROP) { 743 Log.d(TAG, "========== DRAG ENTERED (target = " + mDropTargetAdapterPosition + 744 ")"); 745 } 746 break; 747 case DragEvent.ACTION_DRAG_EXITED: 748 // The drag has left the building 749 if (DEBUG_DRAG_DROP) { 750 Log.d(TAG, "========== DRAG EXITED (target = " + mDropTargetAdapterPosition + 751 ")"); 752 } 753 onDragExited(); 754 break; 755 case DragEvent.ACTION_DRAG_ENDED: 756 // The drag is over 757 if (DEBUG_DRAG_DROP) { 758 Log.d(TAG, "========== DRAG ENDED"); 759 } 760 onDragEnded(); 761 break; 762 case DragEvent.ACTION_DRAG_LOCATION: 763 // We're moving around within our window; handle scroll, if necessary 764 onDragLocation(event); 765 break; 766 case DragEvent.ACTION_DROP: 767 // The drag item was dropped 768 if (DEBUG_DRAG_DROP) { 769 Log.d(TAG, "========== DROP"); 770 } 771 result = onDrop(event); 772 break; 773 default: 774 break; 775 } 776 return result; 777 } 778} 779