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