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