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