MessageList.java revision b87e999fbceb8a95e22f0f3209e0698e3f1effbb
1/* 2 * Copyright (C) 2009 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.ControllerResultUiThreadWrapper; 21import com.android.email.Email; 22import com.android.email.R; 23import com.android.email.Utility; 24import com.android.email.activity.setup.AccountSecurity; 25import com.android.email.activity.setup.AccountSettings; 26import com.android.email.mail.AuthenticationFailedException; 27import com.android.email.mail.CertificateValidationException; 28import com.android.email.mail.MessagingException; 29import com.android.email.provider.EmailContent; 30import com.android.email.provider.EmailContent.Account; 31import com.android.email.provider.EmailContent.AccountColumns; 32import com.android.email.provider.EmailContent.Mailbox; 33import com.android.email.provider.EmailContent.MailboxColumns; 34import com.android.email.provider.EmailContent.MessageColumns; 35import com.android.email.service.MailService; 36 37import android.app.ListActivity; 38import android.app.NotificationManager; 39import android.content.ContentResolver; 40import android.content.ContentUris; 41import android.content.Context; 42import android.content.Intent; 43import android.database.Cursor; 44import android.net.Uri; 45import android.os.AsyncTask; 46import android.os.Bundle; 47import android.os.Handler; 48import android.view.ContextMenu; 49import android.view.LayoutInflater; 50import android.view.Menu; 51import android.view.MenuItem; 52import android.view.View; 53import android.view.ContextMenu.ContextMenuInfo; 54import android.view.View.OnClickListener; 55import android.view.animation.Animation; 56import android.view.animation.AnimationUtils; 57import android.view.animation.Animation.AnimationListener; 58import android.widget.AdapterView; 59import android.widget.Button; 60import android.widget.ListView; 61import android.widget.ProgressBar; 62import android.widget.TextView; 63import android.widget.Toast; 64import android.widget.AdapterView.OnItemClickListener; 65 66import java.util.HashSet; 67import java.util.Set; 68 69public class MessageList extends ListActivity implements OnItemClickListener, OnClickListener, 70 AnimationListener, MessageListAdapter.Callback { 71 // Intent extras (internal to this activity) 72 private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID"; 73 private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE"; 74 private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID"; 75 private static final String STATE_SELECTED_ITEM_TOP = 76 "com.android.email.activity.MessageList.selectedItemTop"; 77 private static final String STATE_SELECTED_POSITION = 78 "com.android.email.activity.MessageList.selectedPosition"; 79 private static final String STATE_CHECKED_ITEMS = 80 "com.android.email.activity.MessageList.checkedItems"; 81 82 private static final int REQUEST_SECURITY = 0; 83 84 // UI support 85 private ListView mListView; 86 private View mMultiSelectPanel; 87 private Button mReadUnreadButton; 88 private Button mFavoriteButton; 89 private Button mDeleteButton; 90 private View mListFooterView; 91 private TextView mListFooterText; 92 private View mListFooterProgress; 93 private TextView mErrorBanner; 94 95 private static final int LIST_FOOTER_MODE_NONE = 0; 96 private static final int LIST_FOOTER_MODE_REFRESH = 1; 97 private static final int LIST_FOOTER_MODE_MORE = 2; 98 private static final int LIST_FOOTER_MODE_SEND = 3; 99 private int mListFooterMode; 100 101 private MessageListAdapter mListAdapter; 102 private final Controller mController = Controller.getInstance(getApplication()); 103 private ControllerResultUiThreadWrapper<ControllerResults> mControllerCallback; 104 105 private TextView mLeftTitle; 106 private ProgressBar mProgressIcon; 107 108 // DB access 109 private ContentResolver mResolver; 110 private long mMailboxId; 111 private LoadMessagesTask mLoadMessagesTask; 112 private FindMailboxTask mFindMailboxTask; 113 private SetTitleTask mSetTitleTask; 114 private SetFooterTask mSetFooterTask; 115 116 public final static String[] MAILBOX_FIND_INBOX_PROJECTION = new String[] { 117 EmailContent.RECORD_ID, MailboxColumns.TYPE, MailboxColumns.FLAG_VISIBLE 118 }; 119 120 private static final int MAILBOX_NAME_COLUMN_ID = 0; 121 private static final int MAILBOX_NAME_COLUMN_ACCOUNT_KEY = 1; 122 private static final int MAILBOX_NAME_COLUMN_TYPE = 2; 123 private static final String[] MAILBOX_NAME_PROJECTION = new String[] { 124 MailboxColumns.DISPLAY_NAME, MailboxColumns.ACCOUNT_KEY, 125 MailboxColumns.TYPE}; 126 127 private static final int ACCOUNT_DISPLAY_NAME_COLUMN_ID = 0; 128 private static final String[] ACCOUNT_NAME_PROJECTION = new String[] { 129 AccountColumns.DISPLAY_NAME }; 130 131 private static final int ACCOUNT_INFO_COLUMN_FLAGS = 0; 132 private static final String[] ACCOUNT_INFO_PROJECTION = new String[] { 133 AccountColumns.FLAGS }; 134 135 private static final String ID_SELECTION = EmailContent.RECORD_ID + "=?"; 136 137 private Boolean mPushModeMailbox = null; 138 private int mSavedItemTop = 0; 139 private int mSavedItemPosition = -1; 140 private int mFirstSelectedItemTop = 0; 141 private int mFirstSelectedItemPosition = -1; 142 private int mFirstSelectedItemHeight = -1; 143 private boolean mCanAutoRefresh = false; 144 145 /* package */ static final String[] MESSAGE_PROJECTION = new String[] { 146 EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, 147 MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP, 148 MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT, 149 MessageColumns.FLAGS, 150 }; 151 152 /** 153 * Open a specific mailbox. 154 * 155 * TODO This should just shortcut to a more generic version that can accept a list of 156 * accounts/mailboxes (e.g. merged inboxes). 157 * 158 * @param context 159 * @param id mailbox key 160 */ 161 public static void actionHandleMailbox(Context context, long id) { 162 context.startActivity(createIntent(context, -1, id, -1)); 163 } 164 165 /** 166 * Open a specific mailbox by account & type 167 * 168 * @param context The caller's context (for generating an intent) 169 * @param accountId The account to open 170 * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX) 171 */ 172 public static void actionHandleAccount(Context context, long accountId, int mailboxType) { 173 context.startActivity(createIntent(context, accountId, -1, mailboxType)); 174 } 175 176 /** 177 * Open the inbox of the account with a UUID. It's used to handle old style 178 * (Android <= 1.6) desktop shortcut intents. 179 */ 180 public static void actionOpenAccountInboxUuid(Context context, String accountUuid) { 181 Intent i = createIntent(context, -1, -1, Mailbox.TYPE_INBOX); 182 i.setData(Account.getShortcutSafeUriFromUuid(accountUuid)); 183 context.startActivity(i); 184 } 185 186 /** 187 * Return an intent to open a specific mailbox by account & type. 188 * 189 * @param context The caller's context (for generating an intent) 190 * @param accountId The account to open, or -1 191 * @param mailboxId the ID of the mailbox to open, or -1 192 * @param mailboxType the type of mailbox to open (e.g. @see Mailbox.TYPE_INBOX) or -1 193 */ 194 public static Intent createIntent(Context context, long accountId, long mailboxId, 195 int mailboxType) { 196 Intent intent = new Intent(context, MessageList.class); 197 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 198 if (accountId != -1) intent.putExtra(EXTRA_ACCOUNT_ID, accountId); 199 if (mailboxId != -1) intent.putExtra(EXTRA_MAILBOX_ID, mailboxId); 200 if (mailboxType != -1) intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType); 201 return intent; 202 } 203 204 /** 205 * Create and return an intent for a desktop shortcut for an account. 206 * 207 * @param context Calling context for building the intent 208 * @param account The account of interest 209 * @param mailboxType The folder name to open (typically Mailbox.TYPE_INBOX) 210 * @return an Intent which can be used to view that account 211 */ 212 public static Intent createAccountIntentForShortcut(Context context, Account account, 213 int mailboxType) { 214 Intent i = createIntent(context, -1, -1, mailboxType); 215 i.setData(account.getShortcutSafeUri()); 216 return i; 217 } 218 219 @Override 220 public void onCreate(Bundle icicle) { 221 super.onCreate(icicle); 222 setContentView(R.layout.message_list); 223 224 mControllerCallback = new ControllerResultUiThreadWrapper<ControllerResults>( 225 new Handler(), new ControllerResults()); 226 mCanAutoRefresh = true; 227 mListView = getListView(); 228 mMultiSelectPanel = findViewById(R.id.footer_organize); 229 mReadUnreadButton = (Button) findViewById(R.id.btn_read_unread); 230 mFavoriteButton = (Button) findViewById(R.id.btn_multi_favorite); 231 mDeleteButton = (Button) findViewById(R.id.btn_multi_delete); 232 mLeftTitle = (TextView) findViewById(R.id.title_left_text); 233 mProgressIcon = (ProgressBar) findViewById(R.id.title_progress_icon); 234 mErrorBanner = (TextView) findViewById(R.id.connection_error_text); 235 236 mReadUnreadButton.setOnClickListener(this); 237 mFavoriteButton.setOnClickListener(this); 238 mDeleteButton.setOnClickListener(this); 239 ((Button) findViewById(R.id.account_title_button)).setOnClickListener(this); 240 241 mListView.setOnItemClickListener(this); 242 mListView.setItemsCanFocus(false); 243 registerForContextMenu(mListView); 244 245 mListAdapter = new MessageListAdapter(this, new Handler(), this); 246 setListAdapter(mListAdapter); 247 248 mResolver = getContentResolver(); 249 250 // TODO extend this to properly deal with multiple mailboxes, cursor, etc. 251 252 // Show the appropriate account/mailbox specified by an {@link Intent}. 253 selectAccountAndMailbox(getIntent()); 254 } 255 256 /** 257 * Show the appropriate account/mailbox specified by an {@link Intent}. 258 */ 259 private void selectAccountAndMailbox(Intent intent) { 260 mMailboxId = intent.getLongExtra(EXTRA_MAILBOX_ID, -1); 261 if (mMailboxId != -1) { 262 // Specific mailbox ID was provided - go directly to it 263 mSetTitleTask = new SetTitleTask(mMailboxId); 264 mSetTitleTask.execute(); 265 mLoadMessagesTask = new LoadMessagesTask(mMailboxId, -1); 266 mLoadMessagesTask.execute(); 267 addFooterView(mMailboxId, -1, -1); 268 } else { 269 int mailboxType = intent.getIntExtra(EXTRA_MAILBOX_TYPE, Mailbox.TYPE_INBOX); 270 Uri uri = intent.getData(); 271 // TODO Possible ANR. getAccountIdFromShortcutSafeUri accesses DB. 272 long accountId = (uri == null) ? -1 273 : Account.getAccountIdFromShortcutSafeUri(this, uri); 274 275 if (accountId != -1) { 276 // A content URI was provided - try to look up the account 277 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false); 278 mFindMailboxTask.execute(); 279 } else { 280 // Go by account id + type 281 accountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1); 282 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, true); 283 mFindMailboxTask.execute(); 284 } 285 addFooterView(-1, accountId, mailboxType); 286 } 287 // TODO set title to "account > mailbox (#unread)" 288 } 289 290 @Override 291 public void onPause() { 292 super.onPause(); 293 mController.removeResultCallback(mControllerCallback); 294 } 295 296 @Override 297 public void onResume() { 298 super.onResume(); 299 mController.addResultCallback(mControllerCallback); 300 301 // clear notifications here 302 NotificationManager notificationManager = (NotificationManager) 303 getSystemService(Context.NOTIFICATION_SERVICE); 304 notificationManager.cancel(MailService.NOTIFICATION_ID_NEW_MESSAGES); 305 306 // Exit immediately if the accounts list has changed (e.g. externally deleted) 307 if (Email.getNotifyUiAccountsChanged()) { 308 Welcome.actionStart(this); 309 finish(); 310 return; 311 } 312 313 restoreListPosition(); 314 autoRefreshStaleMailbox(); 315 } 316 317 @Override 318 protected void onDestroy() { 319 super.onDestroy(); 320 321 Utility.cancelTaskInterrupt(mLoadMessagesTask); 322 mLoadMessagesTask = null; 323 Utility.cancelTaskInterrupt(mFindMailboxTask); 324 mFindMailboxTask = null; 325 Utility.cancelTaskInterrupt(mSetTitleTask); 326 mSetTitleTask = null; 327 Utility.cancelTaskInterrupt(mSetFooterTask); 328 mSetFooterTask = null; 329 330 mListAdapter.changeCursor(null); 331 } 332 333 @Override 334 protected void onSaveInstanceState(Bundle outState) { 335 super.onSaveInstanceState(outState); 336 saveListPosition(); 337 outState.putInt(STATE_SELECTED_POSITION, mSavedItemPosition); 338 outState.putInt(STATE_SELECTED_ITEM_TOP, mSavedItemTop); 339 Set<Long> checkedset = mListAdapter.getSelectedSet(); 340 long[] checkedarray = new long[checkedset.size()]; 341 int i = 0; 342 for (Long l : checkedset) { 343 checkedarray[i] = l; 344 i++; 345 } 346 outState.putLongArray(STATE_CHECKED_ITEMS, checkedarray); 347 } 348 349 @Override 350 protected void onRestoreInstanceState(Bundle savedInstanceState) { 351 super.onRestoreInstanceState(savedInstanceState); 352 mSavedItemTop = savedInstanceState.getInt(STATE_SELECTED_ITEM_TOP, 0); 353 mSavedItemPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION, -1); 354 Set<Long> checkedset = mListAdapter.getSelectedSet(); 355 for (long l: savedInstanceState.getLongArray(STATE_CHECKED_ITEMS)) { 356 checkedset.add(l); 357 } 358 } 359 360 private void saveListPosition() { 361 mSavedItemPosition = getListView().getSelectedItemPosition(); 362 if (mSavedItemPosition >= 0 && getListView().isSelected()) { 363 mSavedItemTop = getListView().getSelectedView().getTop(); 364 } else { 365 mSavedItemPosition = getListView().getFirstVisiblePosition(); 366 if (mSavedItemPosition >= 0) { 367 mSavedItemTop = 0; 368 View topChild = getListView().getChildAt(0); 369 if (topChild != null) { 370 mSavedItemTop = topChild.getTop(); 371 } 372 } 373 } 374 } 375 376 private void restoreListPosition() { 377 if (mSavedItemPosition >= 0 && mSavedItemPosition < getListView().getCount()) { 378 getListView().setSelectionFromTop(mSavedItemPosition, mSavedItemTop); 379 mSavedItemPosition = -1; 380 mSavedItemTop = 0; 381 } 382 } 383 384 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 385 if (view != mListFooterView) { 386 MessageListItem itemView = (MessageListItem) view; 387 onOpenMessage(id, itemView.mMailboxId); 388 } else { 389 doFooterClick(); 390 } 391 } 392 393 public void onClick(View v) { 394 switch (v.getId()) { 395 case R.id.btn_read_unread: 396 onMultiToggleRead(mListAdapter.getSelectedSet()); 397 break; 398 case R.id.btn_multi_favorite: 399 onMultiToggleFavorite(mListAdapter.getSelectedSet()); 400 break; 401 case R.id.btn_multi_delete: 402 onMultiDelete(mListAdapter.getSelectedSet()); 403 break; 404 case R.id.account_title_button: 405 onAccounts(); 406 break; 407 } 408 } 409 410 public void onAnimationEnd(Animation animation) { 411 updateListPosition(); 412 } 413 414 public void onAnimationRepeat(Animation animation) { 415 } 416 417 public void onAnimationStart(Animation animation) { 418 } 419 420 @Override 421 public boolean onCreateOptionsMenu(Menu menu) { 422 super.onCreateOptionsMenu(menu); 423 if (mMailboxId < 0) { 424 getMenuInflater().inflate(R.menu.message_list_option_smart_folder, menu); 425 } else { 426 getMenuInflater().inflate(R.menu.message_list_option, menu); 427 } 428 return true; 429 } 430 431 @Override 432 public boolean onPrepareOptionsMenu(Menu menu) { 433 boolean showDeselect = mListAdapter.getSelectedSet().size() > 0; 434 menu.setGroupVisible(R.id.deselect_all_group, showDeselect); 435 return true; 436 } 437 438 @Override 439 public boolean onOptionsItemSelected(MenuItem item) { 440 switch (item.getItemId()) { 441 case R.id.refresh: 442 onRefresh(); 443 return true; 444 case R.id.folders: 445 onFolders(); 446 return true; 447 case R.id.accounts: 448 onAccounts(); 449 return true; 450 case R.id.compose: 451 onCompose(); 452 return true; 453 case R.id.account_settings: 454 onEditAccount(); 455 return true; 456 case R.id.deselect_all: 457 onDeselectAll(); 458 return true; 459 default: 460 return super.onOptionsItemSelected(item); 461 } 462 } 463 464 @Override 465 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 466 super.onCreateContextMenu(menu, v, menuInfo); 467 468 AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; 469 // There is no context menu for the list footer 470 if (info.targetView == mListFooterView) { 471 return; 472 } 473 MessageListItem itemView = (MessageListItem) info.targetView; 474 475 Cursor c = (Cursor) mListView.getItemAtPosition(info.position); 476 String messageName = c.getString(MessageListAdapter.COLUMN_SUBJECT); 477 478 menu.setHeaderTitle(messageName); 479 480 // TODO: There is probably a special context menu for the trash 481 Mailbox mailbox = Mailbox.restoreMailboxWithId(this, itemView.mMailboxId); 482 if (mailbox == null) { 483 return; 484 } 485 486 switch (mailbox.mType) { 487 case EmailContent.Mailbox.TYPE_DRAFTS: 488 getMenuInflater().inflate(R.menu.message_list_context_drafts, menu); 489 break; 490 case EmailContent.Mailbox.TYPE_OUTBOX: 491 getMenuInflater().inflate(R.menu.message_list_context_outbox, menu); 492 break; 493 case EmailContent.Mailbox.TYPE_TRASH: 494 getMenuInflater().inflate(R.menu.message_list_context_trash, menu); 495 break; 496 default: 497 getMenuInflater().inflate(R.menu.message_list_context, menu); 498 // The default menu contains "mark as read". If the message is read, change 499 // the menu text to "mark as unread." 500 if (itemView.mRead) { 501 menu.findItem(R.id.mark_as_read).setTitle(R.string.mark_as_unread_action); 502 } 503 break; 504 } 505 } 506 507 @Override 508 public boolean onContextItemSelected(MenuItem item) { 509 AdapterView.AdapterContextMenuInfo info = 510 (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 511 MessageListItem itemView = (MessageListItem) info.targetView; 512 513 switch (item.getItemId()) { 514 case R.id.open: 515 onOpenMessage(info.id, itemView.mMailboxId); 516 break; 517 case R.id.delete: 518 onDelete(info.id, itemView.mAccountId); 519 break; 520 case R.id.reply: 521 onReply(itemView.mMessageId); 522 break; 523 case R.id.reply_all: 524 onReplyAll(itemView.mMessageId); 525 break; 526 case R.id.forward: 527 onForward(itemView.mMessageId); 528 break; 529 case R.id.mark_as_read: 530 onSetMessageRead(info.id, !itemView.mRead); 531 break; 532 } 533 return super.onContextItemSelected(item); 534 } 535 536 private void onRefresh() { 537 // TODO: Should not be reading from DB in UI thread - need a cleaner way to get accountId 538 if (mMailboxId >= 0) { 539 Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mMailboxId); 540 if (mailbox != null) { 541 mController.updateMailbox(mailbox.mAccountKey, mMailboxId, mControllerCallback); 542 } 543 } 544 } 545 546 private void onFolders() { 547 if (mMailboxId >= 0) { 548 // TODO smaller projection 549 Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mMailboxId); 550 if (mailbox != null) { 551 MailboxList.actionHandleAccount(this, mailbox.mAccountKey); 552 finish(); 553 } 554 } 555 } 556 557 private void onAccounts() { 558 AccountFolderList.actionShowAccounts(this); 559 finish(); 560 } 561 562 private long lookupAccountIdFromMailboxId(long mailboxId) { 563 // TODO: Select correct account to send from when there are multiple mailboxes 564 // TODO: Should not be reading from DB in UI thread 565 if (mailboxId < 0) { 566 return -1; // no info, default account 567 } 568 EmailContent.Mailbox mailbox = 569 EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId); 570 if (mailbox == null) { 571 return -2; 572 } 573 return mailbox.mAccountKey; 574 } 575 576 private void onCompose() { 577 long accountKey = lookupAccountIdFromMailboxId(mMailboxId); 578 if (accountKey > -2) { 579 MessageCompose.actionCompose(this, accountKey); 580 } else { 581 finish(); 582 } 583 } 584 585 private void onEditAccount() { 586 long accountKey = lookupAccountIdFromMailboxId(mMailboxId); 587 if (accountKey > -2) { 588 AccountSettings.actionSettings(this, accountKey); 589 } else { 590 finish(); 591 } 592 } 593 594 private void onDeselectAll() { 595 mListAdapter.getSelectedSet().clear(); 596 mListView.invalidateViews(); 597 showMultiPanel(false); 598 } 599 600 private void onOpenMessage(long messageId, long mailboxId) { 601 // TODO: Should not be reading from DB in UI thread 602 EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId); 603 if (mailbox == null) { 604 return; 605 } 606 607 if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) { 608 MessageCompose.actionEditDraft(this, messageId); 609 } else { 610 final boolean disableReply = (mailbox.mType == EmailContent.Mailbox.TYPE_TRASH); 611 // WARNING: here we pass mMailboxId, which can be the negative id of a compound 612 // mailbox, instead of the mailboxId of the particular message that is opened 613 MessageView.actionView(this, messageId, mMailboxId, disableReply); 614 } 615 } 616 617 private void onReply(long messageId) { 618 MessageCompose.actionReply(this, messageId, false); 619 } 620 621 private void onReplyAll(long messageId) { 622 MessageCompose.actionReply(this, messageId, true); 623 } 624 625 private void onForward(long messageId) { 626 MessageCompose.actionForward(this, messageId); 627 } 628 629 private void onLoadMoreMessages() { 630 if (mMailboxId >= 0) { 631 mController.loadMoreMessages(mMailboxId, mControllerCallback); 632 } 633 } 634 635 private void onSendPendingMessages() { 636 if (mMailboxId == Mailbox.QUERY_ALL_OUTBOX) { 637 // For the combined Outbox, we loop through all accounts and send the messages 638 Cursor c = mResolver.query(Account.CONTENT_URI, Account.ID_PROJECTION, 639 null, null, null); 640 try { 641 while (c.moveToNext()) { 642 long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); 643 mController.sendPendingMessages(accountId, mControllerCallback); 644 } 645 } finally { 646 c.close(); 647 } 648 } else { 649 long accountKey = lookupAccountIdFromMailboxId(mMailboxId); 650 if (accountKey > -2) { 651 mController.sendPendingMessages(accountKey, mControllerCallback); 652 } else { 653 finish(); 654 } 655 } 656 } 657 658 private void onDelete(long messageId, long accountId) { 659 mController.deleteMessage(messageId, accountId); 660 Toast.makeText(this, getResources().getQuantityString( 661 R.plurals.message_deleted_toast, 1), Toast.LENGTH_SHORT).show(); 662 } 663 664 private void onSetMessageRead(long messageId, boolean newRead) { 665 mController.setMessageRead(messageId, newRead); 666 } 667 668 private void onSetMessageFavorite(long messageId, boolean newFavorite) { 669 mController.setMessageFavorite(messageId, newFavorite); 670 } 671 672 /** 673 * Toggles a set read/unread states. Note, the default behavior is "mark unread", so the 674 * sense of the helper methods is "true=unread". 675 * 676 * @param selectedSet The current list of selected items 677 */ 678 private void onMultiToggleRead(Set<Long> selectedSet) { 679 toggleMultiple(selectedSet, new MultiToggleHelper() { 680 681 public boolean getField(long messageId, Cursor c) { 682 return c.getInt(MessageListAdapter.COLUMN_READ) == 0; 683 } 684 685 public boolean setField(long messageId, Cursor c, boolean newValue) { 686 boolean oldValue = getField(messageId, c); 687 if (oldValue != newValue) { 688 onSetMessageRead(messageId, !newValue); 689 return true; 690 } 691 return false; 692 } 693 }); 694 } 695 696 /** 697 * Toggles a set of favorites (stars) 698 * 699 * @param selectedSet The current list of selected items 700 */ 701 private void onMultiToggleFavorite(Set<Long> selectedSet) { 702 toggleMultiple(selectedSet, new MultiToggleHelper() { 703 704 public boolean getField(long messageId, Cursor c) { 705 return c.getInt(MessageListAdapter.COLUMN_FAVORITE) != 0; 706 } 707 708 public boolean setField(long messageId, Cursor c, boolean newValue) { 709 boolean oldValue = getField(messageId, c); 710 if (oldValue != newValue) { 711 onSetMessageFavorite(messageId, newValue); 712 return true; 713 } 714 return false; 715 } 716 }); 717 } 718 719 private void onMultiDelete(Set<Long> selectedSet) { 720 // Clone the set, because deleting is going to thrash things 721 HashSet<Long> cloneSet = new HashSet<Long>(selectedSet); 722 for (Long id : cloneSet) { 723 mController.deleteMessage(id, -1); 724 } 725 Toast.makeText(this, getResources().getQuantityString( 726 R.plurals.message_deleted_toast, cloneSet.size()), Toast.LENGTH_SHORT).show(); 727 selectedSet.clear(); 728 showMultiPanel(false); 729 } 730 731 private interface MultiToggleHelper { 732 /** 733 * Return true if the field of interest is "set". If one or more are false, then our 734 * bulk action will be to "set". If all are set, our bulk action will be to "clear". 735 * @param messageId the message id of the current message 736 * @param c the cursor, positioned to the item of interest 737 * @return true if the field at this row is "set" 738 */ 739 public boolean getField(long messageId, Cursor c); 740 741 /** 742 * Set or clear the field of interest. Return true if a change was made. 743 * @param messageId the message id of the current message 744 * @param c the cursor, positioned to the item of interest 745 * @param newValue the new value to be set at this row 746 * @return true if a change was actually made 747 */ 748 public boolean setField(long messageId, Cursor c, boolean newValue); 749 } 750 751 /** 752 * Toggle multiple fields in a message, using the following logic: If one or more fields 753 * are "clear", then "set" them. If all fields are "set", then "clear" them all. 754 * 755 * @param selectedSet the set of messages that are selected 756 * @param helper functions to implement the specific getter & setter 757 * @return the number of messages that were updated 758 */ 759 private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) { 760 Cursor c = mListAdapter.getCursor(); 761 boolean anyWereFound = false; 762 boolean allWereSet = true; 763 764 c.moveToPosition(-1); 765 while (c.moveToNext()) { 766 long id = c.getInt(MessageListAdapter.COLUMN_ID); 767 if (selectedSet.contains(Long.valueOf(id))) { 768 anyWereFound = true; 769 if (!helper.getField(id, c)) { 770 allWereSet = false; 771 break; 772 } 773 } 774 } 775 776 int numChanged = 0; 777 778 if (anyWereFound) { 779 boolean newValue = !allWereSet; 780 c.moveToPosition(-1); 781 while (c.moveToNext()) { 782 long id = c.getInt(MessageListAdapter.COLUMN_ID); 783 if (selectedSet.contains(Long.valueOf(id))) { 784 if (helper.setField(id, c, newValue)) { 785 ++numChanged; 786 } 787 } 788 } 789 } 790 791 return numChanged; 792 } 793 794 /** 795 * Test selected messages for showing appropriate labels 796 * @param selectedSet 797 * @param column_id 798 * @param defaultflag 799 * @return true when the specified flagged message is selected 800 */ 801 private boolean testMultiple(Set<Long> selectedSet, int column_id, boolean defaultflag) { 802 Cursor c = mListAdapter.getCursor(); 803 if (c == null || c.isClosed()) { 804 return false; 805 } 806 c.moveToPosition(-1); 807 while (c.moveToNext()) { 808 long id = c.getInt(MessageListAdapter.COLUMN_ID); 809 if (selectedSet.contains(Long.valueOf(id))) { 810 if (c.getInt(column_id) == (defaultflag? 1 : 0)) { 811 return true; 812 } 813 } 814 } 815 return false; 816 } 817 818 /** 819 * Implements a timed refresh of "stale" mailboxes. This should only happen when 820 * multiple conditions are true, including: 821 * Only when the user explicitly opens the mailbox (not onResume, for example) 822 * Only for real, non-push mailboxes 823 * Only when the mailbox is "stale" (currently set to 5 minutes since last refresh) 824 */ 825 private void autoRefreshStaleMailbox() { 826 if (!mCanAutoRefresh 827 || (mListAdapter.getCursor() == null) // Check if messages info is loaded 828 || (mPushModeMailbox != null && mPushModeMailbox) // Check the push mode 829 || (mMailboxId < 0)) { // Check if this mailbox is synthetic/combined 830 return; 831 } 832 mCanAutoRefresh = false; 833 if (!Email.mailboxRequiresRefresh(mMailboxId)) { 834 return; 835 } 836 onRefresh(); 837 } 838 839 private void updateFooterButtonNames () { 840 // Show "unread_action" when one or more read messages are selected. 841 if (testMultiple(mListAdapter.getSelectedSet(), MessageListAdapter.COLUMN_READ, true)) { 842 mReadUnreadButton.setText(R.string.unread_action); 843 } else { 844 mReadUnreadButton.setText(R.string.read_action); 845 } 846 // Show "set_star_action" when one or more un-starred messages are selected. 847 if (testMultiple(mListAdapter.getSelectedSet(), 848 MessageListAdapter.COLUMN_FAVORITE, false)) { 849 mFavoriteButton.setText(R.string.set_star_action); 850 } else { 851 mFavoriteButton.setText(R.string.remove_star_action); 852 } 853 } 854 855 private void updateListPosition () { 856 int listViewHeight = getListView().getHeight(); 857 if (mListAdapter.getSelectedSet().size() == 1 && mFirstSelectedItemPosition >= 0 858 && mFirstSelectedItemPosition < getListView().getCount() 859 && listViewHeight < mFirstSelectedItemTop) { 860 getListView().setSelectionFromTop(mFirstSelectedItemPosition, 861 listViewHeight - mFirstSelectedItemHeight); 862 } 863 } 864 865 /** 866 * Show or hide the panel of multi-select options 867 */ 868 private void showMultiPanel(boolean show) { 869 if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) { 870 mMultiSelectPanel.setVisibility(View.VISIBLE); 871 Animation animation = AnimationUtils.loadAnimation(this, R.anim.footer_appear); 872 animation.setAnimationListener(this); 873 mMultiSelectPanel.startAnimation(animation); 874 } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) { 875 mMultiSelectPanel.setVisibility(View.GONE); 876 mMultiSelectPanel.startAnimation( 877 AnimationUtils.loadAnimation(this, R.anim.footer_disappear)); 878 } 879 if (show) { 880 updateFooterButtonNames(); 881 } 882 } 883 884 /** 885 * Add the fixed footer view if appropriate (not always - not all accounts & mailboxes). 886 * 887 * Here are some rules (finish this list): 888 * 889 * Any merged, synced box (except send): refresh 890 * Any push-mode account: refresh 891 * Any non-push-mode account: load more 892 * Any outbox (send again): 893 * 894 * @param mailboxId the ID of the mailbox 895 * @param accountId the ID of the account 896 * @param mailboxType {@code Mailbox.TYPE_} constant, or -1 897 */ 898 private void addFooterView(long mailboxId, long accountId, int mailboxType) { 899 // first, look for shortcuts that don't need us to spin up a DB access task 900 if (mailboxId == Mailbox.QUERY_ALL_INBOXES 901 || mailboxId == Mailbox.QUERY_ALL_UNREAD 902 || mailboxId == Mailbox.QUERY_ALL_FAVORITES) { 903 finishFooterView(LIST_FOOTER_MODE_REFRESH); 904 return; 905 } 906 if (mailboxId == Mailbox.QUERY_ALL_DRAFTS || mailboxType == Mailbox.TYPE_DRAFTS) { 907 finishFooterView(LIST_FOOTER_MODE_NONE); 908 return; 909 } 910 if (mailboxId == Mailbox.QUERY_ALL_OUTBOX || mailboxType == Mailbox.TYPE_OUTBOX) { 911 finishFooterView(LIST_FOOTER_MODE_SEND); 912 return; 913 } 914 915 // We don't know enough to select the footer command type (yet), so we'll 916 // launch an async task to do the remaining lookups and decide what to do 917 mSetFooterTask = new SetFooterTask(); 918 mSetFooterTask.execute(mailboxId, accountId); 919 } 920 921 private final static String[] MAILBOX_ACCOUNT_AND_TYPE_PROJECTION = 922 new String[] { MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE }; 923 924 private class SetFooterTask extends AsyncTask<Long, Void, Integer> { 925 /** 926 * There are two operational modes here, requiring different lookup. 927 * mailboxIs != -1: A specific mailbox - check its type, then look up its account 928 * accountId != -1: A specific account - look up the account 929 */ 930 @Override 931 protected Integer doInBackground(Long... params) { 932 long mailboxId = params[0]; 933 long accountId = params[1]; 934 int mailboxType = -1; 935 if (mailboxId != -1) { 936 try { 937 Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId); 938 Cursor c = mResolver.query(uri, MAILBOX_ACCOUNT_AND_TYPE_PROJECTION, 939 null, null, null); 940 if (c.moveToFirst()) { 941 try { 942 accountId = c.getLong(0); 943 mailboxType = c.getInt(1); 944 } finally { 945 c.close(); 946 } 947 } 948 } catch (IllegalArgumentException iae) { 949 // can't do any more here 950 return LIST_FOOTER_MODE_NONE; 951 } 952 } 953 switch (mailboxType) { 954 case Mailbox.TYPE_OUTBOX: 955 return LIST_FOOTER_MODE_SEND; 956 case Mailbox.TYPE_DRAFTS: 957 return LIST_FOOTER_MODE_NONE; 958 } 959 if (accountId != -1) { 960 // This is inefficient but the best fix is not here but in isMessagingController 961 Account account = Account.restoreAccountWithId(MessageList.this, accountId); 962 if (account != null) { 963 mPushModeMailbox = account.mSyncInterval == Account.CHECK_INTERVAL_PUSH; 964 if (MessageList.this.mController.isMessagingController(account)) { 965 return LIST_FOOTER_MODE_MORE; // IMAP or POP 966 } else { 967 return LIST_FOOTER_MODE_NONE; // EAS 968 } 969 } 970 } 971 return LIST_FOOTER_MODE_NONE; 972 } 973 974 @Override 975 protected void onPostExecute(Integer listFooterMode) { 976 if (listFooterMode == null) { 977 return; 978 } 979 finishFooterView(listFooterMode); 980 } 981 } 982 983 /** 984 * Add the fixed footer view as specified, and set up the test as well. 985 * 986 * @param listFooterMode the footer mode we've determined should be used for this list 987 */ 988 private void finishFooterView(int listFooterMode) { 989 mListFooterMode = listFooterMode; 990 if (mListFooterMode != LIST_FOOTER_MODE_NONE) { 991 mListFooterView = ((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE)) 992 .inflate(R.layout.message_list_item_footer, mListView, false); 993 getListView().addFooterView(mListFooterView); 994 setListAdapter(mListAdapter); 995 996 mListFooterProgress = mListFooterView.findViewById(R.id.progress); 997 mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text); 998 setListFooterText(false); 999 } 1000 } 1001 1002 /** 1003 * Set the list footer text based on mode and "active" status 1004 */ 1005 private void setListFooterText(boolean active) { 1006 if (mListFooterMode != LIST_FOOTER_MODE_NONE) { 1007 int footerTextId = 0; 1008 switch (mListFooterMode) { 1009 case LIST_FOOTER_MODE_REFRESH: 1010 footerTextId = active ? R.string.status_loading_more 1011 : R.string.refresh_action; 1012 break; 1013 case LIST_FOOTER_MODE_MORE: 1014 footerTextId = active ? R.string.status_loading_more 1015 : R.string.message_list_load_more_messages_action; 1016 break; 1017 case LIST_FOOTER_MODE_SEND: 1018 footerTextId = active ? R.string.status_sending_messages 1019 : R.string.message_list_send_pending_messages_action; 1020 break; 1021 } 1022 mListFooterText.setText(footerTextId); 1023 } 1024 } 1025 1026 /** 1027 * Handle a click in the list footer, which changes meaning depending on what we're looking at. 1028 */ 1029 private void doFooterClick() { 1030 switch (mListFooterMode) { 1031 case LIST_FOOTER_MODE_NONE: // should never happen 1032 break; 1033 case LIST_FOOTER_MODE_REFRESH: 1034 onRefresh(); 1035 break; 1036 case LIST_FOOTER_MODE_MORE: 1037 onLoadMoreMessages(); 1038 break; 1039 case LIST_FOOTER_MODE_SEND: 1040 onSendPendingMessages(); 1041 break; 1042 } 1043 } 1044 1045 /** 1046 * Async task for finding a single mailbox by type (possibly even going to the network). 1047 * 1048 * This is much too complex, as implemented. It uses this AsyncTask to check for a mailbox, 1049 * then (if not found) a Controller call to refresh mailboxes from the server, and a handler 1050 * to relaunch this task (a 2nd time) to read the results of the network refresh. The core 1051 * problem is that we have two different non-UI-thread jobs (reading DB and reading network) 1052 * and two different paradigms for dealing with them. Some unification would be needed here 1053 * to make this cleaner. 1054 * 1055 * TODO: If this problem spreads to other operations, find a cleaner way to handle it. 1056 */ 1057 private class FindMailboxTask extends AsyncTask<Void, Void, Long> { 1058 1059 private final long mAccountId; 1060 private final int mMailboxType; 1061 private final boolean mOkToRecurse; 1062 1063 private static final int ACTION_DEFAULT = 0; 1064 private static final int SHOW_WELCOME_ACTIVITY = 1; 1065 private static final int SHOW_SECURITY_ACTIVITY = 2; 1066 private static final int START_NETWORK_LOOK_UP = 3; 1067 private int mAction = ACTION_DEFAULT; 1068 1069 /** 1070 * Special constructor to cache some local info 1071 */ 1072 public FindMailboxTask(long accountId, int mailboxType, boolean okToRecurse) { 1073 mAccountId = accountId; 1074 mMailboxType = mailboxType; 1075 mOkToRecurse = okToRecurse; 1076 } 1077 1078 @Override 1079 protected Long doInBackground(Void... params) { 1080 // Quick check that account is not in security hold 1081 if (mAccountId != -1 && isSecurityHold(mAccountId)) { 1082 mAction = SHOW_SECURITY_ACTIVITY; 1083 return Mailbox.NO_MAILBOX; 1084 } 1085 // See if we can find the requested mailbox in the DB. 1086 long mailboxId = Mailbox.findMailboxOfType(MessageList.this, mAccountId, mMailboxType); 1087 if (mailboxId == Mailbox.NO_MAILBOX) { 1088 // Mailbox not found. Does the account really exists? 1089 final boolean accountExists = Account.isValidId(MessageList.this, mAccountId); 1090 if (accountExists && mOkToRecurse) { 1091 // launch network lookup 1092 mAction = START_NETWORK_LOOK_UP; 1093 } else { 1094 // We don't want to do the network lookup, or the account doesn't exist in the 1095 // first place. 1096 mAction = SHOW_WELCOME_ACTIVITY; 1097 } 1098 } 1099 return mailboxId; 1100 } 1101 1102 @Override 1103 protected void onPostExecute(Long mailboxId) { 1104 switch (mAction) { 1105 case SHOW_SECURITY_ACTIVITY: 1106 // launch the security setup activity 1107 Intent i = AccountSecurity.actionUpdateSecurityIntent( 1108 MessageList.this, mAccountId); 1109 MessageList.this.startActivityForResult(i, REQUEST_SECURITY); 1110 return; 1111 case SHOW_WELCOME_ACTIVITY: 1112 // Let the Welcome activity show the default screen. 1113 Welcome.actionStart(MessageList.this); 1114 finish(); 1115 return; 1116 case START_NETWORK_LOOK_UP: 1117 mControllerCallback.getWrappee().presetMailboxListCallback( 1118 mMailboxType, mAccountId); 1119 1120 // TODO updateMailboxList accessed DB, so we shouldn't call on the UI thread, 1121 // but we should fix the Controller side. (Other Controller methods too access 1122 // DB but are called on the UI thread.) 1123 mController.updateMailboxList(mAccountId, mControllerCallback); 1124 return; 1125 default: 1126 // At this point, mailboxId != NO_MAILBOX 1127 mMailboxId = mailboxId; 1128 mSetTitleTask = new SetTitleTask(mMailboxId); 1129 mSetTitleTask.execute(); 1130 mLoadMessagesTask = new LoadMessagesTask(mMailboxId, mAccountId); 1131 mLoadMessagesTask.execute(); 1132 return; 1133 } 1134 } 1135 } 1136 1137 /** 1138 * Check a single account for security hold status. Do not call from UI thread. 1139 */ 1140 private boolean isSecurityHold(long accountId) { 1141 Cursor c = MessageList.this.getContentResolver().query( 1142 ContentUris.withAppendedId(Account.CONTENT_URI, accountId), 1143 ACCOUNT_INFO_PROJECTION, null, null, null); 1144 try { 1145 if (c.moveToFirst()) { 1146 int flags = c.getInt(ACCOUNT_INFO_COLUMN_FLAGS); 1147 if ((flags & Account.FLAGS_SECURITY_HOLD) != 0) { 1148 return true; 1149 } 1150 } 1151 } finally { 1152 c.close(); 1153 } 1154 return false; 1155 } 1156 1157 /** 1158 * Handle the eventual result from the security update activity 1159 * 1160 * Note, this is extremely coarse, and it simply returns the user to the Accounts list. 1161 * Anything more requires refactoring of this Activity. 1162 */ 1163 @Override 1164 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 1165 switch (requestCode) { 1166 case REQUEST_SECURITY: 1167 onAccounts(); 1168 } 1169 super.onActivityResult(requestCode, resultCode, data); 1170 } 1171 1172 /** 1173 * Async task for loading a single folder out of the UI thread 1174 * 1175 * The code here (for merged boxes) is a placeholder/hack and should be replaced. Some 1176 * specific notes: 1177 * TODO: Move the double query into a specialized URI that returns all inbox messages 1178 * and do the dirty work in raw SQL in the provider. 1179 * TODO: Generalize the query generation so we can reuse it in MessageView (for next/prev) 1180 */ 1181 private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> { 1182 1183 private long mMailboxKey; 1184 private long mAccountKey; 1185 1186 /** 1187 * Special constructor to cache some local info 1188 */ 1189 public LoadMessagesTask(long mailboxKey, long accountKey) { 1190 mMailboxKey = mailboxKey; 1191 mAccountKey = accountKey; 1192 } 1193 1194 @Override 1195 protected Cursor doInBackground(Void... params) { 1196 String selection = 1197 Utility.buildMailboxIdSelection(MessageList.this.mResolver, mMailboxKey); 1198 Cursor c = MessageList.this.managedQuery( 1199 EmailContent.Message.CONTENT_URI, MESSAGE_PROJECTION, 1200 selection, null, EmailContent.MessageColumns.TIMESTAMP + " DESC"); 1201 return c; 1202 } 1203 1204 @Override 1205 protected void onPostExecute(Cursor cursor) { 1206 if (cursor == null || cursor.isClosed()) { 1207 return; 1208 } 1209 MessageList.this.mListAdapter.changeCursor(cursor); 1210 // changeCursor occurs the jumping of position in ListView, so it's need to restore 1211 // the position; 1212 restoreListPosition(); 1213 autoRefreshStaleMailbox(); 1214 // Reset the "new messages" count in the service, since we're seeing them now 1215 if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) { 1216 MailService.resetNewMessageCount(MessageList.this, -1); 1217 } else if (mMailboxKey >= 0 && mAccountKey != -1) { 1218 MailService.resetNewMessageCount(MessageList.this, mAccountKey); 1219 } 1220 } 1221 } 1222 1223 private class SetTitleTask extends AsyncTask<Void, Void, Object[]> { 1224 1225 private long mMailboxKey; 1226 1227 public SetTitleTask(long mailboxKey) { 1228 mMailboxKey = mailboxKey; 1229 } 1230 1231 @Override 1232 protected Object[] doInBackground(Void... params) { 1233 // Check special Mailboxes 1234 int resIdSpecialMailbox = 0; 1235 if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) { 1236 resIdSpecialMailbox = R.string.account_folder_list_summary_inbox; 1237 } else if (mMailboxKey == Mailbox.QUERY_ALL_FAVORITES) { 1238 resIdSpecialMailbox = R.string.account_folder_list_summary_starred; 1239 } else if (mMailboxKey == Mailbox.QUERY_ALL_DRAFTS) { 1240 resIdSpecialMailbox = R.string.account_folder_list_summary_drafts; 1241 } else if (mMailboxKey == Mailbox.QUERY_ALL_OUTBOX) { 1242 resIdSpecialMailbox = R.string.account_folder_list_summary_outbox; 1243 } 1244 if (resIdSpecialMailbox != 0) { 1245 return new Object[] {null, getString(resIdSpecialMailbox), 0}; 1246 } 1247 1248 String accountName = null; 1249 String mailboxName = null; 1250 String accountKey = null; 1251 Cursor c = MessageList.this.mResolver.query(Mailbox.CONTENT_URI, 1252 MAILBOX_NAME_PROJECTION, ID_SELECTION, 1253 new String[] { Long.toString(mMailboxKey) }, null); 1254 try { 1255 if (c.moveToFirst()) { 1256 mailboxName = Utility.FolderProperties.getInstance(MessageList.this) 1257 .getDisplayName(c.getInt(MAILBOX_NAME_COLUMN_TYPE)); 1258 if (mailboxName == null) { 1259 mailboxName = c.getString(MAILBOX_NAME_COLUMN_ID); 1260 } 1261 accountKey = c.getString(MAILBOX_NAME_COLUMN_ACCOUNT_KEY); 1262 } 1263 } finally { 1264 c.close(); 1265 } 1266 if (accountKey != null) { 1267 c = MessageList.this.mResolver.query(Account.CONTENT_URI, 1268 ACCOUNT_NAME_PROJECTION, ID_SELECTION, new String[] { accountKey }, 1269 null); 1270 try { 1271 if (c.moveToFirst()) { 1272 accountName = c.getString(ACCOUNT_DISPLAY_NAME_COLUMN_ID); 1273 } 1274 } finally { 1275 c.close(); 1276 } 1277 } 1278 int nAccounts = EmailContent.count(MessageList.this, Account.CONTENT_URI, null, null); 1279 return new Object[] {accountName, mailboxName, nAccounts}; 1280 } 1281 1282 @Override 1283 protected void onPostExecute(Object[] result) { 1284 if (result == null) { 1285 return; 1286 } 1287 1288 final int nAccounts = (Integer) result[2]; 1289 if (result[0] != null) { 1290 setTitleAccountName((String) result[0], nAccounts > 1); 1291 } 1292 1293 if (result[1] != null) { 1294 mLeftTitle.setText((String) result[1]); 1295 } 1296 } 1297 } 1298 1299 private void setTitleAccountName(String accountName, boolean showAccountsButton) { 1300 TextView accountsButton = (TextView) findViewById(R.id.account_title_button); 1301 TextView textPlain = (TextView) findViewById(R.id.title_right_text); 1302 if (showAccountsButton) { 1303 accountsButton.setVisibility(View.VISIBLE); 1304 textPlain.setVisibility(View.GONE); 1305 accountsButton.setText(accountName); 1306 } else { 1307 accountsButton.setVisibility(View.GONE); 1308 textPlain.setVisibility(View.VISIBLE); 1309 textPlain.setText(accountName); 1310 } 1311 } 1312 1313 private void showProgressIcon(boolean show) { 1314 int visibility = show ? View.VISIBLE : View.GONE; 1315 mProgressIcon.setVisibility(visibility); 1316 if (mListFooterProgress != null) { 1317 mListFooterProgress.setVisibility(visibility); 1318 } 1319 setListFooterText(show); 1320 } 1321 1322 private void lookupMailboxType(long accountId, int mailboxType) { 1323 // kill running async task, if any 1324 Utility.cancelTaskInterrupt(mFindMailboxTask); 1325 // start new one. do not recurse back to controller. 1326 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false); 1327 mFindMailboxTask.execute(); 1328 } 1329 1330 private void showErrorBanner(String message) { 1331 boolean isVisible = mErrorBanner.getVisibility() == View.VISIBLE; 1332 if (message != null) { 1333 mErrorBanner.setText(message); 1334 if (!isVisible) { 1335 mErrorBanner.setVisibility(View.VISIBLE); 1336 mErrorBanner.startAnimation( 1337 AnimationUtils.loadAnimation( 1338 MessageList.this, R.anim.header_appear)); 1339 } 1340 } else { 1341 if (isVisible) { 1342 mErrorBanner.setVisibility(View.GONE); 1343 mErrorBanner.startAnimation( 1344 AnimationUtils.loadAnimation( 1345 MessageList.this, R.anim.header_disappear)); 1346 } 1347 } 1348 } 1349 1350 /** 1351 * Controller results listener. We wrap it with {@link ControllerResultUiThreadWrapper}, 1352 * so all methods are called on the UI thread. 1353 */ 1354 private class ControllerResults implements Controller.Result { 1355 1356 // This is used to alter the connection banner operation for sending messages 1357 MessagingException mSendMessageException; 1358 1359 // These values are set by FindMailboxTask. 1360 private int mWaitForMailboxType = -1; 1361 private long mWaitForMailboxAccount = -1; 1362 1363 public void presetMailboxListCallback(int mailboxType, long accountId) { 1364 mWaitForMailboxType = mailboxType; 1365 mWaitForMailboxAccount = accountId; 1366 } 1367 1368 public void updateMailboxListCallback(MessagingException result, 1369 long accountKey, int progress) { 1370 // updateMailboxList is never the end goal in MessageList, so we don't show 1371 // these errors. There are a couple of corner cases that we miss reporting, but 1372 // this is better than reporting a number of non-problem intermediate states. 1373 // updateBanner(result, progress, mMailboxId); 1374 1375 updateProgress(result, progress); 1376 if (progress == 100 && accountKey == mWaitForMailboxAccount) { 1377 mWaitForMailboxAccount = -1; 1378 lookupMailboxType(accountKey, mWaitForMailboxType); 1379 } 1380 } 1381 1382 // TODO check accountKey and only react to relevant notifications 1383 public void updateMailboxCallback(MessagingException result, long accountKey, 1384 long mailboxKey, int progress, int numNewMessages) { 1385 updateBanner(result, progress, mailboxKey); 1386 if (result != null || progress == 100) { 1387 Email.updateMailboxRefreshTime(mailboxKey); 1388 } 1389 updateProgress(result, progress); 1390 } 1391 1392 public void loadMessageForViewCallback(MessagingException result, long messageId, 1393 int progress) { 1394 } 1395 1396 public void loadAttachmentCallback(MessagingException result, long messageId, 1397 long attachmentId, int progress) { 1398 } 1399 1400 public void serviceCheckMailCallback(MessagingException result, long accountId, 1401 long mailboxId, int progress, long tag) { 1402 } 1403 1404 /** 1405 * We alter the updateBanner hysteresis here to capture any failures and handle 1406 * them just once at the end. This callback is overly overloaded: 1407 * result == null, messageId == -1, progress == 0: start batch send 1408 * result == null, messageId == xx, progress == 0: start sending one message 1409 * result == xxxx, messageId == xx, progress == 0; failed sending one message 1410 * result == null, messageId == -1, progres == 100; finish sending batch 1411 */ 1412 public void sendMailCallback(MessagingException result, long accountId, long messageId, 1413 int progress) { 1414 if (mListFooterMode == LIST_FOOTER_MODE_SEND) { 1415 // reset captured error when we start sending one or more messages 1416 if (messageId == -1 && result == null && progress == 0) { 1417 mSendMessageException = null; 1418 } 1419 // capture first exception that comes along 1420 if (result != null && mSendMessageException == null) { 1421 mSendMessageException = result; 1422 } 1423 // if we're completing the sequence, change the banner state 1424 if (messageId == -1 && progress == 100) { 1425 updateBanner(mSendMessageException, progress, mMailboxId); 1426 } 1427 // always update the spinner, which has less state to worry about 1428 updateProgress(result, progress); 1429 } 1430 } 1431 1432 private void updateProgress(MessagingException result, int progress) { 1433 showProgressIcon(result == null && progress < 100); 1434 } 1435 1436 /** 1437 * Show or hide the connection error banner, and convert the various MessagingException 1438 * variants into localizable text. There is hysteresis in the show/hide logic: Once shown, 1439 * the banner will remain visible until some progress is made on the connection. The 1440 * goal is to keep it from flickering during retries in a bad connection state. 1441 * 1442 * @param result 1443 * @param progress 1444 */ 1445 private void updateBanner(MessagingException result, int progress, long mailboxKey) { 1446 if (mailboxKey != mMailboxId) { 1447 return; 1448 } 1449 if (result != null) { 1450 int id = R.string.status_network_error; 1451 if (result instanceof AuthenticationFailedException) { 1452 id = R.string.account_setup_failed_dlg_auth_message; 1453 } else if (result instanceof CertificateValidationException) { 1454 id = R.string.account_setup_failed_dlg_certificate_message; 1455 } else { 1456 switch (result.getExceptionType()) { 1457 case MessagingException.IOERROR: 1458 id = R.string.account_setup_failed_ioerror; 1459 break; 1460 case MessagingException.TLS_REQUIRED: 1461 id = R.string.account_setup_failed_tls_required; 1462 break; 1463 case MessagingException.AUTH_REQUIRED: 1464 id = R.string.account_setup_failed_auth_required; 1465 break; 1466 case MessagingException.GENERAL_SECURITY: 1467 id = R.string.account_setup_failed_security; 1468 break; 1469 // TODO Generate a unique string for this case, which is the case 1470 // where the security policy needs to be updated. 1471 case MessagingException.SECURITY_POLICIES_REQUIRED: 1472 id = R.string.account_setup_failed_security; 1473 break; 1474 } 1475 } 1476 showErrorBanner(getString(id)); 1477 } else if (progress > 0) { 1478 showErrorBanner(null); 1479 } 1480 } 1481 } 1482 1483 public void onAdapterRequery() { 1484 if (mMultiSelectPanel.getVisibility() == View.VISIBLE) { 1485 updateFooterButtonNames(); 1486 } 1487 } 1488 1489 public void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite) { 1490 onSetMessageFavorite(itemView.mMessageId, newFavorite); 1491 } 1492 1493 public void onAdapterSelectedChanged(MessageListItem itemView, boolean newSelected, 1494 int mSelectedCount) { 1495 if (mSelectedCount == 1 && newSelected) { 1496 mFirstSelectedItemPosition = getListView().getPositionForView(itemView); 1497 mFirstSelectedItemTop = itemView.getBottom(); 1498 mFirstSelectedItemHeight = itemView.getHeight(); 1499 } else { 1500 mFirstSelectedItemPosition = -1; 1501 } 1502 1503 showMultiPanel(mSelectedCount > 0); 1504 } 1505} 1506