MessageListFragment.java revision e357f5879187124c7af5c2ece5d7d3e4f60f07d2
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.email.activity; 18 19import com.android.email.Controller; 20import com.android.email.Email; 21import com.android.email.R; 22import com.android.email.RefreshManager; 23import com.android.email.Utility; 24import com.android.email.data.MailboxAccountLoader; 25import com.android.email.provider.EmailContent; 26import com.android.email.provider.EmailContent.Account; 27import com.android.email.provider.EmailContent.Mailbox; 28import com.android.email.service.MailService; 29 30import android.app.Activity; 31import android.app.ListFragment; 32import android.app.LoaderManager; 33import android.content.Context; 34import android.content.Loader; 35import android.database.Cursor; 36import android.os.Bundle; 37import android.os.Handler; 38import android.util.Log; 39import android.view.ActionMode; 40import android.view.LayoutInflater; 41import android.view.Menu; 42import android.view.MenuInflater; 43import android.view.MenuItem; 44import android.view.View; 45import android.view.View.OnClickListener; 46import android.view.ViewGroup; 47import android.widget.AdapterView; 48import android.widget.AdapterView.OnItemClickListener; 49import android.widget.AdapterView.OnItemLongClickListener; 50import android.widget.Button; 51import android.widget.ListView; 52import android.widget.TextView; 53import android.widget.Toast; 54 55import java.security.InvalidParameterException; 56import java.util.HashSet; 57import java.util.Set; 58 59// TODO Better handling of restoring list position/adapter check status 60/** 61 * Message list. 62 * 63 * <p>This fragment uses two different loaders to load data. 64 * <ul> 65 * <li>One to load {@link Account} and {@link Mailbox}, with {@link MailboxAccountLoader}. 66 * <li>The other to actually load messages. 67 * </ul> 68 * We run them sequentially. i.e. First starts {@link MailboxAccountLoader}, and when it finishes 69 * starts the other. 70 * 71 * TODO Add "send all messages" button to outboxes 72 */ 73public class MessageListFragment extends ListFragment 74 implements OnItemClickListener, OnItemLongClickListener, MessagesAdapter.Callback, 75 OnClickListener { 76 private static final String BUNDLE_LIST_STATE = "MessageListFragment.state.listState"; 77 78 private static final int LOADER_ID_MAILBOX_LOADER = 1; 79 private static final int LOADER_ID_MESSAGES_LOADER = 2; 80 81 // UI Support 82 private Activity mActivity; 83 private Callback mCallback = EmptyCallback.INSTANCE; 84 85 private View mListFooterView; 86 private TextView mListFooterText; 87 private View mListFooterProgress; 88 private View mSendPanel; 89 90 private static final int LIST_FOOTER_MODE_NONE = 0; 91 private static final int LIST_FOOTER_MODE_MORE = 1; 92 private int mListFooterMode; 93 94 private MessagesAdapter mListAdapter; 95 96 private long mMailboxId = -1; 97 private long mLastLoadedMailboxId = -1; 98 private Account mAccount; 99 private Mailbox mMailbox; 100 private boolean mIsEasAccount; 101 private boolean mIsRefreshable; 102 103 // Controller access 104 private Controller mController; 105 private RefreshManager mRefreshManager; 106 private RefreshListener mRefreshListener = new RefreshListener(); 107 108 // Misc members 109 private boolean mDoAutoRefresh; 110 111 /** true between {@link #onResume} and {@link #onPause}. */ 112 private boolean mResumed; 113 114 /** 115 * {@link ActionMode} shown when 1 or more message is selected. 116 */ 117 private ActionMode mSelectionMode; 118 119 private Utility.ListStateSaver mRestoredListState; 120 121 /** 122 * Callback interface that owning activities must implement 123 */ 124 public interface Callback { 125 public static final int TYPE_REGULAR = 0; 126 public static final int TYPE_DRAFT = 1; 127 public static final int TYPE_TRASH = 2; 128 129 /** 130 * Called when the specified mailbox does not exist. 131 */ 132 public void onMailboxNotFound(); 133 134 /** 135 * Called when the user wants to open a message. 136 * Note {@code mailboxId} is of the actual mailbox of the message, which is different from 137 * {@link MessageListFragment#getMailboxId} if it's magic mailboxes. 138 * 139 * @param messageId the message ID of the message 140 * @param messageMailboxId the mailbox ID of the message. 141 * This will never take values like {@link Mailbox#QUERY_ALL_INBOXES}. 142 * @param listMailboxId the mailbox ID of the listbox shown on this fragment. 143 * This can be that of a magic mailbox, e.g. {@link Mailbox#QUERY_ALL_INBOXES}. 144 * @param type {@link #TYPE_REGULAR}, {@link #TYPE_DRAFT} or {@link #TYPE_TRASH}. 145 */ 146 public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId, 147 int type); 148 } 149 150 private static final class EmptyCallback implements Callback { 151 public static final Callback INSTANCE = new EmptyCallback(); 152 153 @Override 154 public void onMailboxNotFound() { 155 } 156 @Override 157 public void onMessageOpen( 158 long messageId, long messageMailboxId, long listMailboxId, int type) { 159 } 160 } 161 162 @Override 163 public void onCreate(Bundle savedInstanceState) { 164 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 165 Log.d(Email.LOG_TAG, "MessageListFragment onCreate"); 166 } 167 super.onCreate(savedInstanceState); 168 mActivity = getActivity(); 169 mController = Controller.getInstance(mActivity); 170 mRefreshManager = RefreshManager.getInstance(mActivity); 171 mRefreshManager.registerListener(mRefreshListener); 172 } 173 174 @Override 175 public View onCreateView( 176 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 177 // Use a custom layout, which includes the original layout with "send messages" panel. 178 View root = inflater.inflate(R.layout.message_list_fragment,null); 179 mSendPanel = root.findViewById(R.id.send_panel); 180 ((Button) mSendPanel.findViewById(R.id.send_messages)).setOnClickListener(this); 181 return root; 182 } 183 184 @Override 185 public void onActivityCreated(Bundle savedInstanceState) { 186 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 187 Log.d(Email.LOG_TAG, "MessageListFragment onActivityCreated"); 188 } 189 super.onActivityCreated(savedInstanceState); 190 191 ListView listView = getListView(); 192 listView.setOnItemClickListener(this); 193 listView.setOnItemLongClickListener(this); 194 listView.setItemsCanFocus(false); 195 196 mListAdapter = new MessagesAdapter(mActivity, this); 197 198 mListFooterView = getActivity().getLayoutInflater().inflate( 199 R.layout.message_list_item_footer, listView, false); 200 201 if (savedInstanceState != null) { 202 // Fragment doesn't have this method. Call it manually. 203 loadState(savedInstanceState); 204 } 205 } 206 207 @Override 208 public void onStart() { 209 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 210 Log.d(Email.LOG_TAG, "MessageListFragment onStart"); 211 } 212 super.onStart(); 213 } 214 215 @Override 216 public void onResume() { 217 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 218 Log.d(Email.LOG_TAG, "MessageListFragment onResume"); 219 } 220 super.onResume(); 221 mResumed = true; 222 if (mMailboxId != -1) { 223 startLoading(); 224 } 225 } 226 227 @Override 228 public void onPause() { 229 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 230 Log.d(Email.LOG_TAG, "MessageListFragment onPause"); 231 } 232 mResumed = false; 233 super.onStop(); 234 } 235 236 @Override 237 public void onStop() { 238 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 239 Log.d(Email.LOG_TAG, "MessageListFragment onStop"); 240 } 241 super.onStop(); 242 } 243 244 @Override 245 public void onDestroy() { 246 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 247 Log.d(Email.LOG_TAG, "MessageListFragment onDestroy"); 248 } 249 mRefreshManager.unregisterListener(mRefreshListener); 250 super.onDestroy(); 251 } 252 253 @Override 254 public void onSaveInstanceState(Bundle outState) { 255 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 256 Log.d(Email.LOG_TAG, "MessageListFragment onSaveInstanceState"); 257 } 258 super.onSaveInstanceState(outState); 259 mListAdapter.onSaveInstanceState(outState); 260 outState.putParcelable(BUNDLE_LIST_STATE, new Utility.ListStateSaver(getListView())); 261 } 262 263 // Unit tests use it 264 /* package */void loadState(Bundle savedInstanceState) { 265 mListAdapter.loadState(savedInstanceState); 266 mRestoredListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE); 267 } 268 269 public void setCallback(Callback callback) { 270 mCallback = (callback != null) ? callback : EmptyCallback.INSTANCE; 271 } 272 273 /** 274 * Called by an Activity to open an mailbox. 275 * 276 * @param mailboxId the ID of a mailbox, or one of "special" mailbox IDs like 277 * {@link Mailbox#QUERY_ALL_INBOXES}. -1 is not allowed. 278 */ 279 public void openMailbox(long mailboxId) { 280 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 281 Log.d(Email.LOG_TAG, "MessageListFragment openMailbox"); 282 } 283 if (mailboxId == -1) { 284 throw new InvalidParameterException(); 285 } 286 if (mMailboxId == mailboxId) { 287 return; 288 } 289 290 mMailboxId = mailboxId; 291 292 onDeselectAll(); 293 if (mResumed) { 294 startLoading(); 295 } 296 } 297 298 /* package */MessagesAdapter getAdapterForTest() { 299 return mListAdapter; 300 } 301 302 /** 303 * @return the account id or -1 if it's unknown yet. It's also -1 if it's a magic mailbox. 304 */ 305 public long getAccountId() { 306 return (mMailbox == null) ? -1 : mMailbox.mAccountKey; 307 } 308 309 /** 310 * @return the mailbox id, which is the value set to {@link #openMailbox}. 311 * (Meaning it will never return -1, but may return special values, 312 * eg {@link Mailbox#QUERY_ALL_INBOXES}). 313 */ 314 public long getMailboxId() { 315 return mMailboxId; 316 } 317 318 /** 319 * @return true if the mailbox is a "special" box. (e.g. combined inbox, all starred, etc.) 320 */ 321 public boolean isMagicMailbox() { 322 return mMailboxId < 0; 323 } 324 325 /** 326 * @return the number of messages that are currently selecteed. 327 */ 328 public int getSelectedCount() { 329 return mListAdapter.getSelectedSet().size(); 330 } 331 332 /** 333 * @return true if the list is in the "selection" mode. 334 */ 335 private boolean isInSelectionMode() { 336 return mSelectionMode != null; 337 } 338 339 @Override 340 public void onClick(View v) { 341 switch (v.getId()) { 342 case R.id.send_messages: 343 onSendPendingMessages(); 344 break; 345 } 346 } 347 348 /** 349 * Called when a message is clicked. 350 */ 351 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 352 if (view != mListFooterView) { 353 MessageListItem itemView = (MessageListItem) view; 354 if (isInSelectionMode()) { 355 toggleSelection(itemView); 356 } else { 357 onMessageOpen(itemView.mMailboxId, id); 358 } 359 } else { 360 doFooterClick(); 361 } 362 } 363 364 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { 365 if (view != mListFooterView) { 366 if (isInSelectionMode()) { 367 // Already in selection mode. Ignore. 368 } else { 369 toggleSelection((MessageListItem) view); 370 return true; 371 } 372 } 373 return false; 374 } 375 376 private void toggleSelection(MessageListItem itemView) { 377 mListAdapter.toggleSelected(itemView); 378 } 379 380 private void onMessageOpen(final long mailboxId, final long messageId) { 381 final int type; 382 if (mMailbox == null) { // Magic mailbox 383 if (mMailboxId == Mailbox.QUERY_ALL_DRAFTS) { 384 type = Callback.TYPE_DRAFT; 385 } else { 386 type = Callback.TYPE_REGULAR; 387 } 388 } else { 389 switch (mMailbox.mType) { 390 case EmailContent.Mailbox.TYPE_DRAFTS: 391 type = Callback.TYPE_DRAFT; 392 break; 393 case EmailContent.Mailbox.TYPE_TRASH: 394 type = Callback.TYPE_TRASH; 395 break; 396 default: 397 type = Callback.TYPE_REGULAR; 398 break; 399 } 400 } 401 mCallback.onMessageOpen(messageId, mailboxId, getMailboxId(), type); 402 } 403 404 public void onMultiToggleRead() { 405 onMultiToggleRead(mListAdapter.getSelectedSet()); 406 } 407 408 public void onMultiToggleFavorite() { 409 onMultiToggleFavorite(mListAdapter.getSelectedSet()); 410 } 411 412 public void onMultiDelete() { 413 onMultiDelete(mListAdapter.getSelectedSet()); 414 } 415 416 /** 417 * Refresh the list. NOOP for special mailboxes (e.g. combined inbox). 418 * 419 * Note: Manual refresh is enabled even for push accounts. 420 */ 421 public void onRefresh() { 422 if (!mIsRefreshable) { 423 return; 424 } 425 long accountId = getAccountId(); 426 if (accountId != -1) { 427 mRefreshManager.refreshMessageList(accountId, mMailboxId); 428 } 429 } 430 431 public void onDeselectAll() { 432 if ((mListAdapter == null) || (mListAdapter.getSelectedSet().size() == 0)) { 433 return; 434 } 435 mListAdapter.getSelectedSet().clear(); 436 getListView().invalidateViews(); 437 finishSelectionMode(); 438 } 439 440 /** 441 * Load more messages. NOOP for special mailboxes (e.g. combined inbox). 442 */ 443 private void onLoadMoreMessages() { 444 long accountId = getAccountId(); 445 if (accountId != -1) { 446 mRefreshManager.loadMoreMessages(accountId, mMailboxId); 447 } 448 } 449 450 /** 451 * @return if it's an outbox or "all outboxes". 452 * 453 * TODO make it private. It's only used by MessageList, but the callsite is obsolete. 454 */ 455 public boolean isOutbox() { 456 return (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX) 457 || ((mMailbox != null) && (mMailbox.mType == Mailbox.TYPE_OUTBOX)); 458 } 459 460 public void onSendPendingMessages() { 461 RefreshManager rm = RefreshManager.getInstance(mActivity); 462 if (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX) { 463 rm.sendPendingMessagesForAllAccounts(); 464 } else if (mMailbox != null) { // Magic boxes don't have a specific account id. 465 rm.sendPendingMessages(mMailbox.mId); 466 } 467 } 468 469 private void onSetMessageRead(long messageId, boolean newRead) { 470 mController.setMessageRead(messageId, newRead); 471 } 472 473 private void onSetMessageFavorite(long messageId, boolean newFavorite) { 474 mController.setMessageFavorite(messageId, newFavorite); 475 } 476 477 /** 478 * Toggles a set read/unread states. Note, the default behavior is "mark unread", so the 479 * sense of the helper methods is "true=unread". 480 * 481 * @param selectedSet The current list of selected items 482 */ 483 private void onMultiToggleRead(Set<Long> selectedSet) { 484 toggleMultiple(selectedSet, new MultiToggleHelper() { 485 486 public boolean getField(long messageId, Cursor c) { 487 return c.getInt(MessagesAdapter.COLUMN_READ) == 0; 488 } 489 490 public boolean setField(long messageId, Cursor c, boolean newValue) { 491 boolean oldValue = getField(messageId, c); 492 if (oldValue != newValue) { 493 onSetMessageRead(messageId, !newValue); 494 return true; 495 } 496 return false; 497 } 498 }); 499 } 500 501 /** 502 * Toggles a set of favorites (stars) 503 * 504 * @param selectedSet The current list of selected items 505 */ 506 private void onMultiToggleFavorite(Set<Long> selectedSet) { 507 toggleMultiple(selectedSet, new MultiToggleHelper() { 508 509 public boolean getField(long messageId, Cursor c) { 510 return c.getInt(MessagesAdapter.COLUMN_FAVORITE) != 0; 511 } 512 513 public boolean setField(long messageId, Cursor c, boolean newValue) { 514 boolean oldValue = getField(messageId, c); 515 if (oldValue != newValue) { 516 onSetMessageFavorite(messageId, newValue); 517 return true; 518 } 519 return false; 520 } 521 }); 522 } 523 524 private void onMultiDelete(Set<Long> selectedSet) { 525 // Clone the set, because deleting is going to thrash things 526 HashSet<Long> cloneSet = new HashSet<Long>(selectedSet); 527 for (Long id : cloneSet) { 528 mController.deleteMessage(id, -1); 529 } 530 Toast.makeText(mActivity, mActivity.getResources().getQuantityString( 531 R.plurals.message_deleted_toast, cloneSet.size()), Toast.LENGTH_SHORT).show(); 532 selectedSet.clear(); 533 // Message deletion is async... Can't refresh the list immediately. 534 } 535 536 private interface MultiToggleHelper { 537 /** 538 * Return true if the field of interest is "set". If one or more are false, then our 539 * bulk action will be to "set". If all are set, our bulk action will be to "clear". 540 * @param messageId the message id of the current message 541 * @param c the cursor, positioned to the item of interest 542 * @return true if the field at this row is "set" 543 */ 544 public boolean getField(long messageId, Cursor c); 545 546 /** 547 * Set or clear the field of interest. Return true if a change was made. 548 * @param messageId the message id of the current message 549 * @param c the cursor, positioned to the item of interest 550 * @param newValue the new value to be set at this row 551 * @return true if a change was actually made 552 */ 553 public boolean setField(long messageId, Cursor c, boolean newValue); 554 } 555 556 /** 557 * Toggle multiple fields in a message, using the following logic: If one or more fields 558 * are "clear", then "set" them. If all fields are "set", then "clear" them all. 559 * 560 * @param selectedSet the set of messages that are selected 561 * @param helper functions to implement the specific getter & setter 562 * @return the number of messages that were updated 563 */ 564 private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) { 565 Cursor c = mListAdapter.getCursor(); 566 boolean anyWereFound = false; 567 boolean allWereSet = true; 568 569 c.moveToPosition(-1); 570 while (c.moveToNext()) { 571 long id = c.getInt(MessagesAdapter.COLUMN_ID); 572 if (selectedSet.contains(Long.valueOf(id))) { 573 anyWereFound = true; 574 if (!helper.getField(id, c)) { 575 allWereSet = false; 576 break; 577 } 578 } 579 } 580 581 int numChanged = 0; 582 583 if (anyWereFound) { 584 boolean newValue = !allWereSet; 585 c.moveToPosition(-1); 586 while (c.moveToNext()) { 587 long id = c.getInt(MessagesAdapter.COLUMN_ID); 588 if (selectedSet.contains(Long.valueOf(id))) { 589 if (helper.setField(id, c, newValue)) { 590 ++numChanged; 591 } 592 } 593 } 594 } 595 596 refreshList(); 597 598 return numChanged; 599 } 600 601 /** 602 * Test selected messages for showing appropriate labels 603 * @param selectedSet 604 * @param column_id 605 * @param defaultflag 606 * @return true when the specified flagged message is selected 607 */ 608 private boolean testMultiple(Set<Long> selectedSet, int column_id, boolean defaultflag) { 609 Cursor c = mListAdapter.getCursor(); 610 if (c == null || c.isClosed()) { 611 return false; 612 } 613 c.moveToPosition(-1); 614 while (c.moveToNext()) { 615 long id = c.getInt(MessagesAdapter.COLUMN_ID); 616 if (selectedSet.contains(Long.valueOf(id))) { 617 if (c.getInt(column_id) == (defaultflag ? 1 : 0)) { 618 return true; 619 } 620 } 621 } 622 return false; 623 } 624 625 /** 626 * @return true if one or more non-starred messages are selected. 627 */ 628 public boolean doesSelectionContainNonStarredMessage() { 629 return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_FAVORITE, 630 false); 631 } 632 633 /** 634 * @return true if one or more read messages are selected. 635 */ 636 public boolean doesSelectionContainReadMessage() { 637 return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_READ, true); 638 } 639 640 /** 641 * Called by activity to indicate that the user explicitly opened the 642 * mailbox and it needs auto-refresh when it's first shown. TODO: 643 * {@link MessageList} needs to call this as well. 644 * 645 * TODO It's a bit ugly. We can remove this if this fragment "remembers" the current mailbox ID 646 * through configuration changes. 647 */ 648 public void doAutoRefresh() { 649 mDoAutoRefresh = true; 650 } 651 652 /** 653 * Implements a timed refresh of "stale" mailboxes. This should only happen when 654 * multiple conditions are true, including: 655 * Only refreshable mailboxes. 656 * Only when the user explicitly opens the mailbox (not onResume, for example) 657 * Only for real, non-push mailboxes (c.f. manual refresh is still enabled for push accounts) 658 * Only when the mailbox is "stale" (currently set to 5 minutes since last refresh) 659 */ 660 private void autoRefreshStaleMailbox() { 661 if (!mDoAutoRefresh // Not explicitly open 662 || mIsRefreshable // Not refreshable (special box such as drafts, or magic boxes) 663 || (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) // Not push 664 ) { 665 return; 666 } 667 mDoAutoRefresh = false; 668 if (!mRefreshManager.isMailboxStale(mMailboxId)) { 669 return; 670 } 671 onRefresh(); 672 } 673 674 /** Implements {@link MessagesAdapter.Callback} */ 675 @Override 676 public void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite) { 677 onSetMessageFavorite(itemView.mMessageId, newFavorite); 678 } 679 680 /** Implements {@link MessagesAdapter.Callback} */ 681 @Override 682 public void onAdapterSelectedChanged( 683 MessageListItem itemView, boolean newSelected, int mSelectedCount) { 684 updateSelectionMode(); 685 } 686 687 private void determineFooterMode() { 688 mListFooterMode = LIST_FOOTER_MODE_NONE; 689 if ((mMailbox == null) || (mMailbox.mType == Mailbox.TYPE_OUTBOX) 690 || (mMailbox.mType == Mailbox.TYPE_DRAFTS)) { 691 return; // No footer 692 } 693 if (!mIsEasAccount) { 694 // IMAP, POP has "load more" 695 mListFooterMode = LIST_FOOTER_MODE_MORE; 696 } 697 } 698 699 private void addFooterView() { 700 ListView lv = getListView(); 701 if (mListFooterView != null) { 702 lv.removeFooterView(mListFooterView); 703 } 704 determineFooterMode(); 705 if (mListFooterMode != LIST_FOOTER_MODE_NONE) { 706 707 lv.addFooterView(mListFooterView); 708 lv.setAdapter(mListAdapter); 709 710 mListFooterProgress = mListFooterView.findViewById(R.id.progress); 711 mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text); 712 713 updateListFooter(); 714 } 715 } 716 717 /** 718 * Set the list footer text based on mode and the current "network active" status 719 */ 720 private void updateListFooter() { 721 if (mListFooterMode != LIST_FOOTER_MODE_NONE) { 722 int footerTextId = 0; 723 switch (mListFooterMode) { 724 case LIST_FOOTER_MODE_MORE: 725 boolean active = mRefreshManager.isMessageListRefreshing(mMailboxId); 726 footerTextId = active ? R.string.status_loading_messages 727 : R.string.message_list_load_more_messages_action; 728 mListFooterProgress.setVisibility(active ? View.VISIBLE : View.GONE); 729 break; 730 } 731 mListFooterText.setText(footerTextId); 732 } 733 } 734 735 /** 736 * Handle a click in the list footer, which changes meaning depending on what we're looking at. 737 */ 738 private void doFooterClick() { 739 switch (mListFooterMode) { 740 case LIST_FOOTER_MODE_NONE: // should never happen 741 break; 742 case LIST_FOOTER_MODE_MORE: 743 onLoadMoreMessages(); 744 break; 745 } 746 } 747 748 private void hideSendPanel() { 749 mSendPanel.setVisibility(View.GONE); 750 } 751 752 private void showSendPanelIfNecessary() { 753 final boolean show = 754 isOutbox() 755 && (mListAdapter != null) 756 && (mListAdapter.getCount() > 0); 757 mSendPanel.setVisibility(show ? View.VISIBLE : View.GONE); 758 } 759 760 private void startLoading() { 761 // Clear the list. (ListFragment will show the "Loading" animation) 762 setListAdapter(null); 763 setListShown(false); 764 hideSendPanel(); 765 766 // Start loading... 767 final LoaderManager lm = getLoaderManager(); 768 769 // If we're loading a different mailbox, discard the previous result. 770 if ((mLastLoadedMailboxId != -1) && (mLastLoadedMailboxId != mMailboxId)) { 771 lm.stopLoader(LOADER_ID_MAILBOX_LOADER); 772 lm.stopLoader(LOADER_ID_MESSAGES_LOADER); 773 } 774 lm.initLoader(LOADER_ID_MAILBOX_LOADER, null, new MailboxAccountLoaderCallback()); 775 } 776 777 /** 778 * Loader callbacks for {@link MailboxAccountLoader}. 779 */ 780 private class MailboxAccountLoaderCallback implements LoaderManager.LoaderCallbacks< 781 MailboxAccountLoader.Result> { 782 @Override 783 public Loader<MailboxAccountLoader.Result> onCreateLoader(int id, Bundle args) { 784 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 785 Log.d(Email.LOG_TAG, 786 "MessageListFragment onCreateLoader(mailbox) mailboxId=" + mMailboxId); 787 } 788 return new MailboxAccountLoader(getActivity().getApplicationContext(), mMailboxId); 789 } 790 791 @Override 792 public void onLoadFinished(Loader<MailboxAccountLoader.Result> loader, 793 MailboxAccountLoader.Result result) { 794 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 795 Log.d(Email.LOG_TAG, "MessageListFragment onLoadFinished(mailbox) mailboxId=" 796 + mMailboxId); 797 } 798 if (!result.mIsFound) { 799 mCallback.onMailboxNotFound(); 800 return; 801 } 802 803 mLastLoadedMailboxId = mMailboxId; 804 mAccount = result.mAccount; 805 mMailbox = result.mMailbox; 806 mIsEasAccount = result.mIsEasAccount; 807 mIsRefreshable = result.mIsRefreshable; 808 getLoaderManager().initLoader(LOADER_ID_MESSAGES_LOADER, null, 809 new MessagesLoaderCallback()); 810 } 811 } 812 813 /** 814 * Reload the data and refresh the list view. 815 */ 816 private void refreshList() { 817 getLoaderManager().restartLoader(LOADER_ID_MESSAGES_LOADER, null, 818 new MessagesLoaderCallback()); 819 } 820 821 /** 822 * Loader callbacks for message list. 823 */ 824 private class MessagesLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> { 825 @Override 826 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 827 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 828 Log.d(Email.LOG_TAG, 829 "MessageListFragment onCreateLoader(messages) mailboxId=" + mMailboxId); 830 } 831 832 // Reset new message count. 833 // TODO Do it in onLoadFinished(). Unfortunately 834 // resetNewMessageCount() ends up a 835 // db operation, which causes a onContentChanged notification, which 836 // makes cursor 837 // loaders to requery. Until we fix ContentProvider (don't notify 838 // unrelated cursors) 839 // we need to do it here. 840 resetNewMessageCount(mActivity, mMailboxId, getAccountId()); 841 return MessagesAdapter.createLoader(getActivity(), mMailboxId); 842 } 843 844 @Override 845 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 846 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 847 Log.d(Email.LOG_TAG, 848 "MessageListFragment onLoadFinished(messages) mailboxId=" + mMailboxId); 849 } 850 851 // Save list view state (primarily scroll position) 852 final ListView lv = getListView(); 853 final Utility.ListStateSaver lss; 854 if (mRestoredListState != null) { 855 lss = mRestoredListState; 856 mRestoredListState = null; 857 } else { 858 lss = new Utility.ListStateSaver(lv); 859 } 860 861 // Update the list 862 mListAdapter.changeCursor(cursor); 863 setListAdapter(mListAdapter); 864 setListShown(true); 865 866 // Various post processing... 867 // (resetNewMessageCount should be here. See above.) 868 autoRefreshStaleMailbox(); 869 addFooterView(); 870 updateSelectionMode(); 871 showSendPanelIfNecessary(); 872 873 // Restore the state -- it has to be the last. 874 // (Some of the "post processing" resets the state.) 875 lss.restore(lv); 876 } 877 } 878 879 /** 880 * Reset the "new message" count. 881 * <ul> 882 * <li>If {@code mailboxId} is {@link Mailbox#QUERY_ALL_INBOXES}, reset the 883 * counts of all accounts. 884 * <li>If {@code mailboxId} is not of a magic inbox (i.e. >= 0) and {@code 885 * accountId} is valid, reset the count of the specified account. 886 * </ul> 887 */ 888 /* protected */static void resetNewMessageCount( 889 Context context, long mailboxId, long accountId) { 890 if (mailboxId == Mailbox.QUERY_ALL_INBOXES) { 891 MailService.resetNewMessageCount(context, -1); 892 } else if (mailboxId >= 0 && accountId != -1) { 893 MailService.resetNewMessageCount(context, accountId); 894 } 895 } 896 897 /** 898 * Show/hide the "selection" action mode, according to the number of selected messages, 899 * and update the content (title and menus) if necessary. 900 */ 901 public void updateSelectionMode() { 902 final int numSelected = getSelectedCount(); 903 if (numSelected == 0) { 904 finishSelectionMode(); 905 return; 906 } 907 if (isInSelectionMode()) { 908 updateSelectionModeView(); 909 } else { 910 getActivity().startActionMode(new SelectionModeCallback()); 911 } 912 } 913 914 /** Finish the "selection" action mode */ 915 private void finishSelectionMode() { 916 if (isInSelectionMode()) { 917 mSelectionMode.finish(); 918 mSelectionMode = null; 919 } 920 } 921 922 /** Update the "selection" action mode bar */ 923 private void updateSelectionModeView() { 924 mSelectionMode.invalidate(); 925 } 926 927 private class SelectionModeCallback implements ActionMode.Callback { 928 private MenuItem mMarkRead; 929 private MenuItem mMarkUnread; 930 private MenuItem mAddStar; 931 private MenuItem mRemoveStar; 932 933 @Override 934 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 935 mSelectionMode = mode; 936 937 MenuInflater inflater = getActivity().getMenuInflater(); 938 inflater.inflate(R.menu.message_list_selection_mode, menu); 939 mMarkRead = menu.findItem(R.id.mark_read); 940 mMarkUnread = menu.findItem(R.id.mark_unread); 941 mAddStar = menu.findItem(R.id.add_star); 942 mRemoveStar = menu.findItem(R.id.remove_star); 943 return true; 944 } 945 946 @Override 947 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 948 int num = getSelectedCount(); 949 // Set title -- "# selected" 950 mSelectionMode.setTitle(getActivity().getResources().getQuantityString( 951 R.plurals.message_view_selected_message_count, num, num)); 952 953 // Show appropriate menu items. 954 boolean nonStarExists = doesSelectionContainNonStarredMessage(); 955 boolean readExists = doesSelectionContainReadMessage(); 956 mMarkRead.setVisible(!readExists); 957 mMarkUnread.setVisible(readExists); 958 mAddStar.setVisible(nonStarExists); 959 mRemoveStar.setVisible(!nonStarExists); 960 return true; 961 } 962 963 @Override 964 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 965 switch (item.getItemId()) { 966 case R.id.mark_read: 967 case R.id.mark_unread: 968 onMultiToggleRead(); 969 break; 970 case R.id.add_star: 971 case R.id.remove_star: 972 onMultiToggleFavorite(); 973 break; 974 case R.id.delete: 975 onMultiDelete(); 976 break; 977 } 978 return true; 979 } 980 981 @Override 982 public void onDestroyActionMode(ActionMode mode) { 983 onDeselectAll(); 984 mSelectionMode = null; 985 } 986 } 987 988 private class RefreshListener implements RefreshManager.Listener { 989 @Override 990 public void onMessagingError(long accountId, long mailboxId, String message) { 991 } 992 993 @Override 994 public void onRefreshStatusChanged(long accountId, long mailboxId) { 995 updateListFooter(); 996 } 997 } 998}