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