MailboxListFragment.java revision b36ac017926f2557f4a476d0cefe49002d11233b
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.Account; 26import com.android.emailcommon.provider.EmailContent.Message; 27import com.android.emailcommon.provider.Mailbox; 28import com.android.emailcommon.utility.EmailAsyncTask; 29import com.android.emailcommon.utility.Utility; 30 31import android.app.Activity; 32import android.app.ListFragment; 33import android.app.LoaderManager; 34import android.app.LoaderManager.LoaderCallbacks; 35import android.content.ClipData; 36import android.content.ClipDescription; 37import android.content.Loader; 38import android.content.res.Resources; 39import android.database.Cursor; 40import android.graphics.Rect; 41import android.graphics.drawable.Drawable; 42import android.net.Uri; 43import android.os.Bundle; 44import android.os.Parcelable; 45import android.util.Log; 46import android.view.DragEvent; 47import android.view.LayoutInflater; 48import android.view.View; 49import android.view.View.OnDragListener; 50import android.view.ViewGroup; 51import android.widget.AdapterView; 52import android.widget.AdapterView.OnItemClickListener; 53import android.widget.ListView; 54 55import java.util.Timer; 56import java.util.TimerTask; 57 58/** 59 * This fragment presents a list of mailboxes for a given account. 60 */ 61public class MailboxListFragment extends ListFragment implements OnItemClickListener, 62 OnDragListener { 63 private static final String TAG = "MailboxListFragment"; 64 private static final String BUNDLE_KEY_SELECTED_MAILBOX_ID 65 = "MailboxListFragment.state.selected_mailbox_id"; 66 private static final String BUNDLE_LIST_STATE = "MailboxListFragment.state.listState"; 67 private static final boolean DEBUG_DRAG_DROP = false; // MUST NOT SUBMIT SET TO TRUE 68 /** While in drag-n-drop, amount of time before it auto expands; in ms */ 69 private static final long AUTO_EXPAND_DELAY = 750L; 70 71 /** No drop target is available where the user is currently hovering over */ 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 /** Arbitrary number for use with the loader manager */ 79 private static final int MAILBOX_LOADER_ID = 1; 80 81 /** Argument name(s) */ 82 private static final String ARG_ACCOUNT_ID = "accountId"; 83 private static final String ARG_PARENT_MAILBOX_ID = "parentMailboxId"; 84 85 /** Timer to auto-expand folder lists during drag-n-drop */ 86 private static final Timer sDragTimer = new Timer(); 87 /** Rectangle used for hit testing children */ 88 private static final Rect sTouchFrame = new Rect(); 89 90 private RefreshManager mRefreshManager; 91 92 // UI Support 93 private Activity mActivity; 94 private MailboxesAdapter mListAdapter; 95 private Callback mCallback = EmptyCallback.INSTANCE; 96 97 private ListView mListView; 98 99 private boolean mResumed; 100 101 // Colors used for drop targets 102 private static Integer sDropTrashColor; 103 private static Drawable sDropActiveDrawable; 104 105 /** 106 * Account ID passed to {@link #newInstance}. Cache of {@link #getAccountIdArg()}, but usable 107 * only after {@link #onCreate}. 108 */ 109 private long mAccountId; 110 111 /** 112 * Mailbox ID passed to {@link #newInstance}. Cache of {@link #getParentMailboxIdArg()}, but 113 * usable only after {@link #onCreate}. 114 */ 115 private long mParentMailboxId; 116 117 /** ID of the mailbox to hightlight. */ 118 private long mSelectedMailboxId = -1; 119 120 // True if a drag is currently in progress 121 private boolean mDragInProgress; 122 /** Mailbox ID of the item being dragged. Used to determine valid drop targets. */ 123 private long mDragItemMailboxId = -1; 124 /** A unique identifier for the drop target. May be {@link #NO_DROP_TARGET}. */ 125 private int mDropTargetId = NO_DROP_TARGET; 126 // The mailbox list item view that the user's finger is hovering over 127 private MailboxListItem mDropTargetView; 128 // Lazily instantiated height of a mailbox list item (-1 is a sentinel for 'not initialized') 129 private int mDragItemHeight = -1; 130 /** Task that actually does the work to auto-expand folder lists during drag-n-drop */ 131 private TimerTask mDragTimerTask; 132 // True if we are currently scrolling under the drag item 133 private boolean mTargetScrolling; 134 135 private Parcelable mSavedListState; 136 137 private final MailboxesAdapter.Callback mMailboxesAdapterCallback = 138 new MailboxesAdapter.Callback() { 139 @Override 140 public void onBind(MailboxListItem listItem) { 141 listItem.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); 142 } 143 }; 144 145 /** 146 * Callback interface that owning activities must implement 147 */ 148 public interface Callback { 149 /** 150 * STOPSHIP split this into separate callbacks. 151 * - Drill in to a mailbox and open a mailbox (= show message list) are different operations 152 * on the phone 153 * - Regular navigation and navigation for D&D are different; the latter case we probably 154 * want to go back to the original mailbox afterwards. (Need another callback for this) 155 * 156 * Called when any mailbox (even a combined mailbox) is selected. 157 * @param accountId 158 * The ID of the account for which a mailbox was selected 159 * @param mailboxId 160 * The ID of the selected mailbox. This may be real mailbox ID [e.g. a number > 0], 161 * or a combined mailbox ID [e.g. {@link Mailbox#QUERY_ALL_INBOXES}]. 162 * @param navigate navigate to the mailbox. 163 * @param dragDrop true if D&D is in progress. 164 */ 165 public void onMailboxSelected(long accountId, long mailboxId, boolean navigate, 166 boolean dragDrop); 167 168 /** Called when an account is selected on the combined view. */ 169 public void onAccountSelected(long accountId); 170 171 /** 172 * Called when the list updates to propagate the current mailbox name and the unread count 173 * for it. 174 * 175 * Note the reason why it's separated from onMailboxSelected is because this needs to be 176 * reported when the unread count changes without changing the current mailbox. 177 */ 178 public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, int unreadCount); 179 } 180 181 private static class EmptyCallback implements Callback { 182 public static final Callback INSTANCE = new EmptyCallback(); 183 @Override public void onMailboxSelected(long accountId, long mailboxId, boolean navigate, 184 boolean dragDrop) { 185 } 186 @Override public void onAccountSelected(long accountId) { } 187 @Override public void onCurrentMailboxUpdated(long mailboxId, String mailboxName, 188 int unreadCount) { } 189 } 190 191 /** 192 * Returns the index of the view located at the specified coordinates in the given list. 193 * If the coordinates are outside of the list, {@code NO_DROP_TARGET} is returned. 194 */ 195 private static int pointToIndex(ListView list, int x, int y) { 196 final int count = list.getChildCount(); 197 for (int i = count - 1; i >= 0; i--) { 198 final View child = list.getChildAt(i); 199 if (child.getVisibility() == View.VISIBLE) { 200 child.getHitRect(sTouchFrame); 201 if (sTouchFrame.contains(x, y)) { 202 return i; 203 } 204 } 205 } 206 return NO_DROP_TARGET; 207 } 208 209 /** 210 * Create a new instance with initialization parameters. 211 * 212 * This fragment should be created only with this method. (Arguments should always be set.) 213 * 214 * @param accountId The ID of the account we want to view 215 * @param parentMailboxId The ID of the parent mailbox. Use {@link Mailbox#PARENT_KEY_NONE} 216 * to open the root. 217 */ 218 public static MailboxListFragment newInstance(long accountId, long parentMailboxId) { 219 if (accountId == Account.NO_ACCOUNT) { 220 throw new IllegalArgumentException(); 221 } 222 final MailboxListFragment instance = new MailboxListFragment(); 223 final Bundle args = new Bundle(); 224 args.putLong(ARG_ACCOUNT_ID, accountId); 225 args.putLong(ARG_PARENT_MAILBOX_ID, parentMailboxId); 226 instance.setArguments(args); 227 return instance; 228 } 229 230 /** @return the account ID passed to {@link #newInstance}. */ 231 public long getAccountIdArg() { 232 return getArguments().getLong(ARG_ACCOUNT_ID); 233 } 234 235 /** @return the mailbox ID passed to {@link #newInstance}. */ 236 public long getParentMailboxIdArg() { 237 return getArguments().getLong(ARG_PARENT_MAILBOX_ID); 238 } 239 240 /** 241 * Called to do initial creation of a fragment. This is called after 242 * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}. 243 */ 244 @Override 245 public void onCreate(Bundle savedInstanceState) { 246 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 247 Log.d(Logging.LOG_TAG, "MailboxListFragment onCreate"); 248 } 249 super.onCreate(savedInstanceState); 250 251 mAccountId = getAccountIdArg(); 252 mParentMailboxId = getParentMailboxIdArg(); 253 254 mActivity = getActivity(); 255 mRefreshManager = RefreshManager.getInstance(mActivity); 256 mListAdapter = new MailboxFragmentAdapter(mActivity, mMailboxesAdapterCallback); 257 if (savedInstanceState != null) { 258 restoreInstanceState(savedInstanceState); 259 } 260 if (sDropTrashColor == null) { 261 Resources res = getResources(); 262 sDropTrashColor = res.getColor(R.color.mailbox_drop_destructive_bg_color); 263 sDropActiveDrawable = res.getDrawable(R.drawable.list_activated_holo); 264 } 265 } 266 267 @Override 268 public View onCreateView( 269 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 270 return inflater.inflate(R.layout.mailbox_list_fragment, container, false); 271 } 272 273 @Override 274 public void onActivityCreated(Bundle savedInstanceState) { 275 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 276 Log.d(Logging.LOG_TAG, "MailboxListFragment onActivityCreated"); 277 } 278 super.onActivityCreated(savedInstanceState); 279 280 mListView = getListView(); 281 mListView.setOnItemClickListener(this); 282 mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 283 mListView.setOnDragListener(this); 284 registerForContextMenu(mListView); 285 286 startLoading(); 287 } 288 289 public void setCallback(Callback callback) { 290 mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback; 291 } 292 293 /** 294 * Returns whether or not the specified mailbox can be navigated to. 295 */ 296 private boolean isNavigable(long mailboxId) { 297 final int count = mListView.getCount(); 298 for (int i = 0; i < count; i++) { 299 final MailboxListItem item = (MailboxListItem) mListView.getChildAt(i); 300 if (item.mMailboxId != mailboxId) { 301 continue; 302 } 303 return item.isNavigable(); 304 } 305 return false; 306 } 307 308 /** 309 * Sets the selected mailbox to the given ID. Sub-folders will not be loaded. 310 * @param mailboxId The ID of the mailbox to select. 311 */ 312 public void setSelectedMailbox(long mailboxId) { 313 mSelectedMailboxId = mailboxId; 314 if (mResumed) { 315 highlightSelectedMailbox(true); 316 } 317 } 318 319 /** 320 * Called when the Fragment is visible to the user. 321 */ 322 @Override 323 public void onStart() { 324 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 325 Log.d(Logging.LOG_TAG, "MailboxListFragment onStart"); 326 } 327 super.onStart(); 328 } 329 330 /** 331 * Called when the fragment is visible to the user and actively running. 332 */ 333 @Override 334 public void onResume() { 335 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 336 Log.d(Logging.LOG_TAG, "MailboxListFragment onResume"); 337 } 338 super.onResume(); 339 mResumed = true; 340 341 // Fetch the latest mailbox list from the server here if stale so that the user always 342 // sees the (reasonably) up-to-date mailbox list, without pressing "refresh". 343 if (mRefreshManager.isMailboxListStale(mAccountId)) { 344 mRefreshManager.refreshMailboxList(mAccountId); 345 } 346 } 347 348 @Override 349 public void onPause() { 350 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 351 Log.d(Logging.LOG_TAG, "MailboxListFragment onPause"); 352 } 353 mResumed = false; 354 super.onPause(); 355 mSavedListState = getListView().onSaveInstanceState(); 356 } 357 358 /** 359 * Called when the Fragment is no longer started. 360 */ 361 @Override 362 public void onStop() { 363 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 364 Log.d(Logging.LOG_TAG, "MailboxListFragment onStop"); 365 } 366 super.onStop(); 367 } 368 369 /** 370 * Called when the fragment is no longer in use. 371 */ 372 @Override 373 public void onDestroy() { 374 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 375 Log.d(Logging.LOG_TAG, "MailboxListFragment onDestroy"); 376 } 377 super.onDestroy(); 378 } 379 380 @Override 381 public void onSaveInstanceState(Bundle outState) { 382 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 383 Log.d(Logging.LOG_TAG, "MailboxListFragment onSaveInstanceState"); 384 } 385 super.onSaveInstanceState(outState); 386 outState.putLong(BUNDLE_KEY_SELECTED_MAILBOX_ID, mSelectedMailboxId); 387 outState.putParcelable(BUNDLE_LIST_STATE, getListView().onSaveInstanceState()); 388 } 389 390 private void restoreInstanceState(Bundle savedInstanceState) { 391 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 392 Log.d(Logging.LOG_TAG, "MailboxListFragment restoreInstanceState"); 393 } 394 mSelectedMailboxId = savedInstanceState.getLong(BUNDLE_KEY_SELECTED_MAILBOX_ID); 395 mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE); 396 } 397 398 private void startLoading() { 399 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 400 Log.d(Logging.LOG_TAG, "MailboxListFragment startLoading"); 401 } 402 // Clear the list. (ListFragment will show the "Loading" animation) 403 setListShown(false); 404 405 final LoaderManager lm = getLoaderManager(); 406 lm.initLoader(MAILBOX_LOADER_ID, null, new MailboxListLoaderCallbacks()); 407 } 408 409 // TODO This class probably should be made static. There are many calls into the enclosing 410 // class and we need to be cautious about what we call while in these callbacks 411 private class MailboxListLoaderCallbacks implements LoaderCallbacks<Cursor> { 412 private boolean mIsFirstLoad; 413 414 @Override 415 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 416 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 417 Log.d(Logging.LOG_TAG, "MailboxListFragment onCreateLoader"); 418 } 419 mIsFirstLoad = true; 420 return MailboxFragmentAdapter.createLoader(getActivity(), mAccountId, mParentMailboxId); 421 } 422 423 @Override 424 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 425 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 426 Log.d(Logging.LOG_TAG, "MailboxListFragment onLoadFinished"); 427 } 428 // Save list view state (primarily scroll position) 429 final ListView lv = getListView(); 430 final Parcelable listState; 431 if (mSavedListState != null) { 432 listState = mSavedListState; 433 mSavedListState = null; 434 } else { 435 listState = lv.onSaveInstanceState(); 436 } 437 438 if (cursor.getCount() == 0) { 439 // If there's no row, don't set it to the ListView. 440 // Instead use setListShown(false) to make ListFragment show progress icon. 441 mListAdapter.swapCursor(null); 442 setListShown(false); 443 } else { 444 // Set the adapter. 445 mListAdapter.swapCursor(cursor); 446 setListAdapter(mListAdapter); 447 setListShown(true); 448 449 // We want to make visible the selection only for the first load. 450 // Re-load caused by content changed events shouldn't scroll the list. 451 highlightSelectedMailbox(mIsFirstLoad); 452 } 453 454 // List has been reloaded; clear any drop target information 455 mDropTargetId = NO_DROP_TARGET; 456 mDropTargetView = null; 457 458 // Restore the state 459 if (listState != null) { 460 lv.onRestoreInstanceState(listState); 461 } 462 463 mIsFirstLoad = false; 464 } 465 466 @Override 467 public void onLoaderReset(Loader<Cursor> loader) { 468 mListAdapter.swapCursor(null); 469 } 470 } 471 472 /** 473 * {@inheritDoc} 474 * <p> 475 * @param doNotUse <em>IMPORTANT</em>: Do not use this parameter. The ID in the list widget 476 * must be a positive value. However, we rely on negative IDs for special mailboxes. Instead, 477 * we use the ID returned by {@link MailboxesAdapter#getId(int)}. 478 */ 479 @Override 480 public void onItemClick(AdapterView<?> parent, View view, int position, long doNotUse) { 481 final long id = mListAdapter.getId(position); 482 if (mListAdapter.isAccountRow(position)) { 483 mCallback.onAccountSelected(id); 484 } else { 485 // STOPSHIP On phone, we need a way to open a message list without navigating to the 486 // mailbox. 487 mCallback.onMailboxSelected(mAccountId, id, isNavigable(id), false); 488 } 489 } 490 491 /** 492 * Highlight the selected mailbox. 493 */ 494 private void highlightSelectedMailbox(boolean ensureSelectionVisible) { 495 String mailboxName = ""; 496 int unreadCount = 0; 497 if (mSelectedMailboxId == -1) { 498 // No mailbox selected 499 mListView.clearChoices(); 500 } else { 501 // TODO Don't mix list view & list adapter indices. This is a recipe for disaster. 502 final int count = mListView.getCount(); 503 for (int i = 0; i < count; i++) { 504 if (mListAdapter.getId(i) != mSelectedMailboxId) { 505 continue; 506 } 507 mListView.setItemChecked(i, true); 508 if (ensureSelectionVisible) { 509 Utility.listViewSmoothScrollToPosition(getActivity(), mListView, i); 510 } 511 mailboxName = mListAdapter.getDisplayName(mActivity, i); 512 unreadCount = mListAdapter.getUnreadCount(i); 513 break; 514 } 515 } 516 mCallback.onCurrentMailboxUpdated(mSelectedMailboxId, mailboxName, unreadCount); 517 } 518 519 // Drag & Drop handling 520 521 /** 522 * Update all of the list's child views with the proper target background (for now, orange if 523 * a valid target, except red if the trash; standard background otherwise) 524 */ 525 private void updateChildViews() { 526 int itemCount = mListView.getChildCount(); 527 // Lazily initialize the height of our list items 528 if (itemCount > 0 && mDragItemHeight < 0) { 529 mDragItemHeight = mListView.getChildAt(0).getHeight(); 530 } 531 for (int i = 0; i < itemCount; i++) { 532 MailboxListItem item = (MailboxListItem)mListView.getChildAt(i); 533 item.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); 534 } 535 } 536 537 /** 538 * Starts the timer responsible for auto-selecting mailbox items while in drag-n-drop. 539 * If there is already an active task, we first try to cancel it. There are only two 540 * reasons why a new timer may not be started. First, if we are unable to cancel a 541 * previous timer, we must assume that a new mailbox has already been loaded. Second, 542 * if the target item is not permitted to be auto selected. 543 * @param newTarget The drag target that needs to be auto selected 544 */ 545 private void startDragTimer(final MailboxListItem newTarget) { 546 boolean canceledInTime = mDragTimerTask == null || stopDragTimer(); 547 if (canceledInTime 548 && newTarget != null 549 && newTarget.isNavigable() 550 && newTarget.isDropTarget(mDragItemMailboxId)) { 551 mDragTimerTask = new TimerTask() { 552 @Override 553 public void run() { 554 mActivity.runOnUiThread(new Runnable() { 555 @Override 556 public void run() { 557 stopDragTimer(); 558 // STOPSHIP Revisit this -- probably we need a different callback 559 // so that when D&D finishes we can go back to the original mailbox. 560 mCallback.onMailboxSelected(mAccountId, newTarget.mMailboxId, true, 561 true); 562 } 563 }); 564 } 565 }; 566 sDragTimer.schedule(mDragTimerTask, AUTO_EXPAND_DELAY); 567 } 568 } 569 570 /** 571 * Stops the timer responsible for auto-selecting mailbox items while in drag-n-drop. 572 * If the timer is not active, nothing will happen. 573 * @return Whether or not the timer was interrupted. {@link TimerTask#cancel()}. 574 */ 575 private boolean stopDragTimer() { 576 boolean timerInterrupted = false; 577 synchronized (sDragTimer) { 578 if (mDragTimerTask != null) { 579 timerInterrupted = mDragTimerTask.cancel(); 580 mDragTimerTask = null; 581 } 582 } 583 return timerInterrupted; 584 } 585 586 /** 587 * Called when the user has dragged outside of the mailbox list area. 588 */ 589 private void onDragExited() { 590 // Reset the background of the current target 591 if (mDropTargetView != null) { 592 mDropTargetView.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); 593 mDropTargetView = null; 594 } 595 mDropTargetId = NO_DROP_TARGET; 596 stopDragTimer(); 597 stopScrolling(); 598 } 599 600 /** 601 * Called while dragging; highlight possible drop targets, and auto scroll the list. 602 */ 603 private void onDragLocation(DragEvent event) { 604 // TODO The list may be changing while in drag-n-drop; temporarily suspend drag-n-drop 605 // if the list is being updated [i.e. navigated to another mailbox] 606 if (mDragItemHeight <= 0) { 607 // This shouldn't be possible, but avoid NPE 608 Log.w(TAG, "drag item height is not set"); 609 return; 610 } 611 // Find out which item we're in and highlight as appropriate 612 final int rawTouchX = (int) event.getX(); 613 final int rawTouchY = (int) event.getY(); 614 final int viewIndex = pointToIndex(mListView, rawTouchX, rawTouchY); 615 int targetId = viewIndex; 616 if (targetId != mDropTargetId) { 617 if (DEBUG_DRAG_DROP) { 618 Log.d(TAG, "=== Target changed; oldId: " + mDropTargetId + ", newId: " + targetId); 619 } 620 // Remove highlight the current target; if there was one 621 if (mDropTargetView != null) { 622 mDropTargetView.setDropTargetBackground(true, mDragItemMailboxId); 623 mDropTargetView = null; 624 } 625 // Get the new target mailbox view 626 final MailboxListItem newTarget = (MailboxListItem) mListView.getChildAt(viewIndex); 627 if (newTarget == null) { 628 // In any event, we're no longer dragging in the list view if newTarget is null 629 if (DEBUG_DRAG_DROP) { 630 Log.d(TAG, "=== Drag off the list"); 631 } 632 final int childCount = mListView.getChildCount(); 633 if (viewIndex >= childCount) { 634 // Touching beyond the end of the list; may happen for small lists 635 onDragExited(); 636 return; 637 } else { 638 // We should never get here 639 Log.w(TAG, "null view; idx: " + viewIndex + ", cnt: " + childCount); 640 } 641 } else if (newTarget.mMailboxType == Mailbox.TYPE_TRASH) { 642 if (DEBUG_DRAG_DROP) { 643 Log.d(TAG, "=== Trash mailbox; id: " + newTarget.mMailboxId); 644 } 645 newTarget.setBackgroundColor(sDropTrashColor); 646 } else if (newTarget.isDropTarget(mDragItemMailboxId)) { 647 if (DEBUG_DRAG_DROP) { 648 Log.d(TAG, "=== Target mailbox; id: " + newTarget.mMailboxId); 649 } 650 newTarget.setBackgroundDrawable(sDropActiveDrawable); 651 } else { 652 if (DEBUG_DRAG_DROP) { 653 Log.d(TAG, "=== Non-droppable mailbox; id: " + newTarget.mMailboxId); 654 } 655 newTarget.setDropTargetBackground(true, mDragItemMailboxId); 656 targetId = NO_DROP_TARGET; 657 } 658 // Save away our current position and view 659 mDropTargetId = targetId; 660 mDropTargetView = newTarget; 661 startDragTimer(newTarget); 662 } 663 664 // This is a quick-and-dirty implementation of drag-under-scroll; something like this 665 // should eventually find its way into the framework 666 int scrollDiff = rawTouchY - (mListView.getHeight() - SCROLL_ZONE_SIZE); 667 boolean scrollDown = (scrollDiff > 0); 668 boolean scrollUp = (SCROLL_ZONE_SIZE > rawTouchY); 669 if (!mTargetScrolling && scrollDown) { 670 int itemsToScroll = mListView.getCount() - mListView.getLastVisiblePosition(); 671 int pixelsToScroll = (itemsToScroll + 1) * mDragItemHeight; 672 mListView.smoothScrollBy(pixelsToScroll, pixelsToScroll * SCROLL_SPEED); 673 if (DEBUG_DRAG_DROP) { 674 Log.d(TAG, "=== Start scrolling list down"); 675 } 676 mTargetScrolling = true; 677 } else if (!mTargetScrolling && scrollUp) { 678 int pixelsToScroll = (mListView.getFirstVisiblePosition() + 1) * mDragItemHeight; 679 mListView.smoothScrollBy(-pixelsToScroll, pixelsToScroll * SCROLL_SPEED); 680 if (DEBUG_DRAG_DROP) { 681 Log.d(TAG, "=== Start scrolling list up"); 682 } 683 mTargetScrolling = true; 684 } else if (!scrollUp && !scrollDown) { 685 stopScrolling(); 686 } 687 } 688 689 /** 690 * Indicate that scrolling has stopped 691 */ 692 private void stopScrolling() { 693 if (mTargetScrolling) { 694 mTargetScrolling = false; 695 if (DEBUG_DRAG_DROP) { 696 Log.d(TAG, "=== Stop scrolling list"); 697 } 698 // Stop the scrolling 699 mListView.smoothScrollBy(0, 0); 700 } 701 } 702 703 private void onDragEnded() { 704 stopDragTimer(); 705 if (mDragInProgress) { 706 mDragInProgress = false; 707 // Reenable updates to the view and redraw (in case it changed) 708 MailboxesAdapter.enableUpdates(true); 709 mListAdapter.notifyDataSetChanged(); 710 // Stop highlighting targets 711 updateChildViews(); 712 // Stop any scrolling that was going on 713 stopScrolling(); 714 } 715 } 716 717 private boolean onDragStarted(DragEvent event) { 718 // We handle dropping of items with our email mime type 719 // If the mime type has a mailbox id appended, that is the mailbox of the item 720 // being draged 721 ClipDescription description = event.getClipDescription(); 722 int mimeTypeCount = description.getMimeTypeCount(); 723 for (int i = 0; i < mimeTypeCount; i++) { 724 String mimeType = description.getMimeType(i); 725 if (mimeType.startsWith(EmailProvider.EMAIL_MESSAGE_MIME_TYPE)) { 726 if (DEBUG_DRAG_DROP) { 727 Log.d(TAG, "=== Drag started"); 728 } 729 mDragItemMailboxId = -1; 730 // See if we find a mailbox id here 731 int dash = mimeType.lastIndexOf('-'); 732 if (dash > 0) { 733 try { 734 mDragItemMailboxId = Long.parseLong(mimeType.substring(dash + 1)); 735 } catch (NumberFormatException e) { 736 // Ignore; we just won't know the mailbox 737 } 738 } 739 mDragInProgress = true; 740 // Stop the list from updating 741 MailboxesAdapter.enableUpdates(false); 742 // Update the backgrounds of our child views to highlight drop targets 743 updateChildViews(); 744 return true; 745 } 746 } 747 return false; 748 } 749 750 /** 751 * Perform a "drop" action. If the user is not on top of a valid drop target, no action 752 * is performed. 753 * @return {@code true} if the drop action was performed. Otherwise {@code false}. 754 */ 755 private boolean onDrop(DragEvent event) { 756 stopDragTimer(); 757 stopScrolling(); 758 // If we're not on a target, we're done 759 if (mDropTargetId == NO_DROP_TARGET) { 760 return false; 761 } 762 final Controller controller = Controller.getInstance(mActivity); 763 ClipData clipData = event.getClipData(); 764 int count = clipData.getItemCount(); 765 if (DEBUG_DRAG_DROP) { 766 Log.d(TAG, "=== Dropping " + count + " items."); 767 } 768 // Extract the messageId's to move from the ClipData (set up in MessageListItem) 769 final long[] messageIds = new long[count]; 770 for (int i = 0; i < count; i++) { 771 Uri uri = clipData.getItemAt(i).getUri(); 772 String msgNum = uri.getPathSegments().get(1); 773 long id = Long.parseLong(msgNum); 774 messageIds[i] = id; 775 } 776 final MailboxListItem targetItem = mDropTargetView; 777 // Call either deleteMessage or moveMessage, depending on the target 778 EmailAsyncTask.runAsyncSerial(new Runnable() { 779 @Override 780 public void run() { 781 if (targetItem.mMailboxType == Mailbox.TYPE_TRASH) { 782 for (long messageId: messageIds) { 783 // TODO Get this off UI thread (put in clip) 784 Message msg = Message.restoreMessageWithId(mActivity, messageId); 785 if (msg != null) { 786 controller.deleteMessage(messageId, msg.mAccountKey); 787 } 788 } 789 } else { 790 controller.moveMessages(messageIds, targetItem.mMailboxId); 791 } 792 } 793 }); 794 return true; 795 } 796 797 @Override 798 public boolean onDrag(View view, DragEvent event) { 799 boolean result = false; 800 switch (event.getAction()) { 801 case DragEvent.ACTION_DRAG_STARTED: 802 result = onDragStarted(event); 803 break; 804 case DragEvent.ACTION_DRAG_ENTERED: 805 // The drag has entered the ListView window 806 if (DEBUG_DRAG_DROP) { 807 Log.d(TAG, "=== Drag entered; targetId: " + mDropTargetId); 808 } 809 break; 810 case DragEvent.ACTION_DRAG_EXITED: 811 // The drag has left the building 812 if (DEBUG_DRAG_DROP) { 813 Log.d(TAG, "=== Drag exited; targetId: " + mDropTargetId); 814 } 815 onDragExited(); 816 break; 817 case DragEvent.ACTION_DRAG_ENDED: 818 // The drag is over 819 if (DEBUG_DRAG_DROP) { 820 Log.d(TAG, "=== Drag ended"); 821 } 822 onDragEnded(); 823 break; 824 case DragEvent.ACTION_DRAG_LOCATION: 825 // We're moving around within our window; handle scroll, if necessary 826 onDragLocation(event); 827 break; 828 case DragEvent.ACTION_DROP: 829 // The drag item was dropped 830 if (DEBUG_DRAG_DROP) { 831 Log.d(TAG, "=== Drop"); 832 } 833 result = onDrop(event); 834 break; 835 default: 836 break; 837 } 838 return result; 839 } 840} 841