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