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