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