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