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