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