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