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