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