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