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