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