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