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