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