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