MessageListFragment.java revision d632426c0a05ac7f1e8db08cc0b2e21a261771f4
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.email.activity; 18 19import com.android.email.Controller; 20import com.android.email.Email; 21import com.android.email.R; 22import com.android.email.Utility; 23import com.android.email.provider.EmailContent; 24import com.android.email.provider.EmailContent.Account; 25import com.android.email.provider.EmailContent.Mailbox; 26import com.android.email.provider.EmailContent.MailboxColumns; 27import com.android.email.provider.EmailContent.MessageColumns; 28import com.android.email.service.MailService; 29 30import android.app.Activity; 31import android.app.ListFragment; 32import android.content.ContentResolver; 33import android.content.ContentUris; 34import android.content.Context; 35import android.database.Cursor; 36import android.net.Uri; 37import android.os.AsyncTask; 38import android.os.Bundle; 39import android.os.Handler; 40import android.util.Log; 41import android.view.View; 42import android.widget.AdapterView; 43import android.widget.AdapterView.OnItemClickListener; 44import android.widget.AdapterView.OnItemLongClickListener; 45import android.widget.ListView; 46import android.widget.TextView; 47import android.widget.Toast; 48 49import java.security.InvalidParameterException; 50import java.util.HashSet; 51import java.util.Set; 52 53public class MessageListFragment extends ListFragment implements OnItemClickListener, 54 OnItemLongClickListener, MessagesAdapter.Callback { 55 private static final String STATE_SELECTED_ITEM_TOP = 56 "com.android.email.activity.MessageList.selectedItemTop"; 57 private static final String STATE_SELECTED_POSITION = 58 "com.android.email.activity.MessageList.selectedPosition"; 59 private static final String STATE_CHECKED_ITEMS = 60 "com.android.email.activity.MessageList.checkedItems"; 61 62 // UI Support 63 private Activity mActivity; 64 private Callback mCallback = EmptyCallback.INSTANCE; 65 private View mListFooterView; 66 private TextView mListFooterText; 67 private View mListFooterProgress; 68 69 private static final int LIST_FOOTER_MODE_NONE = 0; 70 private static final int LIST_FOOTER_MODE_REFRESH = 1; 71 private static final int LIST_FOOTER_MODE_MORE = 2; 72 private static final int LIST_FOOTER_MODE_SEND = 3; 73 private int mListFooterMode; 74 75 private MessagesAdapter mListAdapter; 76 77 // DB access 78 private ContentResolver mResolver; 79 private long mAccountId = -1; 80 private long mMailboxId = -1; 81 private LoadMessagesTask mLoadMessagesTask; 82 private SetFooterTask mSetFooterTask; 83 84 /* package */ static final String[] MESSAGE_PROJECTION = new String[] { 85 EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, 86 MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP, 87 MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT, 88 MessageColumns.FLAGS, 89 }; 90 91 // Controller access 92 private Controller mController; 93 94 // Misc members 95 private Boolean mPushModeMailbox = null; 96 private int mSavedItemTop = 0; 97 private int mSavedItemPosition = -1; 98 private int mFirstSelectedItemTop = 0; 99 private int mFirstSelectedItemPosition = -1; 100 private int mFirstSelectedItemHeight = -1; 101 private boolean mCanAutoRefresh; 102 103 private boolean mStarted; 104 105 /** 106 * Callback interface that owning activities must implement 107 */ 108 public interface Callback { 109 public static final int TYPE_REGULAR = 0; 110 public static final int TYPE_DRAFT = 1; 111 public static final int TYPE_TRASH = 2; 112 113 /** 114 * Called when selected messages have been changed. 115 */ 116 public void onSelectionChanged(); 117 118 /** 119 * Called when the specified mailbox does not exist. 120 */ 121 public void onMailboxNotFound(); 122 123 /** 124 * Called when the user wants to open a message. 125 * Note {@code mailboxId} is of the actual mailbox of the message, which is different from 126 * {@link MessageListFragment#getMailboxId} if it's magic mailboxes. 127 * 128 * @param messageId the message ID of the message 129 * @param messageMailboxId the mailbox ID of the message. 130 * This will never take values like {@link Mailbox#QUERY_ALL_INBOXES}. 131 * @param listMailboxId the mailbox ID of the listbox shown on this fragment. 132 * This can be that of a magic mailbox, e.g. {@link Mailbox#QUERY_ALL_INBOXES}. 133 * @param type {@link #TYPE_REGULAR}, {@link #TYPE_DRAFT} or {@link #TYPE_TRASH}. 134 */ 135 public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId, 136 int type); 137 } 138 139 private static final class EmptyCallback implements Callback { 140 public static final Callback INSTANCE = new EmptyCallback(); 141 142 @Override 143 public void onMailboxNotFound() { 144 } 145 @Override 146 public void onSelectionChanged() { 147 } 148 @Override 149 public void onMessageOpen( 150 long messageId, long messageMailboxId, long listMailboxId, int type) { 151 } 152 } 153 154 @Override 155 public void onCreate(Bundle savedInstanceState) { 156 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 157 Log.d(Email.LOG_TAG, "MessageListFragment onCreate"); 158 } 159 super.onCreate(savedInstanceState); 160 mActivity = getActivity(); 161 mResolver = mActivity.getContentResolver(); 162 mController = Controller.getInstance(mActivity); 163 mCanAutoRefresh = true; 164 } 165 166 @Override 167 public void onActivityCreated(Bundle savedInstanceState) { 168 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 169 Log.d(Email.LOG_TAG, "MessageListFragment onActivityCreated"); 170 } 171 super.onActivityCreated(savedInstanceState); 172 173 ListView listView = getListView(); 174 listView.setOnItemClickListener(this); 175 listView.setOnItemLongClickListener(this); 176 listView.setItemsCanFocus(false); 177 178 mListAdapter = new MessagesAdapter(mActivity, new Handler(), this); 179 180 mListFooterView = getActivity().getLayoutInflater().inflate( 181 R.layout.message_list_item_footer, listView, false); 182 183 // TODO extend this to properly deal with multiple mailboxes, cursor, etc. 184 185 if (savedInstanceState != null) { 186 // Fragment doesn't have this method. Call it manually. 187 loadState(savedInstanceState); 188 } 189 } 190 191 @Override 192 public void onStart() { 193 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 194 Log.d(Email.LOG_TAG, "MessageListFragment onStart"); 195 } 196 super.onStart(); 197 mStarted = true; 198 if (mMailboxId != -1) { 199 startLoading(); 200 } 201 } 202 203 @Override 204 public void onStop() { 205 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 206 Log.d(Email.LOG_TAG, "MessageListFragment onStop"); 207 } 208 mStarted = false; 209 super.onStop(); 210 } 211 212 @Override 213 public void onResume() { 214 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 215 Log.d(Email.LOG_TAG, "MessageListFragment onResume"); 216 } 217 super.onResume(); 218 restoreListPosition(); 219 autoRefreshStaleMailbox(); 220 } 221 222 @Override 223 public void onDestroy() { 224 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 225 Log.d(Email.LOG_TAG, "MessageListFragment onDestroy"); 226 } 227 Utility.cancelTaskInterrupt(mLoadMessagesTask); 228 mLoadMessagesTask = null; 229 Utility.cancelTaskInterrupt(mSetFooterTask); 230 mSetFooterTask = null; 231 232 if (mListAdapter != null) { 233 mListAdapter.changeCursor(null); 234 } 235 super.onDestroy(); 236 } 237 238 @Override 239 public void onSaveInstanceState(Bundle outState) { 240 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 241 Log.d(Email.LOG_TAG, "MessageListFragment onSaveInstanceState"); 242 } 243 super.onSaveInstanceState(outState); 244 saveListPosition(); 245 outState.putInt(STATE_SELECTED_POSITION, mSavedItemPosition); 246 outState.putInt(STATE_SELECTED_ITEM_TOP, mSavedItemTop); 247 Set<Long> checkedset = mListAdapter.getSelectedSet(); 248 long[] checkedarray = new long[checkedset.size()]; 249 int i = 0; 250 for (Long l : checkedset) { 251 checkedarray[i] = l; 252 i++; 253 } 254 outState.putLongArray(STATE_CHECKED_ITEMS, checkedarray); 255 } 256 257 // Unit tests use it 258 /* package */ void loadState(Bundle savedInstanceState) { 259 mSavedItemTop = savedInstanceState.getInt(STATE_SELECTED_ITEM_TOP, 0); 260 mSavedItemPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION, -1); 261 Set<Long> checkedset = mListAdapter.getSelectedSet(); 262 for (long l: savedInstanceState.getLongArray(STATE_CHECKED_ITEMS)) { 263 checkedset.add(l); 264 } 265 } 266 267 public void setCallback(Callback callback) { 268 mCallback = (callback != null) ? callback : EmptyCallback.INSTANCE; 269 } 270 271 /** 272 * Called by an Activity to open an mailbox. 273 * 274 * @param accountId account id of the mailbox, if already known. Pass -1 if unknown or 275 * {@code mailboxId} is of a special mailbox. If -1 is passed, this fragment will find it 276 * using {@code mailboxId}, which the activity can get later with {@link #getAccountId()}. 277 * Passing -1 is always safe, but we can skip a database lookup if specified. 278 * 279 * @param mailboxId the ID of a mailbox, or one of "special" mailbox IDs like 280 * {@link Mailbox#QUERY_ALL_INBOXES}. -1 is not allowed. 281 */ 282 public void openMailbox(long accountId, long mailboxId) { 283 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 284 Log.d(Email.LOG_TAG, "MessageListFragment openMailbox"); 285 } 286 if (mailboxId == -1) { 287 throw new InvalidParameterException(); 288 } 289 if ((mAccountId == accountId) && (mMailboxId == mailboxId)) { 290 return; 291 } 292 293 mAccountId = accountId; 294 mMailboxId = mailboxId; 295 296 if (mStarted) { 297 startLoading(); 298 } 299 } 300 301 private void startLoading() { 302 // Clear the list. (ListFragment will show the "Loading" animation) 303 getListView().removeFooterView(mListFooterView); 304 setListAdapter(null); 305 setListShown(false); 306 307 // Start loading... 308 Utility.cancelTaskInterrupt(mLoadMessagesTask); 309 mLoadMessagesTask = new LoadMessagesTask(mMailboxId, mAccountId); 310 mLoadMessagesTask.execute(); 311 } 312 313 /* package */ MessagesAdapter getAdapterForTest() { 314 return mListAdapter; 315 } 316 317 /** 318 * @return the account id or -1 if it's unknown yet. It's also -1 if it's a magic mailbox. 319 */ 320 public long getAccountId() { 321 return mAccountId; 322 } 323 324 /** 325 * @return the mailbox id, which is the value set to {@link #openMailbox(long, long)}. 326 * (Meaning it will never return -1, but may return special values, 327 * eg {@link Mailbox#QUERY_ALL_INBOXES}). 328 */ 329 public long getMailboxId() { 330 return mMailboxId; 331 } 332 333 /** 334 * @return true if the mailbox is a "special" box. (e.g. combined inbox, all starred, etc.) 335 */ 336 public boolean isMagicMailbox() { 337 return mMailboxId < 0; 338 } 339 340 /** 341 * @return if it's an outbox. 342 */ 343 public boolean isOutbox() { 344 return mListFooterMode == LIST_FOOTER_MODE_SEND; 345 } 346 347 /** 348 * @return the number of messages that are currently selecteed. 349 */ 350 public int getSelectedCount() { 351 return mListAdapter.getSelectedSet().size(); 352 } 353 354 /** 355 * @return true if the list is in the "selection" mode. 356 */ 357 private boolean isInSelectionMode() { 358 return getSelectedCount() > 0; 359 } 360 361 /** 362 * Save the focused list item. 363 * 364 * TODO It's not really working. Fix it. 365 */ 366 private void saveListPosition() { 367 mSavedItemPosition = getListView().getSelectedItemPosition(); 368 if (mSavedItemPosition >= 0 && getListView().isSelected()) { 369 mSavedItemTop = getListView().getSelectedView().getTop(); 370 } else { 371 mSavedItemPosition = getListView().getFirstVisiblePosition(); 372 if (mSavedItemPosition >= 0) { 373 mSavedItemTop = 0; 374 View topChild = getListView().getChildAt(0); 375 if (topChild != null) { 376 mSavedItemTop = topChild.getTop(); 377 } 378 } 379 } 380 } 381 382 /** 383 * Restore the focused list item. 384 * 385 * TODO It's not really working. Fix it. 386 */ 387 private void restoreListPosition() { 388 if (mSavedItemPosition >= 0 && mSavedItemPosition < getListView().getCount()) { 389 getListView().setSelectionFromTop(mSavedItemPosition, mSavedItemTop); 390 mSavedItemPosition = -1; 391 mSavedItemTop = 0; 392 } 393 } 394 395 /** 396 * Called when a message is clicked. 397 */ 398 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 399 if (view != mListFooterView) { 400 MessageListItem itemView = (MessageListItem) view; 401 if (isInSelectionMode()) { 402 toggleSelection(itemView); 403 } else { 404 onMessageOpen(itemView.mMailboxId, id); 405 } 406 } else { 407 doFooterClick(); 408 } 409 } 410 411 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { 412 if (view != mListFooterView) { 413 if (isInSelectionMode()) { 414 // Already in selection mode. Ignore. 415 } else { 416 toggleSelection((MessageListItem) view); 417 return true; 418 } 419 } 420 return false; 421 } 422 423 private void toggleSelection(MessageListItem itemView) { 424 mListAdapter.updateSelected(itemView, !mListAdapter.isSelected(itemView)); 425 } 426 427 private void onMessageOpen(final long mailboxId, final long messageId) { 428 // Use asynctask to determine the type. 429 new AsyncTask<Void, Void, Integer>() { 430 @Override 431 protected Integer doInBackground(Void... params) { 432 EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId( 433 getActivity(), mailboxId); 434 if (mailbox == null) { 435 return null; 436 } 437 switch (mailbox.mType) { 438 case EmailContent.Mailbox.TYPE_DRAFTS: 439 return Callback.TYPE_DRAFT; 440 case EmailContent.Mailbox.TYPE_TRASH: 441 return Callback.TYPE_TRASH; 442 default: 443 return Callback.TYPE_REGULAR; 444 } 445 } 446 447 @Override 448 protected void onPostExecute(Integer type) { 449 if (type == null) { 450 return; 451 } 452 mCallback.onMessageOpen(messageId, mailboxId, getMailboxId(), type); 453 } 454 }.execute(); 455 } 456 457 public void onMultiToggleRead() { 458 onMultiToggleRead(mListAdapter.getSelectedSet()); 459 } 460 461 public void onMultiToggleFavorite() { 462 onMultiToggleFavorite(mListAdapter.getSelectedSet()); 463 } 464 465 public void onMultiDelete() { 466 onMultiDelete(mListAdapter.getSelectedSet()); 467 } 468 469 /** 470 * Refresh the list. NOOP for special mailboxes (e.g. combined inbox). 471 */ 472 public void onRefresh() { 473 if (!isMagicMailbox()) { 474 // Note we can use mAccountId here because it's not a magic mailbox, which doesn't have 475 // a specific account id. 476 mController.updateMailbox(mAccountId, mMailboxId); 477 } 478 } 479 480 public void onDeselectAll() { 481 mListAdapter.getSelectedSet().clear(); 482 getListView().invalidateViews(); 483 mCallback.onSelectionChanged(); 484 } 485 486 /** 487 * Load more messages. NOOP for special mailboxes (e.g. combined inbox). 488 */ 489 private void onLoadMoreMessages() { 490 if (!isMagicMailbox()) { 491 mController.loadMoreMessages(mMailboxId); 492 } 493 } 494 495 public void onSendPendingMessages() { 496 if (getMailboxId() == Mailbox.QUERY_ALL_OUTBOX) { 497 mController.sendPendingMessagesForAllAccounts(mActivity); 498 } else if (!isMagicMailbox()) { // Magic boxes don't have a specific account id. 499 mController.sendPendingMessages(getAccountId()); 500 } 501 } 502 503 private void onSetMessageRead(long messageId, boolean newRead) { 504 mController.setMessageRead(messageId, newRead); 505 } 506 507 private void onSetMessageFavorite(long messageId, boolean newFavorite) { 508 mController.setMessageFavorite(messageId, newFavorite); 509 } 510 511 /** 512 * Toggles a set read/unread states. Note, the default behavior is "mark unread", so the 513 * sense of the helper methods is "true=unread". 514 * 515 * @param selectedSet The current list of selected items 516 */ 517 private void onMultiToggleRead(Set<Long> selectedSet) { 518 toggleMultiple(selectedSet, new MultiToggleHelper() { 519 520 public boolean getField(long messageId, Cursor c) { 521 return c.getInt(MessagesAdapter.COLUMN_READ) == 0; 522 } 523 524 public boolean setField(long messageId, Cursor c, boolean newValue) { 525 boolean oldValue = getField(messageId, c); 526 if (oldValue != newValue) { 527 onSetMessageRead(messageId, !newValue); 528 return true; 529 } 530 return false; 531 } 532 }); 533 } 534 535 /** 536 * Toggles a set of favorites (stars) 537 * 538 * @param selectedSet The current list of selected items 539 */ 540 private void onMultiToggleFavorite(Set<Long> selectedSet) { 541 toggleMultiple(selectedSet, new MultiToggleHelper() { 542 543 public boolean getField(long messageId, Cursor c) { 544 return c.getInt(MessagesAdapter.COLUMN_FAVORITE) != 0; 545 } 546 547 public boolean setField(long messageId, Cursor c, boolean newValue) { 548 boolean oldValue = getField(messageId, c); 549 if (oldValue != newValue) { 550 onSetMessageFavorite(messageId, newValue); 551 return true; 552 } 553 return false; 554 } 555 }); 556 } 557 558 private void onMultiDelete(Set<Long> selectedSet) { 559 // Clone the set, because deleting is going to thrash things 560 HashSet<Long> cloneSet = new HashSet<Long>(selectedSet); 561 for (Long id : cloneSet) { 562 mController.deleteMessage(id, -1); 563 } 564 Toast.makeText(mActivity, mActivity.getResources().getQuantityString( 565 R.plurals.message_deleted_toast, cloneSet.size()), Toast.LENGTH_SHORT).show(); 566 selectedSet.clear(); 567 mCallback.onSelectionChanged(); 568 } 569 570 private interface MultiToggleHelper { 571 /** 572 * Return true if the field of interest is "set". If one or more are false, then our 573 * bulk action will be to "set". If all are set, our bulk action will be to "clear". 574 * @param messageId the message id of the current message 575 * @param c the cursor, positioned to the item of interest 576 * @return true if the field at this row is "set" 577 */ 578 public boolean getField(long messageId, Cursor c); 579 580 /** 581 * Set or clear the field of interest. Return true if a change was made. 582 * @param messageId the message id of the current message 583 * @param c the cursor, positioned to the item of interest 584 * @param newValue the new value to be set at this row 585 * @return true if a change was actually made 586 */ 587 public boolean setField(long messageId, Cursor c, boolean newValue); 588 } 589 590 /** 591 * Toggle multiple fields in a message, using the following logic: If one or more fields 592 * are "clear", then "set" them. If all fields are "set", then "clear" them all. 593 * 594 * @param selectedSet the set of messages that are selected 595 * @param helper functions to implement the specific getter & setter 596 * @return the number of messages that were updated 597 */ 598 private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) { 599 Cursor c = mListAdapter.getCursor(); 600 boolean anyWereFound = false; 601 boolean allWereSet = true; 602 603 c.moveToPosition(-1); 604 while (c.moveToNext()) { 605 long id = c.getInt(MessagesAdapter.COLUMN_ID); 606 if (selectedSet.contains(Long.valueOf(id))) { 607 anyWereFound = true; 608 if (!helper.getField(id, c)) { 609 allWereSet = false; 610 break; 611 } 612 } 613 } 614 615 int numChanged = 0; 616 617 if (anyWereFound) { 618 boolean newValue = !allWereSet; 619 c.moveToPosition(-1); 620 while (c.moveToNext()) { 621 long id = c.getInt(MessagesAdapter.COLUMN_ID); 622 if (selectedSet.contains(Long.valueOf(id))) { 623 if (helper.setField(id, c, newValue)) { 624 ++numChanged; 625 } 626 } 627 } 628 } 629 630 return numChanged; 631 } 632 633 /** 634 * Test selected messages for showing appropriate labels 635 * @param selectedSet 636 * @param column_id 637 * @param defaultflag 638 * @return true when the specified flagged message is selected 639 */ 640 private boolean testMultiple(Set<Long> selectedSet, int column_id, boolean defaultflag) { 641 Cursor c = mListAdapter.getCursor(); 642 if (c == null || c.isClosed()) { 643 return false; 644 } 645 c.moveToPosition(-1); 646 while (c.moveToNext()) { 647 long id = c.getInt(MessagesAdapter.COLUMN_ID); 648 if (selectedSet.contains(Long.valueOf(id))) { 649 if (c.getInt(column_id) == (defaultflag? 1 : 0)) { 650 return true; 651 } 652 } 653 } 654 return false; 655 } 656 657 /** 658 * @return true if one or more non-starred messages are selected. 659 */ 660 public boolean doesSelectionContainNonStarredMessage() { 661 return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_FAVORITE, 662 false); 663 } 664 665 /** 666 * @return true if one or more read messages are selected. 667 */ 668 public boolean doesSelectionContainReadMessage() { 669 return testMultiple(mListAdapter.getSelectedSet(), MessagesAdapter.COLUMN_READ, true); 670 } 671 672 /** 673 * Implements a timed refresh of "stale" mailboxes. This should only happen when 674 * multiple conditions are true, including: 675 * Only when the user explicitly opens the mailbox (not onResume, for example) 676 * Only for real, non-push mailboxes 677 * Only when the mailbox is "stale" (currently set to 5 minutes since last refresh) 678 */ 679 private void autoRefreshStaleMailbox() { 680 if (!mCanAutoRefresh 681 || (mListAdapter.getCursor() == null) // Check if messages info is loaded 682 || (mPushModeMailbox != null && mPushModeMailbox) // Check the push mode 683 || isMagicMailbox()) { // Check if this mailbox is synthetic/combined 684 return; 685 } 686 mCanAutoRefresh = false; 687 if (!Email.mailboxRequiresRefresh(mMailboxId)) { 688 return; 689 } 690 onRefresh(); 691 } 692 693 public void updateListPosition() { // TODO give it a better name 694 int listViewHeight = getListView().getHeight(); 695 if (mListAdapter.getSelectedSet().size() == 1 && mFirstSelectedItemPosition >= 0 696 && mFirstSelectedItemPosition < getListView().getCount() 697 && listViewHeight < mFirstSelectedItemTop) { 698 getListView().setSelectionFromTop(mFirstSelectedItemPosition, 699 listViewHeight - mFirstSelectedItemHeight); 700 } 701 } 702 703 /** 704 * Show/hide the progress icon on the list footer. It's called by the host activity. 705 * TODO: It might be cleaner if the fragment listen to the controller events and show it by 706 * itself, rather than letting the activity controll this. 707 */ 708 public void showProgressIcon(boolean show) { 709 if (mListFooterProgress != null) { 710 mListFooterProgress.setVisibility(show ? View.VISIBLE : View.GONE); 711 } 712 setListFooterText(show); 713 } 714 715 // Adapter callbacks 716 public void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite) { 717 onSetMessageFavorite(itemView.mMessageId, newFavorite); 718 } 719 720 public void onAdapterRequery() { 721 // This updates the "multi-selection" button labels. 722 mCallback.onSelectionChanged(); 723 } 724 725 public void onAdapterSelectedChanged(MessageListItem itemView, boolean newSelected, 726 int mSelectedCount) { 727 if (mSelectedCount == 1 && newSelected) { 728 mFirstSelectedItemPosition = getListView().getPositionForView(itemView); 729 mFirstSelectedItemTop = itemView.getBottom(); 730 mFirstSelectedItemHeight = itemView.getHeight(); 731 } else { 732 mFirstSelectedItemPosition = -1; 733 } 734 mCallback.onSelectionChanged(); 735 } 736 737 /** 738 * Add the fixed footer view if appropriate (not always - not all accounts & mailboxes). 739 * 740 * Here are some rules (finish this list): 741 * 742 * Any merged, synced box (except send): refresh 743 * Any push-mode account: refresh 744 * Any non-push-mode account: load more 745 * Any outbox (send again): 746 * 747 * @param mailboxId the ID of the mailbox 748 * @param accountId the ID of the account 749 */ 750 private void addFooterView(long mailboxId, long accountId) { 751 // first, look for shortcuts that don't need us to spin up a DB access task 752 if (mailboxId == Mailbox.QUERY_ALL_INBOXES 753 || mailboxId == Mailbox.QUERY_ALL_UNREAD 754 || mailboxId == Mailbox.QUERY_ALL_FAVORITES) { 755 finishFooterView(LIST_FOOTER_MODE_REFRESH); 756 return; 757 } 758 if (mailboxId == Mailbox.QUERY_ALL_DRAFTS) { 759 finishFooterView(LIST_FOOTER_MODE_NONE); 760 return; 761 } 762 if (mailboxId == Mailbox.QUERY_ALL_OUTBOX) { 763 finishFooterView(LIST_FOOTER_MODE_SEND); 764 return; 765 } 766 767 // We don't know enough to select the footer command type (yet), so we'll 768 // launch an async task to do the remaining lookups and decide what to do 769 mSetFooterTask = new SetFooterTask(); 770 mSetFooterTask.execute(mailboxId, accountId); 771 } 772 773 private final static String[] MAILBOX_ACCOUNT_AND_TYPE_PROJECTION = 774 new String[] { MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE }; 775 776 private class SetFooterTask extends AsyncTask<Long, Void, Integer> { 777 /** 778 * There are two operational modes here, requiring different lookup. 779 * mailboxIs != -1: A specific mailbox - check its type, then look up its account 780 * accountId != -1: A specific account - look up the account 781 */ 782 @Override 783 protected Integer doInBackground(Long... params) { 784 long mailboxId = params[0]; 785 long accountId = params[1]; 786 int mailboxType = -1; 787 if (mailboxId != -1) { 788 try { 789 Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId); 790 Cursor c = mResolver.query(uri, MAILBOX_ACCOUNT_AND_TYPE_PROJECTION, 791 null, null, null); 792 if (c.moveToFirst()) { 793 try { 794 accountId = c.getLong(0); 795 mailboxType = c.getInt(1); 796 } finally { 797 c.close(); 798 } 799 } 800 } catch (IllegalArgumentException iae) { 801 // can't do any more here 802 return LIST_FOOTER_MODE_NONE; 803 } 804 } 805 switch (mailboxType) { 806 case Mailbox.TYPE_OUTBOX: 807 return LIST_FOOTER_MODE_SEND; 808 case Mailbox.TYPE_DRAFTS: 809 return LIST_FOOTER_MODE_NONE; 810 } 811 if (accountId != -1) { 812 // This is inefficient but the best fix is not here but in isMessagingController 813 Account account = Account.restoreAccountWithId(mActivity, accountId); 814 if (account != null) { 815 // TODO move this to more appropriate place 816 // (don't change member fields on a worker thread.) 817 mPushModeMailbox = account.mSyncInterval == Account.CHECK_INTERVAL_PUSH; 818 if (mController.isMessagingController(account)) { 819 return LIST_FOOTER_MODE_MORE; // IMAP or POP 820 } else { 821 return LIST_FOOTER_MODE_NONE; // EAS 822 } 823 } 824 } 825 return LIST_FOOTER_MODE_NONE; 826 } 827 828 @Override 829 protected void onPostExecute(Integer listFooterMode) { 830 if (isCancelled()) { 831 return; 832 } 833 if (listFooterMode == null) { 834 return; 835 } 836 finishFooterView(listFooterMode); 837 } 838 } 839 840 /** 841 * Add the fixed footer view as specified, and set up the test as well. 842 * 843 * @param listFooterMode the footer mode we've determined should be used for this list 844 */ 845 private void finishFooterView(int listFooterMode) { 846 mListFooterMode = listFooterMode; 847 if (mListFooterMode != LIST_FOOTER_MODE_NONE) { 848 getListView().addFooterView(mListFooterView); 849 getListView().setAdapter(mListAdapter); 850 851 mListFooterProgress = mListFooterView.findViewById(R.id.progress); 852 mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text); 853 setListFooterText(false); 854 } 855 } 856 857 /** 858 * Set the list footer text based on mode and "active" status 859 */ 860 private void setListFooterText(boolean active) { 861 if (mListFooterMode != LIST_FOOTER_MODE_NONE) { 862 int footerTextId = 0; 863 switch (mListFooterMode) { 864 case LIST_FOOTER_MODE_REFRESH: 865 footerTextId = active ? R.string.status_loading_more 866 : R.string.refresh_action; 867 break; 868 case LIST_FOOTER_MODE_MORE: 869 footerTextId = active ? R.string.status_loading_more 870 : R.string.message_list_load_more_messages_action; 871 break; 872 case LIST_FOOTER_MODE_SEND: 873 footerTextId = active ? R.string.status_sending_messages 874 : R.string.message_list_send_pending_messages_action; 875 break; 876 } 877 mListFooterText.setText(footerTextId); 878 } 879 } 880 881 /** 882 * Handle a click in the list footer, which changes meaning depending on what we're looking at. 883 */ 884 private void doFooterClick() { 885 switch (mListFooterMode) { 886 case LIST_FOOTER_MODE_NONE: // should never happen 887 break; 888 case LIST_FOOTER_MODE_REFRESH: 889 onRefresh(); 890 break; 891 case LIST_FOOTER_MODE_MORE: 892 onLoadMoreMessages(); 893 break; 894 case LIST_FOOTER_MODE_SEND: 895 onSendPendingMessages(); 896 break; 897 } 898 } 899 900 /** 901 * Async task for loading a single folder out of the UI thread 902 * 903 * The code here (for merged boxes) is a placeholder/hack and should be replaced. Some 904 * specific notes: 905 * TODO: Move the double query into a specialized URI that returns all inbox messages 906 * and do the dirty work in raw SQL in the provider. 907 * TODO: Generalize the query generation so we can reuse it in MessageView (for next/prev) 908 */ 909 private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> { 910 911 private final long mMailboxKey; 912 private long mAccountKey; 913 914 /** 915 * Special constructor to cache some local info 916 */ 917 public LoadMessagesTask(long mailboxKey, long accountKey) { 918 mMailboxKey = mailboxKey; 919 mAccountKey = accountKey; 920 } 921 922 @Override 923 protected Cursor doInBackground(Void... params) { 924 // First, determine account id, if unknown 925 if (mAccountKey == -1) { // TODO Use constant instead of -1 926 if (isMagicMailbox()) { 927 // Magic mailbox. No accountid. 928 } else { 929 EmailContent.Mailbox mailbox = 930 EmailContent.Mailbox.restoreMailboxWithId(mActivity, mMailboxKey); 931 if (mailbox != null) { 932 mAccountKey = mailbox.mAccountKey; 933 } else { 934 // Mailbox not found. 935 // TODO We used to close the activity in this case, but what to do now?? 936 return null; 937 } 938 } 939 } 940 941 // Load messages 942 String selection = 943 Utility.buildMailboxIdSelection(mResolver, mMailboxKey); 944 Cursor c = mActivity.getContentResolver().query( 945 EmailContent.Message.CONTENT_URI, MESSAGE_PROJECTION, 946 selection, null, EmailContent.MessageColumns.TIMESTAMP + " DESC"); 947 return c; 948 } 949 950 @Override 951 protected void onPostExecute(Cursor cursor) { 952 if (isCancelled()) { 953 return; 954 } 955 if (cursor == null || cursor.isClosed()) { 956 mCallback.onMailboxNotFound(); 957 return; 958 } 959 MessageListFragment.this.mAccountId = mAccountKey; 960 961 mListAdapter.changeCursor(cursor); 962 setListAdapter(mListAdapter); 963 964 addFooterView(mMailboxKey, mAccountKey); 965 966 // changeCursor occurs the jumping of position in ListView, so it's need to restore 967 // the position; 968 restoreListPosition(); 969 autoRefreshStaleMailbox(); 970 // Reset the "new messages" count in the service, since we're seeing them now 971 if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) { 972 MailService.resetNewMessageCount(mActivity, -1); 973 } else if (mMailboxKey >= 0 && mAccountKey != -1) { 974 MailService.resetNewMessageCount(mActivity, mAccountKey); 975 } 976 } 977 } 978} 979