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