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 17 package com.android.email.activity; 18 19 import android.app.Activity; 20 import android.app.ListFragment; 21 import android.app.LoaderManager; 22 import android.app.LoaderManager.LoaderCallbacks; 23 import android.content.ClipData; 24 import android.content.ClipDescription; 25 import android.content.Context; 26 import android.content.Loader; 27 import android.database.Cursor; 28 import android.graphics.Rect; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.os.Parcelable; 32 import android.util.Log; 33 import android.view.DragEvent; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.View.OnDragListener; 37 import android.view.ViewGroup; 38 import android.widget.AdapterView; 39 import android.widget.AdapterView.OnItemClickListener; 40 import android.widget.ListView; 41 42 import com.android.email.Controller; 43 import com.android.email.Email; 44 import com.android.email.R; 45 import com.android.email.RefreshManager; 46 import com.android.email.provider.EmailProvider; 47 import com.android.emailcommon.Logging; 48 import com.android.emailcommon.provider.Account; 49 import com.android.emailcommon.provider.Mailbox; 50 import com.android.emailcommon.utility.EmailAsyncTask; 51 import com.android.emailcommon.utility.Utility; 52 import com.google.common.annotations.VisibleForTesting; 53 54 import java.util.Timer; 55 56 /** 57 * This fragment presents a list of mailboxes for a given account or the combined mailboxes. 58 * 59 * This fragment has several parameters that determine the current view. 60 * 61 * <pre> 62 * Parameters: 63 * - Account ID. 64 * - Set via {@link #newInstance}. 65 * - Can be obtained with {@link #getAccountId()}. 66 * - Will not change throughout fragment lifecycle. 67 * - Either an actual account ID, or {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 68 * 69 * - "Highlight enabled?" flag 70 * - Set via {@link #newInstance}. 71 * - Can be obtained with {@link #getEnableHighlight()}. 72 * - Will not change throughout fragment lifecycle. 73 * - If {@code true}, we highlight the "selected" mailbox (used only on 2-pane). 74 * - Note even if it's {@code true}, there may be no highlighted mailbox. 75 * (This usually happens on 2-pane before the UI controller finds the Inbox to highlight.) 76 * 77 * - "Parent" mailbox ID 78 * - Stored in {@link #mParentMailboxId} 79 * - Changes as the user navigates through nested mailboxes. 80 * - Initialized using the {@code mailboxId} parameter for {@link #newInstance} 81 * in {@link #setInitialParentAndHighlight()}. 82 * 83 * - "Highlighted" mailbox 84 * - Only used when highlighting is enabled. (Otherwise always {@link Mailbox#NO_MAILBOX}.) 85 * i.e. used only on two-pane. 86 * - Stored in {@link #mHighlightedMailboxId} 87 * - Initialized using the {@code mailboxId} parameter for {@link #newInstance} 88 * in {@link #setInitialParentAndHighlight()}. 89 * 90 * - Can be changed any time, using {@link #setHighlightedMailbox(long)}. 91 * 92 * - If set, it's considered "selected", and we highlight the list item. 93 * 94 * - (It should always be the ID of the list item selected in the list view, but we store it in 95 * a member for efficiency.) 96 * 97 * - Sometimes, we need to set the highlighted mailbox while we're still loading data. 98 * In this case, we can't update {@link #mHighlightedMailboxId} right away, but need to do so 99 * in when the next data set arrives, in 100 * {@link MailboxListFragment.MailboxListLoaderCallbacks#onLoadFinished}. For this, we use 101 * we store the mailbox ID in {@link #mNextHighlightedMailboxId} and update 102 * {@link #mHighlightedMailboxId} in onLoadFinished. 103 * 104 * 105 * The "selected" is defined using the "parent" and "highlighted" mailboxes. 106 * - "Selected" mailbox (also sometimes called "current".) 107 * - This is what the user thinks it's now selected. 108 * 109 * - Can be obtained with {@link #getSelectedMailboxId()} 110 * - If the "highlighted" mailbox exists, it's the "selected." Otherwise, the "parent" 111 * is considered "selected." 112 * - This is what is passed to {@link Callback#onMailboxSelected}. 113 * </pre> 114 * 115 * 116 * This fragment shows the content in one of the three following views, depending on the 117 * parameters above. 118 * 119 * <pre> 120 * 1. Combined view 121 * - Used if the account ID == {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 122 * - Parent mailbox is always {@link Mailbox#NO_MAILBOX}. 123 * - List contains: 124 * - combined mailboxes 125 * - all accounts 126 * 127 * 2. Root view for an account 128 * - Used if the account ID != {@link Account#ACCOUNT_ID_COMBINED_VIEW} and 129 * Parent mailbox == {@link Mailbox#NO_MAILBOX} 130 * - List contains 131 * - all the top level mailboxes for the selected account. 132 * 133 * 3. Root view for a mailbox. (nested view) 134 * - Used if the account ID != {@link Account#ACCOUNT_ID_COMBINED_VIEW} and 135 * Parent mailbox != {@link Mailbox#NO_MAILBOX} 136 * - List contains: 137 * - parent mailbox (determined by "parent" mailbox ID) 138 * - all child mailboxes of the parent mailbox. 139 * </pre> 140 * 141 * 142 * Note that when a fragment is put in the back stack, it'll lose the content view but the fragment 143 * itself is not destroyed. If you call {@link #getListView()} in this state it'll throw 144 * an {@link IllegalStateException}. So, 145 * - If code is supposed to be executed only when the fragment has the content view, use 146 * {@link #getListView()} directly to make sure it doesn't accidentally get executed when there's 147 * no views. 148 * - Otherwise, make sure to check if the fragment has views with {@link #isViewCreated()} 149 * before touching any views. 150 */ 151 public class MailboxListFragment extends ListFragment implements OnItemClickListener, 152 OnDragListener { 153 private static final String TAG = "MailboxListFragment"; 154 155 private static final String BUNDLE_KEY_PARENT_MAILBOX_ID 156 = "MailboxListFragment.state.parent_mailbox_id"; 157 private static final String BUNDLE_KEY_HIGHLIGHTED_MAILBOX_ID 158 = "MailboxListFragment.state.selected_mailbox_id"; 159 private static final String BUNDLE_LIST_STATE = "MailboxListFragment.state.listState"; 160 private static final boolean DEBUG_DRAG_DROP = false; // MUST NOT SUBMIT SET TO TRUE 161 162 /** No drop target is available where the user is currently hovering over */ 163 private static final int NO_DROP_TARGET = -1; 164 // Total height of the top and bottom scroll zones, in pixels 165 private static final int SCROLL_ZONE_SIZE = 64; 166 // The amount of time to scroll by one pixel, in ms 167 private static final int SCROLL_SPEED = 4; 168 169 /** Arbitrary number for use with the loader manager */ 170 private static final int MAILBOX_LOADER_ID = 1; 171 172 /** Argument name(s) */ 173 private static final String ARG_ACCOUNT_ID = "accountId"; 174 private static final String ARG_ENABLE_HIGHLIGHT = "enablehighlight"; 175 private static final String ARG_INITIAL_CURRENT_MAILBOX_ID = "initialParentMailboxId"; 176 177 private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); 178 179 /** Rectangle used for hit testing children */ 180 private static final Rect sTouchFrame = new Rect(); 181 182 private RefreshManager mRefreshManager; 183 184 // UI Support 185 private Activity mActivity; 186 private MailboxFragmentAdapter mListAdapter; 187 private Callback mCallback = EmptyCallback.INSTANCE; 188 189 // See the class javadoc 190 private long mParentMailboxId; 191 private long mHighlightedMailboxId; 192 193 /** 194 * Becomes {@code true} once we determine which mailbox to use as the parent. 195 */ 196 private boolean mParentDetermined; 197 198 /** 199 * ID of the mailbox that should be highlighted when the next cursor is loaded. 200 */ 201 private long mNextHighlightedMailboxId = Mailbox.NO_MAILBOX; 202 203 // True if a drag is currently in progress 204 private boolean mDragInProgress; 205 /** Mailbox ID of the item being dragged. Used to determine valid drop targets. */ 206 private long mDragItemMailboxId = -1; 207 /** A unique identifier for the drop target. May be {@link #NO_DROP_TARGET}. */ 208 private int mDropTargetId = NO_DROP_TARGET; 209 // The mailbox list item view that the user's finger is hovering over 210 private MailboxListItem mDropTargetView; 211 // Lazily instantiated height of a mailbox list item (-1 is a sentinel for 'not initialized') 212 private int mDragItemHeight = -1; 213 /** {@code true} if we are currently scrolling under the drag item */ 214 private boolean mTargetScrolling; 215 216 private Parcelable mSavedListState; 217 218 private final MailboxFragmentAdapter.Callback mMailboxesAdapterCallback = 219 new MailboxFragmentAdapter.Callback() { 220 @Override 221 public void onBind(MailboxListItem listItem) { 222 listItem.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); 223 } 224 }; 225 226 /** 227 * Callback interface that owning activities must implement 228 */ 229 public interface Callback { 230 /** 231 * Called when any mailbox (even a combined mailbox) is selected. 232 * 233 * @param accountId 234 * The ID of the owner account of the selected mailbox. 235 * Or {@link Account#ACCOUNT_ID_COMBINED_VIEW} if it's a combined mailbox. 236 * @param mailboxId 237 * The ID of the selected mailbox. This may be real mailbox ID [e.g. a number > 0], 238 * or a combined mailbox ID [e.g. {@link Mailbox#QUERY_ALL_INBOXES}]. 239 * @param nestedNavigation {@code true} if the event is caused by nested mailbox navigation, 240 * that is, going up or drilling-in to a child mailbox. 241 */ 242 public void onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation); 243 244 /** Called when an account is selected on the combined view. */ 245 public void onAccountSelected(long accountId); 246 247 /** 248 * Called when the parent mailbox is changing. 249 */ 250 public void onParentMailboxChanged(); 251 } 252 253 private static class EmptyCallback implements Callback { 254 public static final Callback INSTANCE = new EmptyCallback(); 255 @Override public void onMailboxSelected(long accountId, long mailboxId, 256 boolean nestedNavigation) { } 257 @Override public void onAccountSelected(long accountId) { } 258 @Override 259 public void onParentMailboxChanged() { } 260 } 261 262 /** 263 * Returns the index of the view located at the specified coordinates in the given list. 264 * If the coordinates are outside of the list, {@code NO_DROP_TARGET} is returned. 265 */ 266 private static int pointToIndex(ListView list, int x, int y) { 267 final int count = list.getChildCount(); 268 for (int i = count - 1; i >= 0; i--) { 269 final View child = list.getChildAt(i); 270 if (child.getVisibility() == View.VISIBLE) { 271 child.getHitRect(sTouchFrame); 272 if (sTouchFrame.contains(x, y)) { 273 return i; 274 } 275 } 276 } 277 return NO_DROP_TARGET; 278 } 279 280 /** 281 * Create a new instance with initialization parameters. 282 * 283 * This fragment should be created only with this method. (Arguments should always be set.) 284 * 285 * @param accountId The ID of the account we want to view 286 * @param initialCurrentMailboxId ID of the mailbox of interest. 287 * Pass {@link Mailbox#NO_MAILBOX} to show top-level mailboxes. 288 * @param enableHighlight {@code true} if highlighting is enabled on the current screen 289 * configuration. (We don't highlight mailboxes on one-pane.) 290 */ 291 public static MailboxListFragment newInstance(long accountId, long initialCurrentMailboxId, 292 boolean enableHighlight) { 293 final MailboxListFragment instance = new MailboxListFragment(); 294 final Bundle args = new Bundle(); 295 args.putLong(ARG_ACCOUNT_ID, accountId); 296 args.putLong(ARG_INITIAL_CURRENT_MAILBOX_ID, initialCurrentMailboxId); 297 args.putBoolean(ARG_ENABLE_HIGHLIGHT, enableHighlight); 298 instance.setArguments(args); 299 return instance; 300 } 301 302 /** 303 * The account ID the mailbox is associated with. Do not use directly; instead, use 304 * {@link #getAccountId()}. 305 * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language 306 * constructs, this <em>must</em> be considered immutable. 307 */ 308 private Long mImmutableAccountId; 309 310 /** 311 * {@code initialCurrentMailboxId} passed to {@link #newInstance}. 312 * Do not use directly; instead, use {@link #getInitialCurrentMailboxId()}. 313 * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language 314 * constructs, this <em>must</em> be considered immutable. 315 */ 316 private long mImmutableInitialCurrentMailboxId; 317 318 /** 319 * {@code enableHighlight} passed to {@link #newInstance}. 320 * Do not use directly; instead, use {@link #getEnableHighlight()}. 321 * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language 322 * constructs, this <em>must</em> be considered immutable. 323 */ 324 private boolean mImmutableEnableHighlight; 325 326 private void initializeArgCache() { 327 if (mImmutableAccountId != null) return; 328 mImmutableAccountId = getArguments().getLong(ARG_ACCOUNT_ID); 329 mImmutableInitialCurrentMailboxId = getArguments().getLong(ARG_INITIAL_CURRENT_MAILBOX_ID); 330 mImmutableEnableHighlight = getArguments().getBoolean(ARG_ENABLE_HIGHLIGHT); 331 } 332 333 /** 334 * @return {@code accountId} passed to {@link #newInstance}. Safe to call even before onCreate. 335 */ 336 public long getAccountId() { 337 initializeArgCache(); 338 return mImmutableAccountId; 339 } 340 341 /** 342 * @return {@code initialCurrentMailboxId} passed to {@link #newInstance}. 343 * Safe to call even before onCreate. 344 */ 345 public long getInitialCurrentMailboxId() { 346 initializeArgCache(); 347 return mImmutableInitialCurrentMailboxId; 348 } 349 350 /** 351 * @return {@code enableHighlight} passed to {@link #newInstance}. 352 * Safe to call even before onCreate. 353 */ 354 public boolean getEnableHighlight() { 355 initializeArgCache(); 356 return mImmutableEnableHighlight; 357 } 358 359 @Override 360 public void onAttach(Activity activity) { 361 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 362 Log.d(Logging.LOG_TAG, this + " onAttach"); 363 } 364 super.onAttach(activity); 365 } 366 367 /** 368 * Called to do initial creation of a fragment. This is called after 369 * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}. 370 */ 371 @Override 372 public void onCreate(Bundle savedInstanceState) { 373 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 374 Log.d(Logging.LOG_TAG, this + " onCreate"); 375 } 376 super.onCreate(savedInstanceState); 377 378 mActivity = getActivity(); 379 mRefreshManager = RefreshManager.getInstance(mActivity); 380 mListAdapter = new MailboxFragmentAdapter(mActivity, mMailboxesAdapterCallback); 381 setListAdapter(mListAdapter); // It's safe to do even before the list view is created. 382 383 if (savedInstanceState == null) { 384 setInitialParentAndHighlight(); 385 } else { 386 restoreInstanceState(savedInstanceState); 387 } 388 } 389 390 /** 391 * Set {@link #mParentMailboxId} and {@link #mHighlightedMailboxId} from the fragment arguments. 392 */ 393 private void setInitialParentAndHighlight() { 394 final long initialMailboxId = getInitialCurrentMailboxId(); 395 if (getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW) { 396 // For the combined view, always show the top-level, but highlight the "current". 397 mParentMailboxId = Mailbox.NO_MAILBOX; 398 } else { 399 // Inbox needs special care. 400 // Note we can't get the mailbox type on the UI thread but this method *can* be used... 401 final long inboxId = Mailbox.findMailboxOfType(getActivity(), getAccountId(), 402 Mailbox.TYPE_INBOX); 403 if (initialMailboxId == inboxId) { 404 // If Inbox is set as the initial current, we show the top level mailboxes 405 // with inbox highlighted. 406 mParentMailboxId = Mailbox.NO_MAILBOX; 407 } else { 408 // Otherwise, try using the "current" as the "parent" (and also highlight it). 409 // If it has no children, we go up in onLoadFinished(). 410 mParentMailboxId = initialMailboxId; 411 } 412 } 413 // Highlight the mailbox of interest 414 if (getEnableHighlight()) { 415 mHighlightedMailboxId = initialMailboxId; 416 } 417 } 418 419 @Override 420 public View onCreateView( 421 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 422 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 423 Log.d(Logging.LOG_TAG, this + " onCreateView"); 424 } 425 return inflater.inflate(R.layout.mailbox_list_fragment, container, false); 426 } 427 428 /** 429 * @return true if the content view is created and not destroyed yet. (i.e. between 430 * {@link #onCreateView} and {@link #onDestroyView}. 431 */ 432 private boolean isViewCreated() { 433 return getView() != null; 434 } 435 436 @Override 437 public void onActivityCreated(Bundle savedInstanceState) { 438 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 439 Log.d(Logging.LOG_TAG, this + " onActivityCreated"); 440 } 441 super.onActivityCreated(savedInstanceState); 442 443 // Note we can't do this in onCreateView. 444 // getListView() is only usable after onCreateView(). 445 final ListView lv = getListView(); 446 lv.setOnItemClickListener(this); 447 lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 448 lv.setOnDragListener(this); 449 450 startLoading(mParentMailboxId, mHighlightedMailboxId); 451 452 UiUtilities.installFragment(this); 453 } 454 455 public void setCallback(Callback callback) { 456 mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback; 457 } 458 459 /** 460 * Called when the Fragment is visible to the user. 461 */ 462 @Override 463 public void onStart() { 464 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 465 Log.d(Logging.LOG_TAG, this + " onStart"); 466 } 467 super.onStart(); 468 } 469 470 /** 471 * Called when the fragment is visible to the user and actively running. 472 */ 473 @Override 474 public void onResume() { 475 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 476 Log.d(Logging.LOG_TAG, this + " onResume"); 477 } 478 super.onResume(); 479 480 // Fetch the latest mailbox list from the server here if stale so that the user always 481 // sees the (reasonably) up-to-date mailbox list, without pressing "refresh". 482 final long accountId = getAccountId(); 483 if (mRefreshManager.isMailboxListStale(accountId)) { 484 mRefreshManager.refreshMailboxList(accountId); 485 } 486 } 487 488 @Override 489 public void onPause() { 490 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 491 Log.d(Logging.LOG_TAG, this + " onPause"); 492 } 493 mSavedListState = getListView().onSaveInstanceState(); 494 super.onPause(); 495 } 496 497 /** 498 * Called when the Fragment is no longer started. 499 */ 500 @Override 501 public void onStop() { 502 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 503 Log.d(Logging.LOG_TAG, this + " onStop"); 504 } 505 super.onStop(); 506 } 507 508 @Override 509 public void onDestroyView() { 510 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 511 Log.d(Logging.LOG_TAG, this + " onDestroyView"); 512 } 513 UiUtilities.uninstallFragment(this); 514 super.onDestroyView(); 515 } 516 517 /** 518 * Called when the fragment is no longer in use. 519 */ 520 @Override 521 public void onDestroy() { 522 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 523 Log.d(Logging.LOG_TAG, this + " onDestroy"); 524 } 525 mTaskTracker.cancellAllInterrupt(); 526 super.onDestroy(); 527 } 528 529 @Override 530 public void onDetach() { 531 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 532 Log.d(Logging.LOG_TAG, this + " onDetach"); 533 } 534 super.onDetach(); 535 } 536 537 @Override 538 public void onSaveInstanceState(Bundle outState) { 539 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 540 Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); 541 } 542 super.onSaveInstanceState(outState); 543 outState.putLong(BUNDLE_KEY_PARENT_MAILBOX_ID, mParentMailboxId); 544 outState.putLong(BUNDLE_KEY_HIGHLIGHTED_MAILBOX_ID, mHighlightedMailboxId); 545 if (isViewCreated()) { 546 outState.putParcelable(BUNDLE_LIST_STATE, getListView().onSaveInstanceState()); 547 } 548 } 549 550 private void restoreInstanceState(Bundle savedInstanceState) { 551 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 552 Log.d(Logging.LOG_TAG, this + " restoreInstanceState"); 553 } 554 mParentMailboxId = savedInstanceState.getLong(BUNDLE_KEY_PARENT_MAILBOX_ID); 555 mNextHighlightedMailboxId = savedInstanceState.getLong(BUNDLE_KEY_HIGHLIGHTED_MAILBOX_ID); 556 mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE); 557 } 558 559 /** 560 * @return "Selected" mailbox ID. 561 */ 562 public long getSelectedMailboxId() { 563 return (mHighlightedMailboxId != Mailbox.NO_MAILBOX) ? mHighlightedMailboxId 564 : mParentMailboxId; 565 } 566 567 /** 568 * @return {@code true} if top-level mailboxes are shown. {@code false} otherwise. 569 */ 570 private boolean isRoot() { 571 return mParentMailboxId == Mailbox.NO_MAILBOX; 572 } 573 574 /** 575 * Navigate one level up in the mailbox hierarchy. Does nothing if at the root account view. 576 */ 577 public boolean navigateUp() { 578 if (isRoot()) { 579 return false; 580 } 581 FindParentMailboxTask.ResultCallback callback = new FindParentMailboxTask.ResultCallback() { 582 @Override public void onResult(long nextParentMailboxId, 583 long nextHighlightedMailboxId, long nextSelectedMailboxId) { 584 585 startLoading(nextParentMailboxId, nextHighlightedMailboxId); 586 } 587 }; 588 new FindParentMailboxTask( 589 getActivity().getApplicationContext(), mTaskTracker, getAccountId(), 590 getEnableHighlight(), mParentMailboxId, mHighlightedMailboxId, callback 591 ).cancelPreviousAndExecuteParallel((Void[]) null); 592 return true; 593 } 594 595 /** 596 * @return {@code true} if the fragment is showing nested mailboxes and we can go one level up. 597 * {@code false} otherwise, meaning we're showing the top level mailboxes *OR* 598 * we're still loading initial data and we can't determine if we're going to show 599 * top-level or not. 600 */ 601 public boolean canNavigateUp() { 602 if (!mParentDetermined) { 603 return false; // We can't determine yet... 604 } 605 return !isRoot(); 606 } 607 608 /** 609 * A task to determine what parent mailbox ID/highlighted mailbox ID to use for the "UP" 610 * navigation, given the current parent mailbox ID, the highlighted mailbox ID, and {@link 611 * #mEnableHighlight}. 612 */ 613 @VisibleForTesting 614 static class FindParentMailboxTask extends EmailAsyncTask<Void, Void, Long[]> { 615 public interface ResultCallback { 616 /** 617 * Callback to get the result. 618 * 619 * @param nextParentMailboxId ID of the mailbox to use 620 * @param nextHighlightedMailboxId ID of the mailbox to highlight 621 * @param nextSelectedMailboxId ID of the mailbox to notify with 622 * {@link Callback#onMailboxSelected}. 623 */ 624 public void onResult(long nextParentMailboxId, long nextHighlightedMailboxId, 625 long nextSelectedMailboxId); 626 } 627 628 private final Context mContext; 629 private final long mAccountId; 630 private final boolean mEnableHighlight; 631 private final long mParentMailboxId; 632 private final long mHighlightedMailboxId; 633 private final ResultCallback mCallback; 634 635 public FindParentMailboxTask(Context context, EmailAsyncTask.Tracker taskTracker, 636 long accountId, boolean enableHighlight, long parentMailboxId, 637 long highlightedMailboxId, ResultCallback callback) { 638 super(taskTracker); 639 mContext = context; 640 mAccountId = accountId; 641 mEnableHighlight = enableHighlight; 642 mParentMailboxId = parentMailboxId; 643 mHighlightedMailboxId = highlightedMailboxId; 644 mCallback = callback; 645 } 646 647 @Override 648 protected Long[] doInBackground(Void... params) { 649 Mailbox parentMailbox = Mailbox.restoreMailboxWithId(mContext, mParentMailboxId); 650 final long nextParentId = (parentMailbox == null) ? Mailbox.NO_MAILBOX 651 : parentMailbox.mParentKey; 652 final long nextHighlightedId; 653 final long nextSelectedId; 654 if (mEnableHighlight) { 655 // If the "parent" is highlighted before the transition, it should still be 656 // highlighted after the upper level view. 657 if (mParentMailboxId == mHighlightedMailboxId) { 658 nextHighlightedId = mParentMailboxId; 659 } else { 660 // Otherwise, the next parent will be highlighted, unless we're going up to 661 // the root, in which case Inbox should be highlighted. 662 if (nextParentId == Mailbox.NO_MAILBOX) { 663 nextHighlightedId = Mailbox.findMailboxOfType(mContext, mAccountId, 664 Mailbox.TYPE_INBOX); 665 } else { 666 nextHighlightedId = nextParentId; 667 } 668 } 669 670 // Highlighted one will be "selected". 671 nextSelectedId = nextHighlightedId; 672 673 } else { // !mEnableHighlight 674 nextHighlightedId = Mailbox.NO_MAILBOX; 675 676 // Parent will be selected. 677 nextSelectedId = nextParentId; 678 } 679 return new Long[]{nextParentId, nextHighlightedId, nextSelectedId}; 680 } 681 682 @Override 683 protected void onSuccess(Long[] result) { 684 mCallback.onResult(result[0], result[1], result[2]); 685 } 686 } 687 688 /** 689 * Starts the loader. 690 * 691 * @param parentMailboxId Mailbox ID to be used as the "parent" mailbox 692 * @param highlightedMailboxId Mailbox ID that should be highlighted when the data is loaded. 693 */ 694 private void startLoading(long parentMailboxId, long highlightedMailboxId) { 695 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 696 Log.d(Logging.LOG_TAG, this + " startLoading parent=" + parentMailboxId 697 + " highlighted=" + highlightedMailboxId); 698 } 699 final LoaderManager lm = getLoaderManager(); 700 boolean parentMailboxChanging = false; 701 702 // Parent mailbox changing -- destroy the current loader to force reload. 703 if (mParentMailboxId != parentMailboxId) { 704 lm.destroyLoader(MAILBOX_LOADER_ID); 705 setListShown(false); 706 parentMailboxChanging = true; 707 } 708 mParentMailboxId = parentMailboxId; 709 if (getEnableHighlight()) { 710 mNextHighlightedMailboxId = highlightedMailboxId; 711 } 712 713 lm.initLoader(MAILBOX_LOADER_ID, null, new MailboxListLoaderCallbacks()); 714 715 if (parentMailboxChanging) { 716 mCallback.onParentMailboxChanged(); 717 } 718 } 719 720 /** 721 * Highlight the given mailbox. 722 * 723 * If data is already loaded, it just sets {@link #mHighlightedMailboxId} and highlight the 724 * corresponding list item. (And if the corresponding list item is not found, 725 * {@link #mHighlightedMailboxId} is set to {@link Mailbox#NO_MAILBOX}) 726 * 727 * If we're still loading data, it sets {@link #mNextHighlightedMailboxId} instead, and then 728 * it'll be set to {@link #mHighlightedMailboxId} in 729 * {@link MailboxListLoaderCallbacks#onLoadFinished}. 730 * 731 * @param mailboxId The ID of the mailbox to highlight. 732 */ 733 public void setHighlightedMailbox(long mailboxId) { 734 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 735 Log.d(Logging.LOG_TAG, this + " setHighlightedMailbox mailbox=" + mailboxId); 736 } 737 if (!getEnableHighlight()) { 738 return; 739 } 740 if (mHighlightedMailboxId == mailboxId) { 741 return; // already highlighted. 742 } 743 if (mListAdapter.getCursor() == null) { 744 // List not loaded yet. Just remember the ID here and let onLoadFinished() update 745 // mHighlightedMailboxId. 746 mNextHighlightedMailboxId = mailboxId; 747 return; 748 } 749 mHighlightedMailboxId = mailboxId; 750 updateHighlightedMailbox(true); 751 } 752 753 // TODO This class probably should be made static. There are many calls into the enclosing 754 // class and we need to be cautious about what we call while in these callbacks 755 private class MailboxListLoaderCallbacks implements LoaderCallbacks<Cursor> { 756 private boolean mIsFirstLoad; 757 758 @Override 759 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 760 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 761 Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onCreateLoader"); 762 } 763 mIsFirstLoad = true; 764 if (getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW) { 765 return MailboxFragmentAdapter.createCombinedViewLoader(getActivity()); 766 } else { 767 return MailboxFragmentAdapter.createMailboxesLoader(getActivity(), getAccountId(), 768 mParentMailboxId); 769 } 770 } 771 772 @Override 773 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 774 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 775 Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onLoadFinished count=" 776 + cursor.getCount()); 777 } 778 // Note in onLoadFinished we can assume the view is created. 779 // The loader manager doesn't deliver results when a fragment is stopped. 780 781 // If we're showing a nested mailboxes, and the current parent mailbox has no children, 782 // go up. 783 if (getAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW) { 784 MailboxFragmentAdapter.CursorWithExtras c = 785 (MailboxFragmentAdapter.CursorWithExtras) cursor; 786 if ((c.mChildCount == 0) && !isRoot()) { 787 // Always swap out the cursor so we don't hold a reference to a stale one. 788 mListAdapter.swapCursor(cursor); 789 navigateUp(); 790 return; 791 } 792 } 793 794 // Save list view state (primarily scroll position) 795 final ListView lv = getListView(); 796 final Parcelable listState; 797 if (mSavedListState != null) { 798 listState = mSavedListState; 799 mSavedListState = null; 800 } else { 801 listState = lv.onSaveInstanceState(); 802 } 803 804 if (cursor.getCount() == 0) { 805 // There's no row -- call setListShown(false) to make ListFragment show progress 806 // icon. 807 mListAdapter.swapCursor(null); 808 setListShown(false); 809 810 } else { 811 mParentDetermined = true; // Okay now we're sure which mailbox is the parent. 812 813 mListAdapter.swapCursor(cursor); 814 setListShown(true); 815 816 // Restore the list state, so scroll position is restored - this has to happen 817 // prior to setting the checked/highlighted mailbox below. 818 lv.onRestoreInstanceState(listState); 819 820 // Update the highlighted mailbox 821 if (mNextHighlightedMailboxId != Mailbox.NO_MAILBOX) { 822 setHighlightedMailbox(mNextHighlightedMailboxId); 823 mNextHighlightedMailboxId = Mailbox.NO_MAILBOX; 824 } 825 826 // We want to make visible the selection only for the first load. 827 // Re-load caused by content changed events shouldn't scroll the list. 828 if (!updateHighlightedMailbox(mIsFirstLoad)) { 829 // This may happen if the mailbox to be selected is not actually in the list 830 // that was loaded. Let the user just pick one manually if needed. 831 return; 832 } 833 } 834 835 // List has been reloaded; clear any drop target information 836 mDropTargetId = NO_DROP_TARGET; 837 mDropTargetView = null; 838 839 mIsFirstLoad = false; 840 } 841 842 @Override 843 public void onLoaderReset(Loader<Cursor> loader) { 844 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 845 Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onLoaderReset"); 846 } 847 mListAdapter.swapCursor(null); 848 } 849 } 850 851 /** 852 * {@inheritDoc} 853 * <p> 854 * @param doNotUse <em>IMPORTANT</em>: Do not use this parameter. The ID in the list widget 855 * must be a positive value. However, we rely on negative IDs for special mailboxes. Instead, 856 * we use the ID returned by {@link MailboxFragmentAdapter#getId(int)}. 857 */ 858 @Override 859 public void onItemClick(AdapterView<?> parent, View view, int position, long doNotUse) { 860 final long id = mListAdapter.getId(position); 861 if (mListAdapter.isAccountRow(position)) { 862 mCallback.onAccountSelected(id); 863 } else if (mListAdapter.isMailboxRow(position)) { 864 // Save account-id. (Need to do this before startLoading() below, which will destroy 865 // the current loader and make the mListAdapter lose the cursor. 866 // Note, don't just use getAccountId(). A mailbox may tied to a different account ID 867 // from getAccountId(). (Currently "Starred" does so.) 868 long accountId = mListAdapter.getAccountId(position); 869 boolean nestedNavigation = false; 870 if (((MailboxListItem) view).isNavigable() && (id != mParentMailboxId)) { 871 // Drill-in. Selected one will be the next parent, and it'll also be highlighted. 872 startLoading(id, id); 873 nestedNavigation = true; 874 } 875 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 876 // Virtual mailboxes, such as "Starred", will have a "combined view" ID. However, 877 // we really want to relay the current active account, so that 878 // things like per-account starred mailboxes work as expected. 879 accountId = getAccountId(); 880 } 881 mCallback.onMailboxSelected(accountId, id, nestedNavigation); 882 } 883 } 884 885 /** 886 * Really highlight the mailbox for {@link #mHighlightedMailboxId} on the list view. 887 * 888 * Note if a list item for {@link #mHighlightedMailboxId} is not found, 889 * {@link #mHighlightedMailboxId} will be set to {@link Mailbox#NO_MAILBOX}. 890 * 891 * @return false when the highlighted mailbox seems to be gone; i.e. if 892 * {@link #mHighlightedMailboxId} is set but not found in the list. 893 */ 894 private boolean updateHighlightedMailbox(boolean ensureSelectionVisible) { 895 if (!getEnableHighlight() || !isViewCreated()) { 896 return true; // Nothing to highlight 897 } 898 final ListView lv = getListView(); 899 boolean found = false; 900 if (mHighlightedMailboxId == Mailbox.NO_MAILBOX) { 901 // No mailbox selected 902 lv.clearChoices(); 903 found = true; 904 } else { 905 // TODO Don't mix list view & list adapter indices. This is a recipe for disaster. 906 final int count = lv.getCount(); 907 for (int i = 0; i < count; i++) { 908 if (mListAdapter.getId(i) != mHighlightedMailboxId) { 909 continue; 910 } 911 found = true; 912 lv.setItemChecked(i, true); 913 if (ensureSelectionVisible) { 914 Utility.listViewSmoothScrollToPosition(getActivity(), lv, i); 915 } 916 break; 917 } 918 } 919 if (!found) { 920 mHighlightedMailboxId = Mailbox.NO_MAILBOX; 921 } 922 return found; 923 } 924 925 // Drag & Drop handling 926 927 /** 928 * Update all of the list's child views with the proper target background (for now, orange if 929 * a valid target, except red if the trash; standard background otherwise) 930 */ 931 private void updateChildViews() { 932 final ListView lv = getListView(); 933 int itemCount = lv.getChildCount(); 934 // Lazily initialize the height of our list items 935 if (itemCount > 0 && mDragItemHeight < 0) { 936 mDragItemHeight = lv.getChildAt(0).getHeight(); 937 } 938 for (int i = 0; i < itemCount; i++) { 939 final View child = lv.getChildAt(i); 940 if (!(child instanceof MailboxListItem)) { 941 continue; 942 } 943 MailboxListItem item = (MailboxListItem) child; 944 item.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); 945 } 946 } 947 948 /** 949 * Called when the user has dragged outside of the mailbox list area. 950 */ 951 private void onDragExited() { 952 // Reset the background of the current target 953 if (mDropTargetView != null) { 954 mDropTargetView.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); 955 mDropTargetView = null; 956 } 957 mDropTargetId = NO_DROP_TARGET; 958 stopScrolling(); 959 } 960 961 /** 962 * Called while dragging; highlight possible drop targets, and auto scroll the list. 963 */ 964 private void onDragLocation(DragEvent event) { 965 final ListView lv = getListView(); 966 // TODO The list may be changing while in drag-n-drop; temporarily suspend drag-n-drop 967 // if the list is being updated [i.e. navigated to another mailbox] 968 if (mDragItemHeight <= 0) { 969 // This shouldn't be possible, but avoid NPE 970 Log.w(TAG, "drag item height is not set"); 971 return; 972 } 973 // Find out which item we're in and highlight as appropriate 974 final int rawTouchX = (int) event.getX(); 975 final int rawTouchY = (int) event.getY(); 976 final int viewIndex = pointToIndex(lv, rawTouchX, rawTouchY); 977 int targetId = viewIndex; 978 if (targetId != mDropTargetId) { 979 if (DEBUG_DRAG_DROP) { 980 Log.d(TAG, "=== Target changed; oldId: " + mDropTargetId + ", newId: " + targetId); 981 } 982 // Remove highlight the current target; if there was one 983 if (mDropTargetView != null) { 984 mDropTargetView.setDropTargetBackground(true, mDragItemMailboxId); 985 mDropTargetView = null; 986 } 987 // Get the new target mailbox view 988 final View childView = lv.getChildAt(viewIndex); 989 final MailboxListItem newTarget; 990 if (childView == null) { 991 // In any event, we're no longer dragging in the list view if newTarget is null 992 if (DEBUG_DRAG_DROP) { 993 Log.d(TAG, "=== Drag off the list"); 994 } 995 newTarget = null; 996 final int childCount = lv.getChildCount(); 997 if (viewIndex >= childCount) { 998 // Touching beyond the end of the list; may happen for small lists 999 onDragExited(); 1000 return; 1001 } else { 1002 // We should never get here 1003 Log.w(TAG, "null view; idx: " + viewIndex + ", cnt: " + childCount); 1004 } 1005 } else if (!(childView instanceof MailboxListItem)) { 1006 // We're over a header suchas "Recent folders". We shouldn't finish DnD, but 1007 // drop should be disabled. 1008 newTarget = null; 1009 targetId = NO_DROP_TARGET; 1010 } else { 1011 newTarget = (MailboxListItem) childView; 1012 if (newTarget.mMailboxType == Mailbox.TYPE_TRASH) { 1013 if (DEBUG_DRAG_DROP) { 1014 Log.d(TAG, "=== Trash mailbox; id: " + newTarget.mMailboxId); 1015 } 1016 newTarget.setDropTrashBackground(); 1017 } else if (newTarget.isDropTarget(mDragItemMailboxId)) { 1018 if (DEBUG_DRAG_DROP) { 1019 Log.d(TAG, "=== Target mailbox; id: " + newTarget.mMailboxId); 1020 } 1021 newTarget.setDropActiveBackground(); 1022 } else { 1023 if (DEBUG_DRAG_DROP) { 1024 Log.d(TAG, "=== Non-droppable mailbox; id: " + newTarget.mMailboxId); 1025 } 1026 newTarget.setDropTargetBackground(true, mDragItemMailboxId); 1027 targetId = NO_DROP_TARGET; 1028 } 1029 } 1030 // Save away our current position and view 1031 mDropTargetId = targetId; 1032 mDropTargetView = newTarget; 1033 } 1034 1035 // This is a quick-and-dirty implementation of drag-under-scroll; something like this 1036 // should eventually find its way into the framework 1037 int scrollDiff = rawTouchY - (lv.getHeight() - SCROLL_ZONE_SIZE); 1038 boolean scrollDown = (scrollDiff > 0); 1039 boolean scrollUp = (SCROLL_ZONE_SIZE > rawTouchY); 1040 if (!mTargetScrolling && scrollDown) { 1041 int itemsToScroll = lv.getCount() - lv.getLastVisiblePosition(); 1042 int pixelsToScroll = (itemsToScroll + 1) * mDragItemHeight; 1043 lv.smoothScrollBy(pixelsToScroll, pixelsToScroll * SCROLL_SPEED); 1044 if (DEBUG_DRAG_DROP) { 1045 Log.d(TAG, "=== Start scrolling list down"); 1046 } 1047 mTargetScrolling = true; 1048 } else if (!mTargetScrolling && scrollUp) { 1049 int pixelsToScroll = (lv.getFirstVisiblePosition() + 1) * mDragItemHeight; 1050 lv.smoothScrollBy(-pixelsToScroll, pixelsToScroll * SCROLL_SPEED); 1051 if (DEBUG_DRAG_DROP) { 1052 Log.d(TAG, "=== Start scrolling list up"); 1053 } 1054 mTargetScrolling = true; 1055 } else if (!scrollUp && !scrollDown) { 1056 stopScrolling(); 1057 } 1058 } 1059 1060 /** 1061 * Indicate that scrolling has stopped 1062 */ 1063 private void stopScrolling() { 1064 final ListView lv = getListView(); 1065 if (mTargetScrolling) { 1066 mTargetScrolling = false; 1067 if (DEBUG_DRAG_DROP) { 1068 Log.d(TAG, "=== Stop scrolling list"); 1069 } 1070 // Stop the scrolling 1071 lv.smoothScrollBy(0, 0); 1072 } 1073 } 1074 1075 private void onDragEnded() { 1076 if (mDragInProgress) { 1077 mDragInProgress = false; 1078 // Reenable updates to the view and redraw (in case it changed) 1079 MailboxFragmentAdapter.enableUpdates(true); 1080 mListAdapter.notifyDataSetChanged(); 1081 // Stop highlighting targets 1082 updateChildViews(); 1083 // Stop any scrolling that was going on 1084 stopScrolling(); 1085 } 1086 } 1087 1088 private boolean onDragStarted(DragEvent event) { 1089 // We handle dropping of items with our email mime type 1090 // If the mime type has a mailbox id appended, that is the mailbox of the item 1091 // being draged 1092 ClipDescription description = event.getClipDescription(); 1093 int mimeTypeCount = description.getMimeTypeCount(); 1094 for (int i = 0; i < mimeTypeCount; i++) { 1095 String mimeType = description.getMimeType(i); 1096 if (mimeType.startsWith(EmailProvider.EMAIL_MESSAGE_MIME_TYPE)) { 1097 if (DEBUG_DRAG_DROP) { 1098 Log.d(TAG, "=== Drag started"); 1099 } 1100 mDragItemMailboxId = -1; 1101 // See if we find a mailbox id here 1102 int dash = mimeType.lastIndexOf('-'); 1103 if (dash > 0) { 1104 try { 1105 mDragItemMailboxId = Long.parseLong(mimeType.substring(dash + 1)); 1106 } catch (NumberFormatException e) { 1107 // Ignore; we just won't know the mailbox 1108 } 1109 } 1110 mDragInProgress = true; 1111 // Stop the list from updating 1112 MailboxFragmentAdapter.enableUpdates(false); 1113 // Update the backgrounds of our child views to highlight drop targets 1114 updateChildViews(); 1115 return true; 1116 } 1117 } 1118 return false; 1119 } 1120 1121 /** 1122 * Perform a "drop" action. If the user is not on top of a valid drop target, no action 1123 * is performed. 1124 * @return {@code true} if the drop action was performed. Otherwise {@code false}. 1125 */ 1126 private boolean onDrop(DragEvent event) { 1127 stopScrolling(); 1128 // If we're not on a target, we're done 1129 if (mDropTargetId == NO_DROP_TARGET) { 1130 return false; 1131 } 1132 final Controller controller = Controller.getInstance(mActivity); 1133 ClipData clipData = event.getClipData(); 1134 int count = clipData.getItemCount(); 1135 if (DEBUG_DRAG_DROP) { 1136 Log.d(TAG, "=== Dropping " + count + " items."); 1137 } 1138 // Extract the messageId's to move from the ClipData (set up in MessageListItem) 1139 final long[] messageIds = new long[count]; 1140 for (int i = 0; i < count; i++) { 1141 Uri uri = clipData.getItemAt(i).getUri(); 1142 String msgNum = uri.getPathSegments().get(1); 1143 long id = Long.parseLong(msgNum); 1144 messageIds[i] = id; 1145 } 1146 // Call either deleteMessage or moveMessage, depending on the target 1147 if (mDropTargetView.mMailboxType == Mailbox.TYPE_TRASH) { 1148 controller.deleteMessages(messageIds); 1149 } else { 1150 controller.moveMessages(messageIds, mDropTargetView.mMailboxId); 1151 } 1152 return true; 1153 } 1154 1155 @Override 1156 public boolean onDrag(View view, DragEvent event) { 1157 boolean result = false; 1158 switch (event.getAction()) { 1159 case DragEvent.ACTION_DRAG_STARTED: 1160 result = onDragStarted(event); 1161 break; 1162 case DragEvent.ACTION_DRAG_ENTERED: 1163 // The drag has entered the ListView window 1164 if (DEBUG_DRAG_DROP) { 1165 Log.d(TAG, "=== Drag entered; targetId: " + mDropTargetId); 1166 } 1167 break; 1168 case DragEvent.ACTION_DRAG_EXITED: 1169 // The drag has left the building 1170 if (DEBUG_DRAG_DROP) { 1171 Log.d(TAG, "=== Drag exited; targetId: " + mDropTargetId); 1172 } 1173 onDragExited(); 1174 break; 1175 case DragEvent.ACTION_DRAG_ENDED: 1176 // The drag is over 1177 if (DEBUG_DRAG_DROP) { 1178 Log.d(TAG, "=== Drag ended"); 1179 } 1180 onDragEnded(); 1181 break; 1182 case DragEvent.ACTION_DRAG_LOCATION: 1183 // We're moving around within our window; handle scroll, if necessary 1184 onDragLocation(event); 1185 break; 1186 case DragEvent.ACTION_DROP: 1187 // The drag item was dropped 1188 if (DEBUG_DRAG_DROP) { 1189 Log.d(TAG, "=== Drop"); 1190 } 1191 result = onDrop(event); 1192 break; 1193 default: 1194 break; 1195 } 1196 return result; 1197 } 1198 } 1199