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