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