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