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