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