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