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