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