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