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