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