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.content.ClipData; 23 import android.content.ContentUris; 24 import android.content.Context; 25 import android.content.Loader; 26 import android.content.res.Configuration; 27 import android.content.res.Resources; 28 import android.database.Cursor; 29 import android.graphics.Canvas; 30 import android.graphics.Point; 31 import android.graphics.PointF; 32 import android.graphics.Rect; 33 import android.graphics.Typeface; 34 import android.graphics.drawable.Drawable; 35 import android.os.Bundle; 36 import android.os.Parcelable; 37 import android.text.TextPaint; 38 import android.util.Log; 39 import android.view.ActionMode; 40 import android.view.DragEvent; 41 import android.view.LayoutInflater; 42 import android.view.Menu; 43 import android.view.MenuInflater; 44 import android.view.MenuItem; 45 import android.view.MotionEvent; 46 import android.view.View; 47 import android.view.View.DragShadowBuilder; 48 import android.view.View.OnDragListener; 49 import android.view.View.OnTouchListener; 50 import android.view.ViewGroup; 51 import android.widget.AdapterView; 52 import android.widget.AdapterView.OnItemLongClickListener; 53 import android.widget.ListView; 54 import android.widget.TextView; 55 import android.widget.Toast; 56 57 import com.android.email.Controller; 58 import com.android.email.Email; 59 import com.android.email.MessageListContext; 60 import com.android.email.NotificationController; 61 import com.android.email.R; 62 import com.android.email.RefreshManager; 63 import com.android.email.activity.MessagesAdapter.SearchResultsCursor; 64 import com.android.email.provider.EmailProvider; 65 import com.android.emailcommon.Logging; 66 import com.android.emailcommon.provider.Account; 67 import com.android.emailcommon.provider.EmailContent.Message; 68 import com.android.emailcommon.provider.Mailbox; 69 import com.android.emailcommon.utility.EmailAsyncTask; 70 import com.android.emailcommon.utility.Utility; 71 import com.google.common.annotations.VisibleForTesting; 72 73 import java.util.HashMap; 74 import java.util.Set; 75 76 /** 77 * Message list. 78 * 79 * See the class javadoc for {@link MailboxListFragment} for notes on {@link #getListView()} and 80 * {@link #isViewCreated()}. 81 */ 82 public class MessageListFragment extends ListFragment 83 implements OnItemLongClickListener, MessagesAdapter.Callback, 84 MoveMessageToDialog.Callback, OnDragListener, OnTouchListener { 85 private static final String BUNDLE_LIST_STATE = "MessageListFragment.state.listState"; 86 private static final String BUNDLE_KEY_SELECTED_MESSAGE_ID 87 = "messageListFragment.state.listState.selected_message_id"; 88 89 private static final int LOADER_ID_MESSAGES_LOADER = 1; 90 91 /** Argument name(s) */ 92 private static final String ARG_LIST_CONTEXT = "listContext"; 93 94 // Controller access 95 private Controller mController; 96 private RefreshManager mRefreshManager; 97 private final RefreshListener mRefreshListener = new RefreshListener(); 98 99 // UI Support 100 private Activity mActivity; 101 private Callback mCallback = EmptyCallback.INSTANCE; 102 private boolean mIsViewCreated; 103 104 private View mListPanel; 105 private View mListFooterView; 106 private TextView mListFooterText; 107 private View mListFooterProgress; 108 private ViewGroup mSearchHeader; 109 private ViewGroup mWarningContainer; 110 private TextView mSearchHeaderText; 111 private TextView mSearchHeaderCount; 112 113 private static final int LIST_FOOTER_MODE_NONE = 0; 114 private static final int LIST_FOOTER_MODE_MORE = 1; 115 private int mListFooterMode; 116 117 private MessagesAdapter mListAdapter; 118 private boolean mIsFirstLoad; 119 120 /** ID of the message to hightlight. */ 121 private long mSelectedMessageId = -1; 122 123 private Account mAccount; 124 private Mailbox mMailbox; 125 /** The original mailbox being searched, if this list is showing search results. */ 126 private Mailbox mSearchedMailbox; 127 private boolean mIsEasAccount; 128 private boolean mIsRefreshable; 129 private int mCountTotalAccounts; 130 131 // Misc members 132 133 private boolean mShowSendCommand; 134 private boolean mShowMoveCommand; 135 136 /** 137 * If true, we disable the CAB even if there are selected messages. 138 * It's used in portrait on the tablet when the message view becomes visible and the message 139 * list gets pushed out of the screen, in which case we want to keep the selection but the CAB 140 * should be gone. 141 */ 142 private boolean mDisableCab; 143 144 /** true between {@link #onResume} and {@link #onPause}. */ 145 private boolean mResumed; 146 147 /** 148 * {@link ActionMode} shown when 1 or more message is selected. 149 */ 150 private ActionMode mSelectionMode; 151 private SelectionModeCallback mLastSelectionModeCallback; 152 153 private Parcelable mSavedListState; 154 155 private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); 156 157 /** 158 * Callback interface that owning activities must implement 159 */ 160 public interface Callback { 161 public static final int TYPE_REGULAR = 0; 162 public static final int TYPE_DRAFT = 1; 163 public static final int TYPE_TRASH = 2; 164 165 /** 166 * Called when the specified mailbox does not exist. 167 */ 168 public void onMailboxNotFound(boolean firstLoad); 169 170 /** 171 * Called when the user wants to open a message. 172 * Note {@code mailboxId} is of the actual mailbox of the message, which is different from 173 * {@link MessageListFragment#getMailboxId} if it's magic mailboxes. 174 * 175 * @param messageId the message ID of the message 176 * @param messageMailboxId the mailbox ID of the message. 177 * This will never take values like {@link Mailbox#QUERY_ALL_INBOXES}. 178 * @param listMailboxId the mailbox ID of the listbox shown on this fragment. 179 * This can be that of a magic mailbox, e.g. {@link Mailbox#QUERY_ALL_INBOXES}. 180 * @param type {@link #TYPE_REGULAR}, {@link #TYPE_DRAFT} or {@link #TYPE_TRASH}. 181 */ 182 public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId, 183 int type); 184 185 /** 186 * Called when an operation is initiated that can potentially advance the current 187 * message selection (e.g. a delete operation may advance the selection). 188 * @param affectedMessages the messages the operation will apply to 189 */ 190 public void onAdvancingOpAccepted(Set<Long> affectedMessages); 191 192 /** 193 * Called when a drag & drop is initiated. 194 * 195 * @return true if drag & drop is allowed 196 */ 197 public boolean onDragStarted(); 198 199 /** 200 * Called when a drag & drop is ended. 201 */ 202 public void onDragEnded(); 203 } 204 205 private static final class EmptyCallback implements Callback { 206 public static final Callback INSTANCE = new EmptyCallback(); 207 208 @Override 209 public void onMailboxNotFound(boolean isFirstLoad) { 210 } 211 212 @Override 213 public void onMessageOpen( 214 long messageId, long messageMailboxId, long listMailboxId, int type) { 215 } 216 217 @Override 218 public void onAdvancingOpAccepted(Set<Long> affectedMessages) { 219 } 220 221 @Override 222 public boolean onDragStarted() { 223 return false; // We don't know -- err on the safe side. 224 } 225 226 @Override 227 public void onDragEnded() { 228 } 229 } 230 231 /** 232 * Create a new instance with initialization parameters. 233 * 234 * This fragment should be created only with this method. (Arguments should always be set.) 235 * 236 * @param listContext The list context to show messages for 237 */ 238 public static MessageListFragment newInstance(MessageListContext listContext) { 239 final MessageListFragment instance = new MessageListFragment(); 240 final Bundle args = new Bundle(); 241 args.putParcelable(ARG_LIST_CONTEXT, listContext); 242 instance.setArguments(args); 243 return instance; 244 } 245 246 /** 247 * The context describing the contents to be shown in the list. 248 * Do not use directly; instead, use the getters such as {@link #getAccountId()}. 249 * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language 250 * constructs, this <em>must</em> be considered immutable. 251 */ 252 private MessageListContext mListContext; 253 254 private void initializeArgCache() { 255 if (mListContext != null) return; 256 mListContext = getArguments().getParcelable(ARG_LIST_CONTEXT); 257 } 258 259 /** 260 * @return the account ID passed to {@link #newInstance}. Safe to call even before onCreate. 261 * 262 * NOTE it may return {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 263 */ 264 public long getAccountId() { 265 initializeArgCache(); 266 return mListContext.mAccountId; 267 } 268 269 /** 270 * @return the mailbox ID passed to {@link #newInstance}. Safe to call even before onCreate. 271 */ 272 public long getMailboxId() { 273 initializeArgCache(); 274 return mListContext.getMailboxId(); 275 } 276 277 /** 278 * @return true if the mailbox is a combined mailbox. Safe to call even before onCreate. 279 */ 280 public boolean isCombinedMailbox() { 281 return getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW; 282 } 283 284 public MessageListContext getListContext() { 285 initializeArgCache(); 286 return mListContext; 287 } 288 289 /** 290 * @return Whether or not initial data is loaded in this list. 291 */ 292 public boolean hasDataLoaded() { 293 return mCountTotalAccounts > 0; 294 } 295 296 /** 297 * @return The account object, when known. Null if not yet known. 298 */ 299 public Account getAccount() { 300 return mAccount; 301 } 302 303 /** 304 * @return The mailbox where the messages belong in, when known. Null if not yet known. 305 */ 306 public Mailbox getMailbox() { 307 return mMailbox; 308 } 309 310 /** 311 * @return Whether or not this message list is showing a user's inbox. 312 * Note that combined inbox view is treated as an inbox view. 313 */ 314 public boolean isInboxList() { 315 MessageListContext listContext = getListContext(); 316 long accountId = listContext.mAccountId; 317 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 318 return listContext.getMailboxId() == Mailbox.QUERY_ALL_INBOXES; 319 } 320 321 if (!hasDataLoaded()) { 322 // If the data hasn't finished loading, we don't have the full mailbox - infer from ID. 323 long inboxId = Mailbox.findMailboxOfType(mActivity, accountId, Mailbox.TYPE_INBOX); 324 return listContext.getMailboxId() == inboxId; 325 } 326 return (mMailbox != null) && (mMailbox.mType == Mailbox.TYPE_INBOX); 327 } 328 329 /** 330 * @return The mailbox being searched, when known. Null if not yet known or if not a search 331 * result. 332 */ 333 public Mailbox getSearchedMailbox() { 334 return mSearchedMailbox; 335 } 336 337 @Override 338 public void onAttach(Activity activity) { 339 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 340 Log.d(Logging.LOG_TAG, this + " onAttach"); 341 } 342 super.onAttach(activity); 343 } 344 345 @Override 346 public void onCreate(Bundle savedInstanceState) { 347 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 348 Log.d(Logging.LOG_TAG, this + " onCreate"); 349 } 350 super.onCreate(savedInstanceState); 351 352 mActivity = getActivity(); 353 setHasOptionsMenu(true); 354 mController = Controller.getInstance(mActivity); 355 mRefreshManager = RefreshManager.getInstance(mActivity); 356 357 mListAdapter = new MessagesAdapter(mActivity, this, getListContext().isSearch()); 358 mIsFirstLoad = true; 359 } 360 361 @Override 362 public View onCreateView( 363 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 364 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 365 Log.d(Logging.LOG_TAG, this + " onCreateView"); 366 } 367 // Use a custom layout, which includes the original layout with "send messages" panel. 368 View root = inflater.inflate(R.layout.message_list_fragment,null); 369 mIsViewCreated = true; 370 mListPanel = UiUtilities.getView(root, R.id.list_panel); 371 return root; 372 } 373 374 public void setLayout(ThreePaneLayout layout) { 375 if (UiUtilities.useTwoPane(mActivity)) { 376 mListAdapter.setLayout(layout); 377 } 378 } 379 380 private void initSearchHeader() { 381 if (mSearchHeader == null) { 382 ViewGroup root = (ViewGroup) getView(); 383 mSearchHeader = (ViewGroup) LayoutInflater.from(mActivity).inflate( 384 R.layout.message_list_search_header, root, false); 385 mSearchHeaderText = UiUtilities.getView(mSearchHeader, R.id.search_header_text); 386 mSearchHeaderCount = UiUtilities.getView(mSearchHeader, R.id.search_count); 387 388 // Add above the actual list. 389 root.addView(mSearchHeader, 0); 390 } 391 } 392 393 /** 394 * @return true if the content view is created and not destroyed yet. (i.e. between 395 * {@link #onCreateView} and {@link #onDestroyView}. 396 */ 397 private boolean isViewCreated() { 398 // Note that we don't use "getView() != null". This method is used in updateSelectionMode() 399 // to determine if CAB shold be shown. But because it's called from onDestroyView(), at 400 // this point the fragment still has views but we want to hide CAB, we can't use 401 // getView() here. 402 return mIsViewCreated; 403 } 404 405 @Override 406 public void onActivityCreated(Bundle savedInstanceState) { 407 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 408 Log.d(Logging.LOG_TAG, this + " onActivityCreated"); 409 } 410 super.onActivityCreated(savedInstanceState); 411 412 final ListView lv = getListView(); 413 lv.setOnItemLongClickListener(this); 414 lv.setOnTouchListener(this); 415 lv.setItemsCanFocus(false); 416 lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 417 418 mListFooterView = getActivity().getLayoutInflater().inflate( 419 R.layout.message_list_item_footer, lv, false); 420 setEmptyText(getString(R.string.message_list_no_messages)); 421 422 if (savedInstanceState != null) { 423 // Fragment doesn't have this method. Call it manually. 424 restoreInstanceState(savedInstanceState); 425 } 426 427 startLoading(); 428 429 UiUtilities.installFragment(this); 430 } 431 432 @Override 433 public void onStart() { 434 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 435 Log.d(Logging.LOG_TAG, this + " onStart"); 436 } 437 super.onStart(); 438 } 439 440 @Override 441 public void onResume() { 442 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 443 Log.d(Logging.LOG_TAG, this + " onResume"); 444 } 445 super.onResume(); 446 adjustMessageNotification(false); 447 mRefreshManager.registerListener(mRefreshListener); 448 mResumed = true; 449 } 450 451 @Override 452 public void onPause() { 453 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 454 Log.d(Logging.LOG_TAG, this + " onPause"); 455 } 456 mResumed = false; 457 mSavedListState = getListView().onSaveInstanceState(); 458 adjustMessageNotification(true); 459 super.onPause(); 460 } 461 462 @Override 463 public void onStop() { 464 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 465 Log.d(Logging.LOG_TAG, this + " onStop"); 466 } 467 mTaskTracker.cancellAllInterrupt(); 468 mRefreshManager.unregisterListener(mRefreshListener); 469 470 super.onStop(); 471 } 472 473 @Override 474 public void onDestroyView() { 475 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 476 Log.d(Logging.LOG_TAG, this + " onDestroyView"); 477 } 478 mIsViewCreated = false; // Clear this first for updateSelectionMode(). See isViewCreated(). 479 UiUtilities.uninstallFragment(this); 480 updateSelectionMode(); 481 482 // Reset the footer mode since we just blew away the footer view we were holding on to. 483 // This will get re-updated when/if this fragment is restored. 484 mListFooterMode = LIST_FOOTER_MODE_NONE; 485 super.onDestroyView(); 486 } 487 488 @Override 489 public void onDestroy() { 490 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 491 Log.d(Logging.LOG_TAG, this + " onDestroy"); 492 } 493 494 finishSelectionMode(); 495 super.onDestroy(); 496 } 497 498 @Override 499 public void onDetach() { 500 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 501 Log.d(Logging.LOG_TAG, this + " onDetach"); 502 } 503 super.onDetach(); 504 } 505 506 @Override 507 public void onSaveInstanceState(Bundle outState) { 508 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 509 Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); 510 } 511 super.onSaveInstanceState(outState); 512 mListAdapter.onSaveInstanceState(outState); 513 if (isViewCreated()) { 514 outState.putParcelable(BUNDLE_LIST_STATE, getListView().onSaveInstanceState()); 515 } 516 outState.putLong(BUNDLE_KEY_SELECTED_MESSAGE_ID, mSelectedMessageId); 517 } 518 519 @VisibleForTesting 520 void restoreInstanceState(Bundle savedInstanceState) { 521 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 522 Log.d(Logging.LOG_TAG, this + " restoreInstanceState"); 523 } 524 mListAdapter.loadState(savedInstanceState); 525 mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE); 526 mSelectedMessageId = savedInstanceState.getLong(BUNDLE_KEY_SELECTED_MESSAGE_ID); 527 } 528 529 @Override 530 public void onPrepareOptionsMenu(Menu menu) { 531 MenuItem send = menu.findItem(R.id.send); 532 if (send != null) { 533 send.setVisible(mShowSendCommand); 534 } 535 } 536 537 @Override 538 public boolean onOptionsItemSelected(MenuItem item) { 539 switch (item.getItemId()) { 540 case R.id.send: 541 onSendPendingMessages(); 542 return true; 543 544 } 545 return false; 546 } 547 548 public void setCallback(Callback callback) { 549 mCallback = (callback != null) ? callback : EmptyCallback.INSTANCE; 550 } 551 552 /** 553 * This method must be called when the fragment is hidden/shown. 554 */ 555 public void onHidden(boolean hidden) { 556 // When hidden, we need to disable CAB. 557 if (hidden == mDisableCab) { 558 return; 559 } 560 mDisableCab = hidden; 561 updateSelectionMode(); 562 } 563 564 public void setSelectedMessage(long messageId) { 565 if (mSelectedMessageId == messageId) { 566 return; 567 } 568 mSelectedMessageId = messageId; 569 if (mResumed) { 570 highlightSelectedMessage(true); 571 } 572 } 573 574 /** 575 * @return true if the mailbox is refreshable. false otherwise, or unknown yet. 576 */ 577 public boolean isRefreshable() { 578 return mIsRefreshable; 579 } 580 581 /** 582 * @return the number of messages that are currently selected. 583 */ 584 private int getSelectedCount() { 585 return mListAdapter.getSelectedSet().size(); 586 } 587 588 /** 589 * @return true if the list is in the "selection" mode. 590 */ 591 public boolean isInSelectionMode() { 592 return mSelectionMode != null; 593 } 594 595 /** 596 * Called when a message is clicked. 597 */ 598 @Override 599 public void onListItemClick(ListView parent, View view, int position, long id) { 600 if (view != mListFooterView) { 601 MessageListItem itemView = (MessageListItem) view; 602 onMessageOpen(itemView.mMailboxId, id); 603 } else { 604 doFooterClick(); 605 } 606 } 607 608 // This is tentative drag & drop UI 609 private static class ShadowBuilder extends DragShadowBuilder { 610 private static Drawable sBackground; 611 /** Paint information for the move message text */ 612 private static TextPaint sMessagePaint; 613 /** Paint information for the message count */ 614 private static TextPaint sCountPaint; 615 /** The x location of any touch event; used to ensure the drag overlay is drawn correctly */ 616 private static int sTouchX; 617 618 /** Width of the draggable view */ 619 private final int mDragWidth; 620 /** Height of the draggable view */ 621 private final int mDragHeight; 622 623 private final String mMessageText; 624 private final PointF mMessagePoint; 625 626 private final String mCountText; 627 private final PointF mCountPoint; 628 private int mOldOrientation = Configuration.ORIENTATION_UNDEFINED; 629 630 /** Margin applied to the right of count text */ 631 private static float sCountMargin; 632 /** Margin applied to left of the message text */ 633 private static float sMessageMargin; 634 /** Vertical offset of the drag view */ 635 private static int sDragOffset; 636 637 public ShadowBuilder(View view, int count) { 638 super(view); 639 Resources res = view.getResources(); 640 int newOrientation = res.getConfiguration().orientation; 641 642 mDragHeight = view.getHeight(); 643 mDragWidth = view.getWidth(); 644 645 // TODO: Can we define a layout for the contents of the drag area? 646 if (sBackground == null || mOldOrientation != newOrientation) { 647 mOldOrientation = newOrientation; 648 649 sBackground = res.getDrawable(R.drawable.list_pressed_holo); 650 sBackground.setBounds(0, 0, mDragWidth, mDragHeight); 651 652 sDragOffset = (int)res.getDimension(R.dimen.message_list_drag_offset); 653 654 sMessagePaint = new TextPaint(); 655 float messageTextSize; 656 messageTextSize = res.getDimension(R.dimen.message_list_drag_message_font_size); 657 sMessagePaint.setTextSize(messageTextSize); 658 sMessagePaint.setTypeface(Typeface.DEFAULT_BOLD); 659 sMessagePaint.setAntiAlias(true); 660 sMessageMargin = res.getDimension(R.dimen.message_list_drag_message_right_margin); 661 662 sCountPaint = new TextPaint(); 663 float countTextSize; 664 countTextSize = res.getDimension(R.dimen.message_list_drag_count_font_size); 665 sCountPaint.setTextSize(countTextSize); 666 sCountPaint.setTypeface(Typeface.DEFAULT_BOLD); 667 sCountPaint.setAntiAlias(true); 668 sCountMargin = res.getDimension(R.dimen.message_list_drag_count_left_margin); 669 } 670 671 // Calculate layout positions 672 Rect b = new Rect(); 673 674 mMessageText = res.getQuantityString(R.plurals.move_messages, count, count); 675 sMessagePaint.getTextBounds(mMessageText, 0, mMessageText.length(), b); 676 mMessagePoint = new PointF(mDragWidth - b.right - sMessageMargin, 677 (mDragHeight - b.top)/ 2); 678 679 mCountText = Integer.toString(count); 680 sCountPaint.getTextBounds(mCountText, 0, mCountText.length(), b); 681 mCountPoint = new PointF(sCountMargin, 682 (mDragHeight - b.top) / 2); 683 } 684 685 @Override 686 public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) { 687 shadowSize.set(mDragWidth, mDragHeight); 688 shadowTouchPoint.set(sTouchX, (mDragHeight / 2) + sDragOffset); 689 } 690 691 @Override 692 public void onDrawShadow(Canvas canvas) { 693 super.onDrawShadow(canvas); 694 sBackground.draw(canvas); 695 canvas.drawText(mMessageText, mMessagePoint.x, mMessagePoint.y, sMessagePaint); 696 canvas.drawText(mCountText, mCountPoint.x, mCountPoint.y, sCountPaint); 697 } 698 } 699 700 @Override 701 public boolean onDrag(View view, DragEvent event) { 702 switch(event.getAction()) { 703 case DragEvent.ACTION_DRAG_ENDED: 704 if (event.getResult()) { 705 onDeselectAll(); // Clear the selection 706 } 707 mCallback.onDragEnded(); 708 break; 709 } 710 return false; 711 } 712 713 @Override 714 public boolean onTouch(View v, MotionEvent event) { 715 if (event.getAction() == MotionEvent.ACTION_DOWN) { 716 // Save the touch location to draw the drag overlay at the correct location 717 ShadowBuilder.sTouchX = (int)event.getX(); 718 } 719 // don't do anything, let the system process the event 720 return false; 721 } 722 723 @Override 724 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { 725 if (view != mListFooterView) { 726 // Always toggle the item. 727 MessageListItem listItem = (MessageListItem) view; 728 boolean toggled = false; 729 if (!mListAdapter.isSelected(listItem)) { 730 toggleSelection(listItem); 731 toggled = true; 732 } 733 734 // Additionally, check to see if we can drag the item. 735 if (!mCallback.onDragStarted()) { 736 return toggled; // D&D not allowed. 737 } 738 // We can't move from combined accounts view 739 // We also need to check the actual mailbox to see if we can move items from it 740 final long mailboxId = getMailboxId(); 741 if (mAccount == null || mMailbox == null) { 742 return false; 743 } else if (mailboxId > 0 && !mMailbox.canHaveMessagesMoved()) { 744 return false; 745 } 746 // Start drag&drop. 747 748 // Create ClipData with the Uri of the message we're long clicking 749 ClipData data = ClipData.newUri(mActivity.getContentResolver(), 750 MessageListItem.MESSAGE_LIST_ITEMS_CLIP_LABEL, Message.CONTENT_URI.buildUpon() 751 .appendPath(Long.toString(listItem.mMessageId)) 752 .appendQueryParameter( 753 EmailProvider.MESSAGE_URI_PARAMETER_MAILBOX_ID, 754 Long.toString(mailboxId)) 755 .build()); 756 Set<Long> selectedMessageIds = mListAdapter.getSelectedSet(); 757 int size = selectedMessageIds.size(); 758 // Add additional Uri's for any other selected messages 759 for (Long messageId: selectedMessageIds) { 760 if (messageId.longValue() != listItem.mMessageId) { 761 data.addItem(new ClipData.Item( 762 ContentUris.withAppendedId(Message.CONTENT_URI, messageId))); 763 } 764 } 765 // Start dragging now 766 listItem.setOnDragListener(this); 767 listItem.startDrag(data, new ShadowBuilder(listItem, size), null, 0); 768 return true; 769 } 770 return false; 771 } 772 773 private void toggleSelection(MessageListItem itemView) { 774 itemView.invalidate(); 775 mListAdapter.toggleSelected(itemView); 776 } 777 778 /** 779 * Called when a message on the list is selected 780 * 781 * @param messageMailboxId the actual mailbox ID of the message. Note it's different than 782 * what is returned by {@link #getMailboxId()} for combined mailboxes. 783 * ({@link #getMailboxId()} may return special mailbox values such as 784 * {@link Mailbox#QUERY_ALL_INBOXES}) 785 * @param messageId ID of the message to open. 786 */ 787 private void onMessageOpen(final long messageMailboxId, final long messageId) { 788 if ((mMailbox != null) && (mMailbox.mId == messageMailboxId)) { 789 // Normal case - the message belongs in the mailbox list we're viewing. 790 mCallback.onMessageOpen(messageId, messageMailboxId, 791 getMailboxId(), callbackTypeForMailboxType(mMailbox.mType)); 792 return; 793 } 794 795 // Weird case - a virtual mailbox where the messages could come from different mailbox 796 // types - here we have to query the DB for the type. 797 new MessageOpenTask(messageMailboxId, messageId).cancelPreviousAndExecuteParallel(); 798 } 799 800 private int callbackTypeForMailboxType(int mailboxType) { 801 switch (mailboxType) { 802 case Mailbox.TYPE_DRAFTS: 803 return Callback.TYPE_DRAFT; 804 case Mailbox.TYPE_TRASH: 805 return Callback.TYPE_TRASH; 806 default: 807 return Callback.TYPE_REGULAR; 808 } 809 } 810 811 /** 812 * Task to look up the mailbox type for a message, and kicks the callback. 813 */ 814 private class MessageOpenTask extends EmailAsyncTask<Void, Void, Integer> { 815 private final long mMessageMailboxId; 816 private final long mMessageId; 817 818 public MessageOpenTask(long messageMailboxId, long messageId) { 819 super(mTaskTracker); 820 mMessageMailboxId = messageMailboxId; 821 mMessageId = messageId; 822 } 823 824 @Override 825 protected Integer doInBackground(Void... params) { 826 // Restore the mailbox type. Note we can't use mMailbox.mType here, because 827 // we don't have mMailbox for combined mailbox. 828 // ("All Starred" can contain any kind of messages.) 829 return callbackTypeForMailboxType( 830 Mailbox.getMailboxType(mActivity, mMessageMailboxId)); 831 } 832 833 @Override 834 protected void onSuccess(Integer type) { 835 if (type == null) { 836 return; 837 } 838 mCallback.onMessageOpen(mMessageId, mMessageMailboxId, getMailboxId(), type); 839 } 840 } 841 842 private void showMoveMessagesDialog(Set<Long> selectedSet) { 843 long[] messageIds = Utility.toPrimitiveLongArray(selectedSet); 844 MoveMessageToDialog dialog = MoveMessageToDialog.newInstance(messageIds, this); 845 dialog.show(getFragmentManager(), "dialog"); 846 } 847 848 @Override 849 public void onMoveToMailboxSelected(long newMailboxId, long[] messageIds) { 850 final Context context = getActivity(); 851 if (context == null) { 852 // Detached from activity. This callback was really delayed or a monkey was involved. 853 return; 854 } 855 856 mCallback.onAdvancingOpAccepted(Utility.toLongSet(messageIds)); 857 ActivityHelper.moveMessages(context, newMailboxId, messageIds); 858 859 // Move is async, so we can't refresh now. Instead, just clear the selection. 860 onDeselectAll(); 861 } 862 863 /** 864 * Refresh the list. NOOP for special mailboxes (e.g. combined inbox). 865 * 866 * Note: Manual refresh is enabled even for push accounts. 867 */ 868 public void onRefresh(boolean userRequest) { 869 if (mIsRefreshable) { 870 mRefreshManager.refreshMessageList(getAccountId(), getMailboxId(), userRequest); 871 } 872 } 873 874 private void onDeselectAll() { 875 mListAdapter.clearSelection(); 876 if (isInSelectionMode()) { 877 finishSelectionMode(); 878 } 879 } 880 881 /** 882 * Load more messages. NOOP for special mailboxes (e.g. combined inbox). 883 */ 884 private void onLoadMoreMessages() { 885 if (mIsRefreshable) { 886 mRefreshManager.loadMoreMessages(getAccountId(), getMailboxId()); 887 } 888 } 889 890 public void onSendPendingMessages() { 891 RefreshManager rm = RefreshManager.getInstance(mActivity); 892 if (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX) { 893 rm.sendPendingMessagesForAllAccounts(); 894 } else if (mMailbox != null) { // Magic boxes don't have a specific account id. 895 rm.sendPendingMessages(mMailbox.mAccountKey); 896 } 897 } 898 899 /** 900 * Toggles a set read/unread states. Note, the default behavior is "mark unread", so the 901 * sense of the helper methods is "true=unread"; this may be called from the UI thread 902 * 903 * @param selectedSet The current list of selected items 904 */ 905 private void toggleRead(Set<Long> selectedSet) { 906 toggleMultiple(selectedSet, new MultiToggleHelper() { 907 908 @Override 909 public boolean getField(Cursor c) { 910 return c.getInt(MessagesAdapter.COLUMN_READ) == 0; 911 } 912 913 @Override 914 public void setField(long messageId, boolean newValue) { 915 mController.setMessageReadSync(messageId, !newValue); 916 } 917 }); 918 } 919 920 /** 921 * Toggles a set of favorites (stars); this may be called from the UI thread 922 * 923 * @param selectedSet The current list of selected items 924 */ 925 private void toggleFavorite(Set<Long> selectedSet) { 926 toggleMultiple(selectedSet, new MultiToggleHelper() { 927 928 @Override 929 public boolean getField(Cursor c) { 930 return c.getInt(MessagesAdapter.COLUMN_FAVORITE) != 0; 931 } 932 933 @Override 934 public void setField(long messageId, boolean newValue) { 935 mController.setMessageFavoriteSync(messageId, newValue); 936 } 937 }); 938 } 939 940 private void deleteMessages(Set<Long> selectedSet) { 941 final long[] messageIds = Utility.toPrimitiveLongArray(selectedSet); 942 mController.deleteMessages(messageIds); 943 Toast.makeText(mActivity, mActivity.getResources().getQuantityString( 944 R.plurals.message_deleted_toast, messageIds.length), Toast.LENGTH_SHORT).show(); 945 selectedSet.clear(); 946 // Message deletion is async... Can't refresh the list immediately. 947 } 948 949 private interface MultiToggleHelper { 950 /** 951 * Return true if the field of interest is "set". If one or more are false, then our 952 * bulk action will be to "set". If all are set, our bulk action will be to "clear". 953 * @param c the cursor, positioned to the item of interest 954 * @return true if the field at this row is "set" 955 */ 956 public boolean getField(Cursor c); 957 958 /** 959 * Set or clear the field of interest; setField is called asynchronously via EmailAsyncTask 960 * @param messageId the message id of the current message 961 * @param newValue the new value to be set at this row 962 */ 963 public void setField(long messageId, boolean newValue); 964 } 965 966 /** 967 * Toggle multiple fields in a message, using the following logic: If one or more fields 968 * are "clear", then "set" them. If all fields are "set", then "clear" them all. Provider 969 * calls are applied asynchronously in setField 970 * 971 * @param selectedSet the set of messages that are selected 972 * @param helper functions to implement the specific getter & setter 973 */ 974 private void toggleMultiple(final Set<Long> selectedSet, final MultiToggleHelper helper) { 975 final Cursor c = mListAdapter.getCursor(); 976 if (c == null || c.isClosed()) { 977 return; 978 } 979 980 final HashMap<Long, Boolean> setValues = new HashMap<Long, Boolean>(); 981 boolean allWereSet = true; 982 983 c.moveToPosition(-1); 984 while (c.moveToNext()) { 985 long id = c.getInt(MessagesAdapter.COLUMN_ID); 986 if (selectedSet.contains(id)) { 987 boolean value = helper.getField(c); 988 setValues.put(id, value); 989 allWereSet = allWereSet && value; 990 } 991 } 992 993 if (!setValues.isEmpty()) { 994 final boolean newValue = !allWereSet; 995 c.moveToPosition(-1); 996 // TODO: we should probably put up a dialog or some other progress indicator for this. 997 EmailAsyncTask.runAsyncParallel(new Runnable() { 998 @Override 999 public void run() { 1000 for (long id : setValues.keySet()) { 1001 if (setValues.get(id) != newValue) { 1002 helper.setField(id, newValue); 1003 } 1004 } 1005 }}); 1006 } 1007 } 1008 1009 /** 1010 * Test selected messages for showing appropriate labels 1011 * @param selectedSet 1012 * @param columnId 1013 * @param defaultflag 1014 * @return true when the specified flagged message is selected 1015 */ 1016 private boolean testMultiple(Set<Long> selectedSet, int columnId, boolean defaultflag) { 1017 final Cursor c = mListAdapter.getCursor(); 1018 if (c == null || c.isClosed()) { 1019 return false; 1020 } 1021 c.moveToPosition(-1); 1022 while (c.moveToNext()) { 1023 long id = c.getInt(MessagesAdapter.COLUMN_ID); 1024 if (selectedSet.contains(Long.valueOf(id))) { 1025 if (c.getInt(columnId) == (defaultflag ? 1 : 0)) { 1026 return true; 1027 } 1028 } 1029 } 1030 return false; 1031 } 1032 1033 /** 1034 * @return true if one or more non-starred messages are selected. 1035 */ 1036 public boolean doesSelectionContainNonStarredMessage() { 1037 return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_FAVORITE, 1038 false); 1039 } 1040 1041 /** 1042 * @return true if one or more read messages are selected. 1043 */ 1044 public boolean doesSelectionContainReadMessage() { 1045 return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_READ, true); 1046 } 1047 1048 /** 1049 * Implements a timed refresh of "stale" mailboxes. This should only happen when 1050 * multiple conditions are true, including: 1051 * Only refreshable mailboxes. 1052 * Only when the mailbox is "stale" (currently set to 5 minutes since last refresh) 1053 * Note we do this even if it's a push account; even on Exchange only inbox can be pushed. 1054 */ 1055 private void autoRefreshStaleMailbox() { 1056 if (!mIsRefreshable) { 1057 // Not refreshable (special box such as drafts, or magic boxes) 1058 return; 1059 } 1060 if (!mRefreshManager.isMailboxStale(getMailboxId())) { 1061 return; 1062 } 1063 onRefresh(false); 1064 } 1065 1066 /** Implements {@link MessagesAdapter.Callback} */ 1067 @Override 1068 public void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite) { 1069 mController.setMessageFavorite(itemView.mMessageId, newFavorite); 1070 } 1071 1072 /** Implements {@link MessagesAdapter.Callback} */ 1073 @Override 1074 public void onAdapterSelectedChanged(MessageListItem itemView, boolean newSelected, 1075 int mSelectedCount) { 1076 updateSelectionMode(); 1077 } 1078 1079 private void updateSearchHeader(Cursor cursor) { 1080 MessageListContext listContext = getListContext(); 1081 if (!listContext.isSearch() || cursor == null) { 1082 UiUtilities.setVisibilitySafe(mSearchHeader, View.GONE); 1083 return; 1084 } 1085 1086 SearchResultsCursor searchCursor = (SearchResultsCursor) cursor; 1087 initSearchHeader(); 1088 mSearchHeader.setVisibility(View.VISIBLE); 1089 String header = mActivity.getString(R.string.search_header_text_fmt); 1090 mSearchHeaderText.setText(header); 1091 int resultCount = searchCursor.getResultsCount(); 1092 // Don't show a negative value here; this means that the server request failed 1093 // TODO Use some other text for this case (e.g. "search failed")? 1094 if (resultCount < 0) { 1095 resultCount = 0; 1096 } 1097 mSearchHeaderCount.setText(UiUtilities.getMessageCountForUi( 1098 mActivity, resultCount, false /* replaceZeroWithBlank */)); 1099 } 1100 1101 private int determineFooterMode() { 1102 int result = LIST_FOOTER_MODE_NONE; 1103 if ((mMailbox == null) 1104 || (mMailbox.mType == Mailbox.TYPE_OUTBOX) 1105 || (mMailbox.mType == Mailbox.TYPE_DRAFTS)) { 1106 return result; // No footer 1107 } 1108 if (mMailbox.mType == Mailbox.TYPE_SEARCH) { 1109 // Determine how many results have been loaded. 1110 Cursor c = mListAdapter.getCursor(); 1111 if (c == null || c.isClosed()) { 1112 // Unknown yet - don't do anything. 1113 return result; 1114 } 1115 int total = ((SearchResultsCursor) c).getResultsCount(); 1116 int loaded = c.getCount(); 1117 1118 if (loaded < total) { 1119 result = LIST_FOOTER_MODE_MORE; 1120 } 1121 } else if (!mIsEasAccount) { 1122 // IMAP, POP has "load more" for regular mailboxes. 1123 result = LIST_FOOTER_MODE_MORE; 1124 } 1125 return result; 1126 } 1127 1128 private void updateFooterView() { 1129 // Only called from onLoadFinished -- always has views. 1130 int mode = determineFooterMode(); 1131 if (mListFooterMode == mode) { 1132 return; 1133 } 1134 mListFooterMode = mode; 1135 1136 ListView lv = getListView(); 1137 if (mListFooterMode != LIST_FOOTER_MODE_NONE) { 1138 lv.addFooterView(mListFooterView); 1139 if (getListAdapter() != null) { 1140 // Already have an adapter - reset it to force the mode. But save the scroll 1141 // position so that we don't get kicked to the top. 1142 Parcelable listState = lv.onSaveInstanceState(); 1143 setListAdapter(mListAdapter); 1144 lv.onRestoreInstanceState(listState); 1145 } 1146 1147 mListFooterProgress = mListFooterView.findViewById(R.id.progress); 1148 mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text); 1149 } else { 1150 lv.removeFooterView(mListFooterView); 1151 } 1152 updateListFooter(); 1153 } 1154 1155 /** 1156 * Set the list footer text based on mode and the current "network active" status 1157 */ 1158 private void updateListFooter() { 1159 if (mListFooterMode != LIST_FOOTER_MODE_NONE) { 1160 int footerTextId = 0; 1161 switch (mListFooterMode) { 1162 case LIST_FOOTER_MODE_MORE: 1163 boolean active = mRefreshManager.isMessageListRefreshing(getMailboxId()); 1164 footerTextId = active ? R.string.status_loading_messages 1165 : R.string.message_list_load_more_messages_action; 1166 mListFooterProgress.setVisibility(active ? View.VISIBLE : View.GONE); 1167 break; 1168 } 1169 mListFooterText.setText(footerTextId); 1170 } 1171 } 1172 1173 /** 1174 * Handle a click in the list footer, which changes meaning depending on what we're looking at. 1175 */ 1176 private void doFooterClick() { 1177 switch (mListFooterMode) { 1178 case LIST_FOOTER_MODE_NONE: // should never happen 1179 break; 1180 case LIST_FOOTER_MODE_MORE: 1181 onLoadMoreMessages(); 1182 break; 1183 } 1184 } 1185 1186 private void showSendCommand(boolean show) { 1187 if (show != mShowSendCommand) { 1188 mShowSendCommand = show; 1189 mActivity.invalidateOptionsMenu(); 1190 } 1191 } 1192 1193 private void updateMailboxSpecificActions() { 1194 final boolean isOutbox = (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX) 1195 || ((mMailbox != null) && (mMailbox.mType == Mailbox.TYPE_OUTBOX)); 1196 showSendCommand(isOutbox && (mListAdapter != null) && (mListAdapter.getCount() > 0)); 1197 1198 // A null account/mailbox means we're in a combined view. We show the move icon there, 1199 // even though it may be the case that we can't move messages from one of the mailboxes. 1200 // There's no good way to tell that right now, though. 1201 mShowMoveCommand = (mAccount == null || mAccount.supportsMoveMessages(getActivity())) 1202 && (mMailbox == null || mMailbox.canHaveMessagesMoved()); 1203 1204 // Enable mailbox specific actions on the UIController level if needed. 1205 mActivity.invalidateOptionsMenu(); 1206 } 1207 1208 /** 1209 * Adjusts message notification depending upon the state of the fragment and the currently 1210 * viewed mailbox. If the fragment is resumed, notifications for the current mailbox may 1211 * be suspended. Otherwise, notifications may be re-activated. Not all mailbox types are 1212 * supported for notifications. These include (but are not limited to) special mailboxes 1213 * such as {@link Mailbox#QUERY_ALL_DRAFTS}, {@link Mailbox#QUERY_ALL_FAVORITES}, etc... 1214 * 1215 * @param updateLastSeenKey If {@code true}, the last seen message key for the currently 1216 * viewed mailbox will be updated. 1217 */ 1218 private void adjustMessageNotification(boolean updateLastSeenKey) { 1219 final long accountId = getAccountId(); 1220 final long mailboxId = getMailboxId(); 1221 if (mailboxId == Mailbox.QUERY_ALL_INBOXES || mailboxId > 0) { 1222 if (updateLastSeenKey) { 1223 Utility.updateLastSeenMessageKey(mActivity, accountId); 1224 } 1225 NotificationController notifier = NotificationController.getInstance(mActivity); 1226 notifier.suspendMessageNotification(mResumed, accountId); 1227 } 1228 } 1229 1230 private void startLoading() { 1231 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 1232 Log.d(Logging.LOG_TAG, this + " startLoading"); 1233 } 1234 // Clear the list. (ListFragment will show the "Loading" animation) 1235 showSendCommand(false); 1236 updateSearchHeader(null); 1237 1238 // Start loading... 1239 final LoaderManager lm = getLoaderManager(); 1240 lm.initLoader(LOADER_ID_MESSAGES_LOADER, null, LOADER_CALLBACKS); 1241 } 1242 1243 /** Timeout to show a warning, since some IMAP searches could take a long time. */ 1244 private final int SEARCH_WARNING_DELAY_MS = 10000; 1245 1246 private void onSearchLoadTimeout() { 1247 // Search is taking too long. Show an error message. 1248 ViewGroup root = (ViewGroup) getView(); 1249 Activity host = getActivity(); 1250 if (root != null && host != null) { 1251 mListPanel.setVisibility(View.GONE); 1252 mWarningContainer = (ViewGroup) LayoutInflater.from(host).inflate( 1253 R.layout.message_list_warning, root, false); 1254 TextView title = UiUtilities.getView(mWarningContainer, R.id.message_title); 1255 TextView message = UiUtilities.getView(mWarningContainer, R.id.message_warning); 1256 title.setText(R.string.search_slow_warning_title); 1257 message.setText(R.string.search_slow_warning_message); 1258 root.addView(mWarningContainer); 1259 } 1260 } 1261 1262 /** 1263 * Loader callbacks for message list. 1264 */ 1265 private final LoaderManager.LoaderCallbacks<Cursor> LOADER_CALLBACKS = 1266 new LoaderManager.LoaderCallbacks<Cursor>() { 1267 @Override 1268 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 1269 final MessageListContext listContext = getListContext(); 1270 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 1271 Log.d(Logging.LOG_TAG, MessageListFragment.this 1272 + " onCreateLoader(messages) listContext=" + listContext); 1273 } 1274 1275 if (mListContext.isSearch()) { 1276 final MessageListContext searchInfo = mListContext; 1277 1278 // Search results are not primed with local data, and so will usually be slow. 1279 // In some cases, they could take a long time to return, so we need to be robust. 1280 setListShownNoAnimation(false); 1281 Utility.getMainThreadHandler().postDelayed(new Runnable() { 1282 @Override 1283 public void run() { 1284 if (mListContext != searchInfo) { 1285 // Different list is being shown now. 1286 return; 1287 } 1288 if (!mIsFirstLoad) { 1289 // Something already returned. No need to do anything. 1290 return; 1291 } 1292 onSearchLoadTimeout(); 1293 } 1294 }, SEARCH_WARNING_DELAY_MS); 1295 } 1296 1297 mIsFirstLoad = true; 1298 return MessagesAdapter.createLoader(getActivity(), listContext); 1299 } 1300 1301 @Override 1302 public void onLoadFinished(Loader<Cursor> loader, Cursor c) { 1303 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 1304 Log.d(Logging.LOG_TAG, MessageListFragment.this 1305 + " onLoadFinished(messages) mailboxId=" + getMailboxId()); 1306 } 1307 MessagesAdapter.MessagesCursor cursor = (MessagesAdapter.MessagesCursor) c; 1308 1309 // Update the list 1310 mListAdapter.swapCursor(cursor); 1311 1312 if (!cursor.mIsFound) { 1313 mCallback.onMailboxNotFound(mIsFirstLoad); 1314 return; 1315 } 1316 1317 // Get the "extras" part. 1318 mAccount = cursor.mAccount; 1319 mMailbox = cursor.mMailbox; 1320 mIsEasAccount = cursor.mIsEasAccount; 1321 mIsRefreshable = cursor.mIsRefreshable; 1322 mCountTotalAccounts = cursor.mCountTotalAccounts; 1323 1324 // If this is a search result, open the first message unless we are 1325 // restoring the message position from saved state, in which case, 1326 // mSelectedMessageId was already set and should be respected. 1327 if (UiUtilities.useTwoPane(getActivity()) && mIsFirstLoad && mListContext.isSearch() 1328 && cursor.getCount() > 0 1329 && UiUtilities.showTwoPaneSearchResults(getActivity()) 1330 && mSelectedMessageId == -1) { 1331 cursor.moveToFirst(); 1332 onMessageOpen(getMailboxId(), cursor.getLong(MessagesAdapter.COLUMN_ID)); 1333 } 1334 1335 // Suspend message notifications as long as we're resumed 1336 adjustMessageNotification(false); 1337 1338 // If this is a search mailbox, set the query; otherwise, clear it 1339 if (mIsFirstLoad) { 1340 if (mMailbox != null && mMailbox.mType == Mailbox.TYPE_SEARCH) { 1341 mListAdapter.setQuery(getListContext().getSearchParams().mFilter); 1342 mSearchedMailbox = ((SearchResultsCursor) c).getSearchedMailbox(); 1343 } else { 1344 mListAdapter.setQuery(null); 1345 mSearchedMailbox = null; 1346 } 1347 updateMailboxSpecificActions(); 1348 1349 // Show chips if combined view. 1350 mListAdapter.setShowColorChips(isCombinedMailbox() && mCountTotalAccounts > 1); 1351 } 1352 1353 // Various post processing... 1354 updateSearchHeader(cursor); 1355 autoRefreshStaleMailbox(); 1356 updateFooterView(); 1357 updateSelectionMode(); 1358 1359 // We want to make visible the selection only for the first load. 1360 // Re-load caused by content changed events shouldn't scroll the list. 1361 highlightSelectedMessage(mIsFirstLoad); 1362 1363 if (mIsFirstLoad) { 1364 UiUtilities.setVisibilitySafe(mWarningContainer, View.GONE); 1365 mListPanel.setVisibility(View.VISIBLE); 1366 1367 // Setting the adapter will automatically transition from "Loading" to showing 1368 // the list, which could show "No messages". Avoid showing that on the first sync, 1369 // if we know we're still potentially loading more. 1370 if (!isEmptyAndLoading(cursor)) { 1371 setListAdapter(mListAdapter); 1372 } 1373 } else if ((getListAdapter() == null) && !isEmptyAndLoading(cursor)) { 1374 setListAdapter(mListAdapter); 1375 } 1376 1377 // Restore the state -- this step has to be the last, because Some of the 1378 // "post processing" seems to reset the scroll position. 1379 if (mSavedListState != null) { 1380 getListView().onRestoreInstanceState(mSavedListState); 1381 mSavedListState = null; 1382 } 1383 1384 mIsFirstLoad = false; 1385 } 1386 1387 /** 1388 * Determines whether or not the list is empty, but we're still potentially loading data. 1389 * This represents an ambiguous state where we may not want to show "No messages", since 1390 * it may still just be loading. 1391 */ 1392 private boolean isEmptyAndLoading(Cursor cursor) { 1393 if (mMailbox == null) return false; 1394 return cursor.getCount() == 0 && mRefreshManager.isMessageListRefreshing(mMailbox.mId); 1395 } 1396 1397 @Override 1398 public void onLoaderReset(Loader<Cursor> loader) { 1399 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 1400 Log.d(Logging.LOG_TAG, MessageListFragment.this 1401 + " onLoaderReset(messages)"); 1402 } 1403 mListAdapter.swapCursor(null); 1404 mAccount = null; 1405 mMailbox = null; 1406 mSearchedMailbox = null; 1407 mCountTotalAccounts = 0; 1408 } 1409 }; 1410 1411 /** 1412 * Show/hide the "selection" action mode, according to the number of selected messages and 1413 * the visibility of the fragment. 1414 * Also update the content (title and menus) if necessary. 1415 */ 1416 public void updateSelectionMode() { 1417 final int numSelected = getSelectedCount(); 1418 if ((numSelected == 0) || mDisableCab || !isViewCreated()) { 1419 finishSelectionMode(); 1420 return; 1421 } 1422 if (isInSelectionMode()) { 1423 updateSelectionModeView(); 1424 } else { 1425 mLastSelectionModeCallback = new SelectionModeCallback(); 1426 getActivity().startActionMode(mLastSelectionModeCallback); 1427 } 1428 } 1429 1430 1431 /** 1432 * Finish the "selection" action mode. 1433 * 1434 * Note this method finishes the contextual mode, but does *not* clear the selection. 1435 * If you want to do so use {@link #onDeselectAll()} instead. 1436 */ 1437 private void finishSelectionMode() { 1438 if (isInSelectionMode()) { 1439 mLastSelectionModeCallback.mClosedByUser = false; 1440 mSelectionMode.finish(); 1441 } 1442 } 1443 1444 /** Update the "selection" action mode bar */ 1445 private void updateSelectionModeView() { 1446 mSelectionMode.invalidate(); 1447 } 1448 1449 private class SelectionModeCallback implements ActionMode.Callback { 1450 private MenuItem mMarkRead; 1451 private MenuItem mMarkUnread; 1452 private MenuItem mAddStar; 1453 private MenuItem mRemoveStar; 1454 private MenuItem mMove; 1455 1456 /* package */ boolean mClosedByUser = true; 1457 1458 @Override 1459 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 1460 mSelectionMode = mode; 1461 1462 MenuInflater inflater = getActivity().getMenuInflater(); 1463 inflater.inflate(R.menu.message_list_fragment_cab_options, menu); 1464 mMarkRead = menu.findItem(R.id.mark_read); 1465 mMarkUnread = menu.findItem(R.id.mark_unread); 1466 mAddStar = menu.findItem(R.id.add_star); 1467 mRemoveStar = menu.findItem(R.id.remove_star); 1468 mMove = menu.findItem(R.id.move); 1469 return true; 1470 } 1471 1472 @Override 1473 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 1474 int num = getSelectedCount(); 1475 // Set title -- "# selected" 1476 mSelectionMode.setTitle(getActivity().getResources().getQuantityString( 1477 R.plurals.message_view_selected_message_count, num, num)); 1478 1479 // Show appropriate menu items. 1480 boolean nonStarExists = doesSelectionContainNonStarredMessage(); 1481 boolean readExists = doesSelectionContainReadMessage(); 1482 mMarkRead.setVisible(!readExists); 1483 mMarkUnread.setVisible(readExists); 1484 mAddStar.setVisible(nonStarExists); 1485 mRemoveStar.setVisible(!nonStarExists); 1486 mMove.setVisible(mShowMoveCommand); 1487 return true; 1488 } 1489 1490 @Override 1491 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 1492 Set<Long> selectedConversations = mListAdapter.getSelectedSet(); 1493 if (selectedConversations.isEmpty()) return true; 1494 switch (item.getItemId()) { 1495 case R.id.mark_read: 1496 // Note - marking as read does not trigger auto-advance. 1497 toggleRead(selectedConversations); 1498 break; 1499 case R.id.mark_unread: 1500 mCallback.onAdvancingOpAccepted(selectedConversations); 1501 toggleRead(selectedConversations); 1502 break; 1503 case R.id.add_star: 1504 case R.id.remove_star: 1505 // TODO: removing a star can be a destructive command and cause auto-advance 1506 // if the current mailbox shown is favorites. 1507 toggleFavorite(selectedConversations); 1508 break; 1509 case R.id.delete: 1510 mCallback.onAdvancingOpAccepted(selectedConversations); 1511 deleteMessages(selectedConversations); 1512 break; 1513 case R.id.move: 1514 showMoveMessagesDialog(selectedConversations); 1515 break; 1516 } 1517 return true; 1518 } 1519 1520 @Override 1521 public void onDestroyActionMode(ActionMode mode) { 1522 // Clear this before onDeselectAll() to prevent onDeselectAll() from trying to close the 1523 // contextual mode again. 1524 mSelectionMode = null; 1525 if (mClosedByUser) { 1526 // Clear selection, only when the contextual mode is explicitly closed by the user. 1527 // 1528 // We close the contextual mode when the fragment becomes temporary invisible 1529 // (i.e. mIsVisible == false) too, in which case we want to keep the selection. 1530 onDeselectAll(); 1531 } 1532 } 1533 } 1534 1535 private class RefreshListener implements RefreshManager.Listener { 1536 @Override 1537 public void onMessagingError(long accountId, long mailboxId, String message) { 1538 } 1539 1540 @Override 1541 public void onRefreshStatusChanged(long accountId, long mailboxId) { 1542 updateListFooter(); 1543 } 1544 } 1545 1546 /** 1547 * Highlight the selected message. 1548 */ 1549 private void highlightSelectedMessage(boolean ensureSelectionVisible) { 1550 if (!isViewCreated()) { 1551 return; 1552 } 1553 1554 final ListView lv = getListView(); 1555 if (mSelectedMessageId == -1) { 1556 // No message selected 1557 lv.clearChoices(); 1558 return; 1559 } 1560 1561 final int count = lv.getCount(); 1562 for (int i = 0; i < count; i++) { 1563 if (lv.getItemIdAtPosition(i) != mSelectedMessageId) { 1564 continue; 1565 } 1566 lv.setItemChecked(i, true); 1567 if (ensureSelectionVisible) { 1568 Utility.listViewSmoothScrollToPosition(getActivity(), lv, i); 1569 } 1570 break; 1571 } 1572 } 1573 } 1574