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