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