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