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