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