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