MessageList.java revision bcf32320e2600e96c8a9e997a8903bfc3893b35e
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.service.MailService; 35 36import android.app.Activity; 37import android.app.NotificationManager; 38import android.content.ContentResolver; 39import android.content.ContentUris; 40import android.content.Context; 41import android.content.Intent; 42import android.database.Cursor; 43import android.net.Uri; 44import android.os.AsyncTask; 45import android.os.Bundle; 46import android.os.Handler; 47import android.view.Menu; 48import android.view.MenuItem; 49import android.view.View; 50import android.view.View.OnClickListener; 51import android.view.animation.Animation; 52import android.view.animation.AnimationUtils; 53import android.view.animation.Animation.AnimationListener; 54import android.widget.Button; 55import android.widget.ProgressBar; 56import android.widget.TextView; 57 58public class MessageList extends Activity implements OnClickListener, 59 AnimationListener, MessageListFragment.Callback { 60 // Intent extras (internal to this activity) 61 private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID"; 62 private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE"; 63 private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID"; 64 65 private static final int REQUEST_SECURITY = 0; 66 67 // UI support 68 private MessageListFragment mListFragment; 69 private View mMultiSelectPanel; 70 private Button mReadUnreadButton; 71 private Button mFavoriteButton; 72 private Button mDeleteButton; 73 private TextView mErrorBanner; 74 75 private final Controller mController = Controller.getInstance(getApplication()); 76 private ControllerResultUiThreadWrapper<ControllerResults> mControllerCallback; 77 78 private TextView mLeftTitle; 79 private ProgressBar mProgressIcon; 80 81 // DB access 82 private ContentResolver mResolver; 83 private FindMailboxTask mFindMailboxTask; 84 private SetTitleTask mSetTitleTask; 85 86 private static final int MAILBOX_NAME_COLUMN_ID = 0; 87 private static final int MAILBOX_NAME_COLUMN_ACCOUNT_KEY = 1; 88 private static final int MAILBOX_NAME_COLUMN_TYPE = 2; 89 private static final String[] MAILBOX_NAME_PROJECTION = new String[] { 90 MailboxColumns.DISPLAY_NAME, MailboxColumns.ACCOUNT_KEY, 91 MailboxColumns.TYPE}; 92 93 private static final int ACCOUNT_DISPLAY_NAME_COLUMN_ID = 0; 94 private static final String[] ACCOUNT_NAME_PROJECTION = new String[] { 95 AccountColumns.DISPLAY_NAME }; 96 97 private static final String ID_SELECTION = EmailContent.RECORD_ID + "=?"; 98 99 /* package */ MessageListFragment getListFragmentForTest() { 100 return mListFragment; 101 } 102 103 /** 104 * Open a specific mailbox. 105 * 106 * TODO This should just shortcut to a more generic version that can accept a list of 107 * accounts/mailboxes (e.g. merged inboxes). 108 * 109 * @param context 110 * @param id mailbox key 111 */ 112 public static void actionHandleMailbox(Context context, long id) { 113 context.startActivity(createIntent(context, -1, id, -1)); 114 } 115 116 /** 117 * Open a specific mailbox by account & type 118 * 119 * @param context The caller's context (for generating an intent) 120 * @param accountId The account to open 121 * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX) 122 */ 123 public static void actionHandleAccount(Context context, long accountId, int mailboxType) { 124 context.startActivity(createIntent(context, accountId, -1, mailboxType)); 125 } 126 127 /** 128 * Open the inbox of the account with a UUID. It's used to handle old style 129 * (Android <= 1.6) desktop shortcut intents. 130 */ 131 public static void actionOpenAccountInboxUuid(Context context, String accountUuid) { 132 Intent i = createIntent(context, -1, -1, Mailbox.TYPE_INBOX); 133 i.setData(Account.getShortcutSafeUriFromUuid(accountUuid)); 134 context.startActivity(i); 135 } 136 137 /** 138 * Return an intent to open a specific mailbox by account & type. 139 * 140 * @param context The caller's context (for generating an intent) 141 * @param accountId The account to open, or -1 142 * @param mailboxId the ID of the mailbox to open, or -1 143 * @param mailboxType the type of mailbox to open (e.g. @see Mailbox.TYPE_INBOX) or -1 144 */ 145 public static Intent createIntent(Context context, long accountId, long mailboxId, 146 int mailboxType) { 147 Intent intent = new Intent(context, MessageList.class); 148 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 149 if (accountId != -1) intent.putExtra(EXTRA_ACCOUNT_ID, accountId); 150 if (mailboxId != -1) intent.putExtra(EXTRA_MAILBOX_ID, mailboxId); 151 if (mailboxType != -1) intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType); 152 return intent; 153 } 154 155 /** 156 * Create and return an intent for a desktop shortcut for an account. 157 * 158 * @param context Calling context for building the intent 159 * @param account The account of interest 160 * @param mailboxType The folder name to open (typically Mailbox.TYPE_INBOX) 161 * @return an Intent which can be used to view that account 162 */ 163 public static Intent createAccountIntentForShortcut(Context context, Account account, 164 int mailboxType) { 165 Intent i = createIntent(context, -1, -1, mailboxType); 166 i.setData(account.getShortcutSafeUri()); 167 return i; 168 } 169 170 @Override 171 public void onCreate(Bundle icicle) { 172 super.onCreate(icicle); 173 setContentView(R.layout.message_list); 174 175 mControllerCallback = new ControllerResultUiThreadWrapper<ControllerResults>( 176 new Handler(), new ControllerResults()); 177 mListFragment = (MessageListFragment) findFragmentById(R.id.message_list_fragment); 178 mMultiSelectPanel = findViewById(R.id.footer_organize); 179 mReadUnreadButton = (Button) findViewById(R.id.btn_read_unread); 180 mFavoriteButton = (Button) findViewById(R.id.btn_multi_favorite); 181 mDeleteButton = (Button) findViewById(R.id.btn_multi_delete); 182 mLeftTitle = (TextView) findViewById(R.id.title_left_text); 183 mProgressIcon = (ProgressBar) findViewById(R.id.title_progress_icon); 184 mErrorBanner = (TextView) findViewById(R.id.connection_error_text); 185 186 mReadUnreadButton.setOnClickListener(this); 187 mFavoriteButton.setOnClickListener(this); 188 mDeleteButton.setOnClickListener(this); 189 ((Button) findViewById(R.id.account_title_button)).setOnClickListener(this); 190 191 mListFragment.setCallback(this); 192 193 mResolver = getContentResolver(); 194 195 // Show the appropriate account/mailbox specified by an {@link Intent}. 196 selectAccountAndMailbox(getIntent()); 197 } 198 199 /** 200 * Show the appropriate account/mailbox specified by an {@link Intent}. 201 */ 202 private void selectAccountAndMailbox(Intent intent) { 203 long mailboxId = intent.getLongExtra(EXTRA_MAILBOX_ID, -1); 204 if (mailboxId != -1) { 205 // Specific mailbox ID was provided - go directly to it 206 mSetTitleTask = new SetTitleTask(mailboxId); 207 mSetTitleTask.execute(); 208 mListFragment.openMailbox(-1, mailboxId); 209 } else { 210 int mailboxType = intent.getIntExtra(EXTRA_MAILBOX_TYPE, Mailbox.TYPE_INBOX); 211 Uri uri = intent.getData(); 212 // TODO Possible ANR. getAccountIdFromShortcutSafeUri accesses DB. 213 long accountId = (uri == null) ? -1 214 : Account.getAccountIdFromShortcutSafeUri(this, uri); 215 216 // TODO Both branches have almost identical code. Unify them. 217 // (And why not "okay to recurse" in the first case?) 218 if (accountId != -1) { 219 // A content URI was provided - try to look up the account 220 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false); 221 mFindMailboxTask.execute(); 222 } else { 223 // Go by account id + type 224 accountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1); 225 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, true); 226 mFindMailboxTask.execute(); 227 } 228 } 229 // TODO set title to "account > mailbox (#unread)" 230 } 231 232 @Override 233 public void onPause() { 234 super.onPause(); 235 mController.removeResultCallback(mControllerCallback); 236 } 237 238 @Override 239 public void onResume() { 240 super.onResume(); 241 mController.addResultCallback(mControllerCallback); 242 243 // clear notifications here 244 NotificationManager notificationManager = (NotificationManager) 245 getSystemService(Context.NOTIFICATION_SERVICE); 246 notificationManager.cancel(MailService.NOTIFICATION_ID_NEW_MESSAGES); 247 248 // Exit immediately if the accounts list has changed (e.g. externally deleted) 249 if (Email.getNotifyUiAccountsChanged()) { 250 Welcome.actionStart(this); 251 finish(); 252 return; 253 } 254 } 255 256 @Override 257 protected void onDestroy() { 258 super.onDestroy(); 259 260 Utility.cancelTaskInterrupt(mFindMailboxTask); 261 mFindMailboxTask = null; 262 Utility.cancelTaskInterrupt(mSetTitleTask); 263 mSetTitleTask = null; 264 } 265 266 /** 267 * Called when the list fragment can't find mailbox/account. 268 */ 269 public void onMailboxNotFound() { 270 finish(); 271 } 272 273 public void onMessageOpen(final long messageId, final long mailboxId) { 274 final Context context = this; // Make the code shorter. 275 Utility.runAsync(new Runnable() { 276 public void run() { 277 EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(context, 278 mailboxId); 279 if (mailbox == null) { 280 return; 281 } 282 283 if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) { 284 MessageCompose.actionEditDraft(context, messageId); 285 } else { 286 final boolean disableReply = (mailbox.mType == EmailContent.Mailbox.TYPE_TRASH); 287 // WARNING: here we pass getMailboxId(), which can be the negative id of 288 // a compound mailbox, instead of the mailboxId of the particular message that 289 // is opened. This is to support the next/prev buttons on the message view 290 // properly even for combined mailboxes. 291 MessageView.actionView(context, messageId, mListFragment.getMailboxId(), 292 disableReply); 293 } 294 } 295 }); 296 } 297 298 public void onClick(View v) { 299 switch (v.getId()) { 300 case R.id.btn_read_unread: 301 mListFragment.onMultiToggleRead(); 302 break; 303 case R.id.btn_multi_favorite: 304 mListFragment.onMultiToggleFavorite(); 305 break; 306 case R.id.btn_multi_delete: 307 mListFragment.onMultiDelete(); 308 break; 309 case R.id.account_title_button: 310 onAccounts(); 311 break; 312 } 313 } 314 315 public void onAnimationEnd(Animation animation) { 316 mListFragment.updateListPosition(); 317 } 318 319 public void onAnimationRepeat(Animation animation) { 320 } 321 322 public void onAnimationStart(Animation animation) { 323 } 324 325 @Override 326 public boolean onPrepareOptionsMenu(Menu menu) { 327 // Re-create menu every time. (We may not know the mailbox id yet) 328 menu.clear(); 329 if (mListFragment.isMagicMailbox()) { 330 getMenuInflater().inflate(R.menu.message_list_option_smart_folder, menu); 331 } else { 332 getMenuInflater().inflate(R.menu.message_list_option, menu); 333 } 334 boolean showDeselect = mListFragment.getSelectedCount() > 0; 335 menu.setGroupVisible(R.id.deselect_all_group, showDeselect); 336 return true; 337 } 338 339 @Override 340 public boolean onOptionsItemSelected(MenuItem item) { 341 switch (item.getItemId()) { 342 case R.id.refresh: 343 mListFragment.onRefresh(); 344 return true; 345 case R.id.folders: 346 onFolders(); 347 return true; 348 case R.id.accounts: 349 onAccounts(); 350 return true; 351 case R.id.compose: 352 onCompose(); 353 return true; 354 case R.id.account_settings: 355 onEditAccount(); 356 return true; 357 case R.id.deselect_all: 358 mListFragment.onDeselectAll(); 359 return true; 360 default: 361 return super.onOptionsItemSelected(item); 362 } 363 } 364 365 private void onFolders() { 366 if (!mListFragment.isMagicMailbox()) { // Magic boxes don't have "folders" option. 367 // TODO smaller projection 368 Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mListFragment.getMailboxId()); 369 if (mailbox != null) { 370 MailboxList.actionHandleAccount(this, mailbox.mAccountKey); 371 finish(); 372 } 373 } 374 } 375 376 private void onAccounts() { 377 AccountFolderList.actionShowAccounts(this); 378 finish(); 379 } 380 381 private void onCompose() { 382 MessageCompose.actionCompose(this, mListFragment.getAccountId()); 383 } 384 385 private void onEditAccount() { 386 if (!mListFragment.isMagicMailbox()) { // Magic boxes don't have "accout settings" option. 387 AccountSettings.actionSettings(this, mListFragment.getAccountId()); 388 } 389 } 390 391 /** 392 * Show multi-selection panel, if one or more messages are selected. Button labels will be 393 * updated too. 394 */ 395 public void onSelectionChanged() { 396 showMultiPanel(mListFragment.getSelectedCount() > 0); 397 } 398 399 private void updateFooterButtonNames () { 400 // Show "unread_action" when one or more read messages are selected. 401 if (mListFragment.doesSelectionContainReadMessage()) { 402 mReadUnreadButton.setText(R.string.unread_action); 403 } else { 404 mReadUnreadButton.setText(R.string.read_action); 405 } 406 // Show "set_star_action" when one or more un-starred messages are selected. 407 if (mListFragment.doesSelectionContainNonStarredMessage()) { 408 mFavoriteButton.setText(R.string.set_star_action); 409 } else { 410 mFavoriteButton.setText(R.string.remove_star_action); 411 } 412 } 413 414 /** 415 * Show or hide the panel of multi-select options 416 */ 417 private void showMultiPanel(boolean show) { 418 if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) { 419 mMultiSelectPanel.setVisibility(View.VISIBLE); 420 Animation animation = AnimationUtils.loadAnimation(this, R.anim.footer_appear); 421 animation.setAnimationListener(this); 422 mMultiSelectPanel.startAnimation(animation); 423 } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) { 424 mMultiSelectPanel.setVisibility(View.GONE); 425 mMultiSelectPanel.startAnimation( 426 AnimationUtils.loadAnimation(this, R.anim.footer_disappear)); 427 } 428 if (show) { 429 updateFooterButtonNames(); 430 } 431 } 432 433 /** 434 * Async task for finding a single mailbox by type (possibly even going to the network). 435 * 436 * This is much too complex, as implemented. It uses this AsyncTask to check for a mailbox, 437 * then (if not found) a Controller call to refresh mailboxes from the server, and a handler 438 * to relaunch this task (a 2nd time) to read the results of the network refresh. The core 439 * problem is that we have two different non-UI-thread jobs (reading DB and reading network) 440 * and two different paradigms for dealing with them. Some unification would be needed here 441 * to make this cleaner. 442 * 443 * TODO: If this problem spreads to other operations, find a cleaner way to handle it. 444 */ 445 private class FindMailboxTask extends AsyncTask<Void, Void, Long> { 446 447 private final long mAccountId; 448 private final int mMailboxType; 449 private final boolean mOkToRecurse; 450 451 private static final int ACTION_DEFAULT = 0; 452 private static final int SHOW_WELCOME_ACTIVITY = 1; 453 private static final int SHOW_SECURITY_ACTIVITY = 2; 454 private static final int START_NETWORK_LOOK_UP = 3; 455 private int mAction = ACTION_DEFAULT; 456 457 /** 458 * Special constructor to cache some local info 459 */ 460 public FindMailboxTask(long accountId, int mailboxType, boolean okToRecurse) { 461 mAccountId = accountId; 462 mMailboxType = mailboxType; 463 mOkToRecurse = okToRecurse; 464 } 465 466 @Override 467 protected Long doInBackground(Void... params) { 468 // Quick check that account is not in security hold 469 if (mAccountId != -1 && Account.isSecurityHold(MessageList.this, mAccountId)) { 470 mAction = SHOW_SECURITY_ACTIVITY; 471 return Mailbox.NO_MAILBOX; 472 } 473 // See if we can find the requested mailbox in the DB. 474 long mailboxId = Mailbox.findMailboxOfType(MessageList.this, mAccountId, mMailboxType); 475 if (mailboxId == Mailbox.NO_MAILBOX) { 476 // Mailbox not found. Does the account really exists? 477 final boolean accountExists = Account.isValidId(MessageList.this, mAccountId); 478 if (accountExists && mOkToRecurse) { 479 // launch network lookup 480 mAction = START_NETWORK_LOOK_UP; 481 } else { 482 // We don't want to do the network lookup, or the account doesn't exist in the 483 // first place. 484 mAction = SHOW_WELCOME_ACTIVITY; 485 } 486 } 487 return mailboxId; 488 } 489 490 @Override 491 protected void onPostExecute(Long mailboxId) { 492 switch (mAction) { 493 case SHOW_SECURITY_ACTIVITY: 494 // launch the security setup activity 495 Intent i = AccountSecurity.actionUpdateSecurityIntent( 496 MessageList.this, mAccountId); 497 MessageList.this.startActivityForResult(i, REQUEST_SECURITY); 498 return; 499 case SHOW_WELCOME_ACTIVITY: 500 // Let the Welcome activity show the default screen. 501 Welcome.actionStart(MessageList.this); 502 finish(); 503 return; 504 case START_NETWORK_LOOK_UP: 505 mControllerCallback.getWrappee().presetMailboxListCallback( 506 mMailboxType, mAccountId); 507 508 // TODO updateMailboxList accessed DB, so we shouldn't call on the UI thread, 509 // but we should fix the Controller side. (Other Controller methods too access 510 // DB but are called on the UI thread.) 511 mController.updateMailboxList(mAccountId); 512 return; 513 default: 514 // At this point, mailboxId != NO_MAILBOX 515 mSetTitleTask = new SetTitleTask(mailboxId); 516 mSetTitleTask.execute(); 517 mListFragment.openMailbox(mAccountId, mailboxId); 518 return; 519 } 520 } 521 } 522 523 /** 524 * Handle the eventual result from the security update activity 525 * 526 * Note, this is extremely coarse, and it simply returns the user to the Accounts list. 527 * Anything more requires refactoring of this Activity. 528 */ 529 @Override 530 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 531 switch (requestCode) { 532 case REQUEST_SECURITY: 533 onAccounts(); 534 } 535 super.onActivityResult(requestCode, resultCode, data); 536 } 537 538 private class SetTitleTask extends AsyncTask<Void, Void, Object[]> { 539 540 private long mMailboxKey; 541 542 public SetTitleTask(long mailboxKey) { 543 mMailboxKey = mailboxKey; 544 } 545 546 @Override 547 protected Object[] doInBackground(Void... params) { 548 // Check special Mailboxes 549 int resIdSpecialMailbox = 0; 550 if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) { 551 resIdSpecialMailbox = R.string.account_folder_list_summary_inbox; 552 } else if (mMailboxKey == Mailbox.QUERY_ALL_FAVORITES) { 553 resIdSpecialMailbox = R.string.account_folder_list_summary_starred; 554 } else if (mMailboxKey == Mailbox.QUERY_ALL_DRAFTS) { 555 resIdSpecialMailbox = R.string.account_folder_list_summary_drafts; 556 } else if (mMailboxKey == Mailbox.QUERY_ALL_OUTBOX) { 557 resIdSpecialMailbox = R.string.account_folder_list_summary_outbox; 558 } 559 if (resIdSpecialMailbox != 0) { 560 return new Object[] {null, getString(resIdSpecialMailbox), 0}; 561 } 562 563 String accountName = null; 564 String mailboxName = null; 565 String accountKey = null; 566 Cursor c = MessageList.this.mResolver.query(Mailbox.CONTENT_URI, 567 MAILBOX_NAME_PROJECTION, ID_SELECTION, 568 new String[] { Long.toString(mMailboxKey) }, null); 569 try { 570 if (c.moveToFirst()) { 571 mailboxName = Utility.FolderProperties.getInstance(MessageList.this) 572 .getDisplayName(c.getInt(MAILBOX_NAME_COLUMN_TYPE)); 573 if (mailboxName == null) { 574 mailboxName = c.getString(MAILBOX_NAME_COLUMN_ID); 575 } 576 accountKey = c.getString(MAILBOX_NAME_COLUMN_ACCOUNT_KEY); 577 } 578 } finally { 579 c.close(); 580 } 581 if (accountKey != null) { 582 c = MessageList.this.mResolver.query(Account.CONTENT_URI, 583 ACCOUNT_NAME_PROJECTION, ID_SELECTION, new String[] { accountKey }, 584 null); 585 try { 586 if (c.moveToFirst()) { 587 accountName = c.getString(ACCOUNT_DISPLAY_NAME_COLUMN_ID); 588 } 589 } finally { 590 c.close(); 591 } 592 } 593 int nAccounts = EmailContent.count(MessageList.this, Account.CONTENT_URI, null, null); 594 return new Object[] {accountName, mailboxName, nAccounts}; 595 } 596 597 @Override 598 protected void onPostExecute(Object[] result) { 599 if (result == null) { 600 return; 601 } 602 603 final int nAccounts = (Integer) result[2]; 604 if (result[0] != null) { 605 setTitleAccountName((String) result[0], nAccounts > 1); 606 } 607 608 if (result[1] != null) { 609 mLeftTitle.setText((String) result[1]); 610 } 611 } 612 } 613 614 private void setTitleAccountName(String accountName, boolean showAccountsButton) { 615 TextView accountsButton = (TextView) findViewById(R.id.account_title_button); 616 TextView textPlain = (TextView) findViewById(R.id.title_right_text); 617 if (showAccountsButton) { 618 accountsButton.setVisibility(View.VISIBLE); 619 textPlain.setVisibility(View.GONE); 620 accountsButton.setText(accountName); 621 } else { 622 accountsButton.setVisibility(View.GONE); 623 textPlain.setVisibility(View.VISIBLE); 624 textPlain.setText(accountName); 625 } 626 } 627 628 private void showProgressIcon(boolean show) { 629 int visibility = show ? View.VISIBLE : View.GONE; 630 mProgressIcon.setVisibility(visibility); 631 mListFragment.showProgressIcon(show); 632 } 633 634 private void lookupMailboxType(long accountId, int mailboxType) { 635 // kill running async task, if any 636 Utility.cancelTaskInterrupt(mFindMailboxTask); 637 // start new one. do not recurse back to controller. 638 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false); 639 mFindMailboxTask.execute(); 640 } 641 642 private void showErrorBanner(String message) { 643 boolean isVisible = mErrorBanner.getVisibility() == View.VISIBLE; 644 if (message != null) { 645 mErrorBanner.setText(message); 646 if (!isVisible) { 647 mErrorBanner.setVisibility(View.VISIBLE); 648 mErrorBanner.startAnimation( 649 AnimationUtils.loadAnimation( 650 MessageList.this, R.anim.header_appear)); 651 } 652 } else { 653 if (isVisible) { 654 mErrorBanner.setVisibility(View.GONE); 655 mErrorBanner.startAnimation( 656 AnimationUtils.loadAnimation( 657 MessageList.this, R.anim.header_disappear)); 658 } 659 } 660 } 661 662 /** 663 * Controller results listener. We wrap it with {@link ControllerResultUiThreadWrapper}, 664 * so all methods are called on the UI thread. 665 */ 666 private class ControllerResults extends Controller.Result { 667 668 // This is used to alter the connection banner operation for sending messages 669 MessagingException mSendMessageException; 670 671 // These values are set by FindMailboxTask. 672 private int mWaitForMailboxType = -1; 673 private long mWaitForMailboxAccount = -1; 674 675 public void presetMailboxListCallback(int mailboxType, long accountId) { 676 mWaitForMailboxType = mailboxType; 677 mWaitForMailboxAccount = accountId; 678 } 679 680 @Override 681 public void updateMailboxListCallback(MessagingException result, 682 long accountKey, int progress) { 683 // updateMailboxList is never the end goal in MessageList, so we don't show 684 // these errors. There are a couple of corner cases that we miss reporting, but 685 // this is better than reporting a number of non-problem intermediate states. 686 // updateBanner(result, progress, mMailboxId); 687 688 updateProgress(result, progress); 689 if (progress == 100 && accountKey == mWaitForMailboxAccount) { 690 mWaitForMailboxAccount = -1; 691 lookupMailboxType(accountKey, mWaitForMailboxType); 692 } 693 } 694 695 // TODO check accountKey and only react to relevant notifications 696 @Override 697 public void updateMailboxCallback(MessagingException result, long accountKey, 698 long mailboxKey, int progress, int numNewMessages) { 699 updateBanner(result, progress, mailboxKey); 700 if (result != null || progress == 100) { 701 Email.updateMailboxRefreshTime(mailboxKey); 702 } 703 updateProgress(result, progress); 704 } 705 706 /** 707 * We alter the updateBanner hysteresis here to capture any failures and handle 708 * them just once at the end. This callback is overly overloaded: 709 * result == null, messageId == -1, progress == 0: start batch send 710 * result == null, messageId == xx, progress == 0: start sending one message 711 * result == xxxx, messageId == xx, progress == 0; failed sending one message 712 * result == null, messageId == -1, progres == 100; finish sending batch 713 */ 714 @Override 715 public void sendMailCallback(MessagingException result, long accountId, long messageId, 716 int progress) { 717 if (mListFragment.isOutbox()) { 718 // reset captured error when we start sending one or more messages 719 if (messageId == -1 && result == null && progress == 0) { 720 mSendMessageException = null; 721 } 722 // capture first exception that comes along 723 if (result != null && mSendMessageException == null) { 724 mSendMessageException = result; 725 } 726 // if we're completing the sequence, change the banner state 727 if (messageId == -1 && progress == 100) { 728 updateBanner(mSendMessageException, progress, mListFragment.getMailboxId()); 729 } 730 // always update the spinner, which has less state to worry about 731 updateProgress(result, progress); 732 } 733 } 734 735 private void updateProgress(MessagingException result, int progress) { 736 showProgressIcon(result == null && progress < 100); 737 } 738 739 /** 740 * Show or hide the connection error banner, and convert the various MessagingException 741 * variants into localizable text. There is hysteresis in the show/hide logic: Once shown, 742 * the banner will remain visible until some progress is made on the connection. The 743 * goal is to keep it from flickering during retries in a bad connection state. 744 * 745 * @param result 746 * @param progress 747 */ 748 private void updateBanner(MessagingException result, int progress, long mailboxKey) { 749 if (mailboxKey != mListFragment.getMailboxId()) { 750 return; 751 } 752 if (result != null) { 753 int id = R.string.status_network_error; 754 if (result instanceof AuthenticationFailedException) { 755 id = R.string.account_setup_failed_dlg_auth_message; 756 } else if (result instanceof CertificateValidationException) { 757 id = R.string.account_setup_failed_dlg_certificate_message; 758 } else { 759 switch (result.getExceptionType()) { 760 case MessagingException.IOERROR: 761 id = R.string.account_setup_failed_ioerror; 762 break; 763 case MessagingException.TLS_REQUIRED: 764 id = R.string.account_setup_failed_tls_required; 765 break; 766 case MessagingException.AUTH_REQUIRED: 767 id = R.string.account_setup_failed_auth_required; 768 break; 769 case MessagingException.GENERAL_SECURITY: 770 id = R.string.account_setup_failed_security; 771 break; 772 // TODO Generate a unique string for this case, which is the case 773 // where the security policy needs to be updated. 774 case MessagingException.SECURITY_POLICIES_REQUIRED: 775 id = R.string.account_setup_failed_security; 776 break; 777 } 778 } 779 showErrorBanner(getString(id)); 780 } else if (progress > 0) { 781 showErrorBanner(null); 782 } 783 } 784 } 785} 786