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