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