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