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