MessageList.java revision 899c5b866192a4c4a12413446d10e5d98dbf94fa
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.NotificationController; 23import com.android.email.R; 24import com.android.email.Utility; 25import com.android.email.activity.setup.AccountSecurity; 26import com.android.email.activity.setup.AccountSettingsXL; 27import com.android.email.mail.MessagingException; 28import com.android.email.provider.EmailContent; 29import com.android.email.provider.EmailContent.Account; 30import com.android.email.provider.EmailContent.AccountColumns; 31import com.android.email.provider.EmailContent.Mailbox; 32import com.android.email.provider.EmailContent.MailboxColumns; 33 34import android.app.Activity; 35import android.content.ContentResolver; 36import android.content.Context; 37import android.content.Intent; 38import android.database.Cursor; 39import android.net.Uri; 40import android.os.AsyncTask; 41import android.os.Bundle; 42import android.os.Handler; 43import android.view.Menu; 44import android.view.MenuItem; 45import android.view.View; 46import android.view.View.OnClickListener; 47import android.view.animation.Animation; 48import android.view.animation.Animation.AnimationListener; 49import android.view.animation.AnimationUtils; 50import android.widget.Button; 51import android.widget.ProgressBar; 52import android.widget.TextView; 53 54public class MessageList extends Activity implements OnClickListener, 55 AnimationListener, MessageListFragment.Callback { 56 // Intent extras (internal to this activity) 57 private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID"; 58 private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE"; 59 private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID"; 60 61 private static final int REQUEST_SECURITY = 0; 62 63 // UI support 64 private MessageListFragment mListFragment; 65 private View mMultiSelectPanel; 66 private Button mReadUnreadButton; 67 private Button mFavoriteButton; 68 private Button mDeleteButton; 69 private TextView mErrorBanner; 70 71 private final Controller mController = Controller.getInstance(getApplication()); 72 private ControllerResultUiThreadWrapper<ControllerResults> mControllerCallback; 73 74 private TextView mLeftTitle; 75 private ProgressBar mProgressIcon; 76 77 // DB access 78 private ContentResolver mResolver; 79 private SetTitleTask mSetTitleTask; 80 81 private MailboxFinder mMailboxFinder; 82 private MailboxFinderCallback mMailboxFinderCallback = new MailboxFinderCallback(); 83 84 private static final int MAILBOX_NAME_COLUMN_ID = 0; 85 private static final int MAILBOX_NAME_COLUMN_ACCOUNT_KEY = 1; 86 private static final int MAILBOX_NAME_COLUMN_TYPE = 2; 87 private static final String[] MAILBOX_NAME_PROJECTION = new String[] { 88 MailboxColumns.DISPLAY_NAME, MailboxColumns.ACCOUNT_KEY, 89 MailboxColumns.TYPE}; 90 91 private static final int ACCOUNT_DISPLAY_NAME_COLUMN_ID = 0; 92 private static final String[] ACCOUNT_NAME_PROJECTION = new String[] { 93 AccountColumns.DISPLAY_NAME }; 94 95 private static final String ID_SELECTION = EmailContent.RECORD_ID + "=?"; 96 97 /* package */ MessageListFragment getListFragmentForTest() { 98 return mListFragment; 99 } 100 101 /** 102 * Open a specific mailbox. 103 * 104 * TODO This should just shortcut to a more generic version that can accept a list of 105 * accounts/mailboxes (e.g. merged inboxes). 106 * 107 * @param context 108 * @param id mailbox key 109 */ 110 public static void actionHandleMailbox(Context context, long id) { 111 context.startActivity(createIntent(context, -1, id, -1)); 112 } 113 114 /** 115 * Open a specific mailbox by account & type 116 * 117 * @param context The caller's context (for generating an intent) 118 * @param accountId The account to open 119 * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX) 120 */ 121 public static void actionHandleAccount(Context context, long accountId, int mailboxType) { 122 context.startActivity(createIntent(context, accountId, -1, mailboxType)); 123 } 124 125 /** 126 * Open the inbox of the account with a UUID. It's used to handle old style 127 * (Android <= 1.6) desktop shortcut intents. 128 */ 129 public static void actionOpenAccountInboxUuid(Context context, String accountUuid) { 130 Intent i = createIntent(context, -1, -1, Mailbox.TYPE_INBOX); 131 i.setData(Account.getShortcutSafeUriFromUuid(accountUuid)); 132 context.startActivity(i); 133 } 134 135 /** 136 * Return an intent to open a specific mailbox by account & type. 137 * 138 * @param context The caller's context (for generating an intent) 139 * @param accountId The account to open, or -1 140 * @param mailboxId the ID of the mailbox to open, or -1 141 * @param mailboxType the type of mailbox to open (e.g. @see Mailbox.TYPE_INBOX) or -1 142 */ 143 public static Intent createIntent(Context context, long accountId, long mailboxId, 144 int mailboxType) { 145 Intent intent = new Intent(context, MessageList.class); 146 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 147 if (accountId != -1) intent.putExtra(EXTRA_ACCOUNT_ID, accountId); 148 if (mailboxId != -1) intent.putExtra(EXTRA_MAILBOX_ID, mailboxId); 149 if (mailboxType != -1) intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType); 150 return intent; 151 } 152 153 /** 154 * Create and return an intent for a desktop shortcut for an account. 155 * 156 * @param context Calling context for building the intent 157 * @param account The account of interest 158 * @param mailboxType The folder name to open (typically Mailbox.TYPE_INBOX) 159 * @return an Intent which can be used to view that account 160 */ 161 public static Intent createAccountIntentForShortcut(Context context, Account account, 162 int mailboxType) { 163 Intent i = createIntent(context, -1, -1, mailboxType); 164 i.setData(account.getShortcutSafeUri()); 165 return i; 166 } 167 168 @Override 169 public void onCreate(Bundle icicle) { 170 super.onCreate(icicle); 171 setContentView(R.layout.message_list); 172 173 mControllerCallback = new ControllerResultUiThreadWrapper<ControllerResults>( 174 new Handler(), new ControllerResults()); 175 mListFragment = (MessageListFragment) findFragmentById(R.id.message_list_fragment); 176 mMultiSelectPanel = findViewById(R.id.footer_organize); 177 mReadUnreadButton = (Button) findViewById(R.id.btn_read_unread); 178 mFavoriteButton = (Button) findViewById(R.id.btn_multi_favorite); 179 mDeleteButton = (Button) findViewById(R.id.btn_multi_delete); 180 mLeftTitle = (TextView) findViewById(R.id.title_left_text); 181 mProgressIcon = (ProgressBar) findViewById(R.id.title_progress_icon); 182 mErrorBanner = (TextView) findViewById(R.id.connection_error_text); 183 184 mReadUnreadButton.setOnClickListener(this); 185 mFavoriteButton.setOnClickListener(this); 186 mDeleteButton.setOnClickListener(this); 187 ((Button) findViewById(R.id.account_title_button)).setOnClickListener(this); 188 189 mListFragment.setCallback(this); 190 191 mResolver = getContentResolver(); 192 193 // Show the appropriate account/mailbox specified by an {@link Intent}. 194 selectAccountAndMailbox(getIntent()); 195 } 196 197 /** 198 * Show the appropriate account/mailbox specified by an {@link Intent}. 199 */ 200 private void selectAccountAndMailbox(Intent intent) { 201 long mailboxId = intent.getLongExtra(EXTRA_MAILBOX_ID, -1); 202 if (mailboxId != -1) { 203 // Specific mailbox ID was provided - go directly to it 204 mSetTitleTask = new SetTitleTask(mailboxId); 205 mSetTitleTask.execute(); 206 mListFragment.openMailbox(mailboxId); 207 } else { 208 int mailboxType = intent.getIntExtra(EXTRA_MAILBOX_TYPE, Mailbox.TYPE_INBOX); 209 Uri uri = intent.getData(); 210 // TODO Possible ANR. getAccountIdFromShortcutSafeUri accesses DB. 211 long accountId = (uri == null) ? -1 212 : Account.getAccountIdFromShortcutSafeUri(this, uri); 213 if (accountId == -1) { 214 accountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1); 215 } 216 if (accountId == -1) { 217 launchWelcomeAndFinish(); 218 return; 219 } 220 mMailboxFinder = new MailboxFinder(this, accountId, mailboxType, 221 mMailboxFinderCallback); 222 mMailboxFinder.startLookup(); 223 } 224 // TODO set title to "account > mailbox (#unread)" 225 } 226 227 @Override 228 public void onPause() { 229 super.onPause(); 230 mController.removeResultCallback(mControllerCallback); 231 } 232 233 @Override 234 public void onResume() { 235 super.onResume(); 236 mController.addResultCallback(mControllerCallback); 237 238 // clear notifications here 239 NotificationController.getInstance(this).cancelNewMessageNotification( 240 mListFragment.getAccountId()); 241 242 // Exit immediately if the accounts list has changed (e.g. externally deleted) 243 if (Email.getNotifyUiAccountsChanged()) { 244 Welcome.actionStart(this); 245 finish(); 246 return; 247 } 248 } 249 250 @Override 251 protected void onDestroy() { 252 super.onDestroy(); 253 254 if (mMailboxFinder != null) { 255 mMailboxFinder.cancel(); 256 mMailboxFinder = null; 257 } 258 Utility.cancelTaskInterrupt(mSetTitleTask); 259 mSetTitleTask = null; 260 } 261 262 263 private void launchWelcomeAndFinish() { 264 Welcome.actionStart(this); 265 finish(); 266 } 267 268 /** 269 * Called when the list fragment can't find mailbox/account. 270 */ 271 public void onMailboxNotFound() { 272 finish(); 273 } 274 275 @Override 276 public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId, int type) { 277 if (type == MessageListFragment.Callback.TYPE_DRAFT) { 278 MessageCompose.actionEditDraft(this, messageId); 279 } else { 280 // WARNING: here we pass "listMailboxId", which can be the negative id of 281 // a compound mailbox, instead of the mailboxId of the particular message that 282 // is opened. This is to support the next/prev buttons on the message view 283 // properly even for combined mailboxes. 284 MessageView.actionView(this, messageId, listMailboxId); 285 } 286 } 287 288 @Override 289 public void onEnterSelectionMode(boolean enter) { 290 } 291 292 public void onClick(View v) { 293 switch (v.getId()) { 294 case R.id.btn_read_unread: 295 mListFragment.onMultiToggleRead(); 296 break; 297 case R.id.btn_multi_favorite: 298 mListFragment.onMultiToggleFavorite(); 299 break; 300 case R.id.btn_multi_delete: 301 mListFragment.onMultiDelete(); 302 break; 303 case R.id.account_title_button: 304 onAccounts(); 305 break; 306 } 307 } 308 309 public void onAnimationEnd(Animation animation) { 310 // TODO: If the button panel hides the only selected item, scroll the list to make it 311 // visible again. 312 } 313 314 public void onAnimationRepeat(Animation animation) { 315 } 316 317 public void onAnimationStart(Animation animation) { 318 } 319 320 @Override 321 public boolean onPrepareOptionsMenu(Menu menu) { 322 // Re-create menu every time. (We may not know the mailbox id yet) 323 menu.clear(); 324 if (mListFragment.isMagicMailbox()) { 325 getMenuInflater().inflate(R.menu.message_list_option_smart_folder, menu); 326 } else { 327 getMenuInflater().inflate(R.menu.message_list_option, menu); 328 } 329 boolean showDeselect = mListFragment.getSelectedCount() > 0; 330 menu.setGroupVisible(R.id.deselect_all_group, showDeselect); 331 return true; 332 } 333 334 @Override 335 public boolean onOptionsItemSelected(MenuItem item) { 336 switch (item.getItemId()) { 337 case R.id.refresh: 338 mListFragment.onRefresh(); 339 return true; 340 case R.id.folders: 341 onFolders(); 342 return true; 343 case R.id.accounts: 344 onAccounts(); 345 return true; 346 case R.id.compose: 347 onCompose(); 348 return true; 349 case R.id.account_settings: 350 onEditAccount(); 351 return true; 352 case R.id.deselect_all: 353 mListFragment.onDeselectAll(); 354 return true; 355 default: 356 return super.onOptionsItemSelected(item); 357 } 358 } 359 360 private void onFolders() { 361 if (!mListFragment.isMagicMailbox()) { // Magic boxes don't have "folders" option. 362 // TODO smaller projection 363 Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mListFragment.getMailboxId()); 364 if (mailbox != null) { 365 MailboxList.actionHandleAccount(this, mailbox.mAccountKey); 366 finish(); 367 } 368 } 369 } 370 371 private void onAccounts() { 372 AccountFolderList.actionShowAccounts(this); 373 finish(); 374 } 375 376 private void onCompose() { 377 MessageCompose.actionCompose(this, mListFragment.getAccountId()); 378 } 379 380 private void onEditAccount() { 381 AccountSettingsXL.actionSettings(this, mListFragment.getAccountId()); 382 } 383 384 /** 385 * Show multi-selection panel, if one or more messages are selected. Button labels will be 386 * updated too. 387 * 388 * @deprecated not used any longer. remove them. 389 */ 390 public void onSelectionChanged() { 391 showMultiPanel(mListFragment.getSelectedCount() > 0); 392 } 393 394 /** 395 * @deprecated not used any longer. remove them. (with associated resources, strings, 396 * members, etc) 397 */ 398 private void updateFooterButtonNames () { 399 // Show "unread_action" when one or more read messages are selected. 400 if (mListFragment.doesSelectionContainReadMessage()) { 401 mReadUnreadButton.setText(R.string.unread_action); 402 } else { 403 mReadUnreadButton.setText(R.string.read_action); 404 } 405 // Show "set_star_action" when one or more un-starred messages are selected. 406 if (mListFragment.doesSelectionContainNonStarredMessage()) { 407 mFavoriteButton.setText(R.string.set_star_action); 408 } else { 409 mFavoriteButton.setText(R.string.remove_star_action); 410 } 411 } 412 413 /** 414 * Show or hide the panel of multi-select options 415 * 416 * @deprecated not used any longer. remove them. 417 */ 418 private void showMultiPanel(boolean show) { 419 if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) { 420 mMultiSelectPanel.setVisibility(View.VISIBLE); 421 Animation animation = AnimationUtils.loadAnimation(this, R.anim.footer_appear); 422 animation.setAnimationListener(this); 423 mMultiSelectPanel.startAnimation(animation); 424 } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) { 425 mMultiSelectPanel.setVisibility(View.GONE); 426 mMultiSelectPanel.startAnimation( 427 AnimationUtils.loadAnimation(this, R.anim.footer_disappear)); 428 } 429 if (show) { 430 updateFooterButtonNames(); 431 } 432 } 433 434 /** 435 * Handle the eventual result from the security update activity 436 * 437 * Note, this is extremely coarse, and it simply returns the user to the Accounts list. 438 * Anything more requires refactoring of this Activity. 439 */ 440 @Override 441 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 442 switch (requestCode) { 443 case REQUEST_SECURITY: 444 onAccounts(); 445 } 446 super.onActivityResult(requestCode, resultCode, data); 447 } 448 449 private class SetTitleTask extends AsyncTask<Void, Void, Object[]> { 450 451 private long mMailboxKey; 452 453 public SetTitleTask(long mailboxKey) { 454 mMailboxKey = mailboxKey; 455 } 456 457 @Override 458 protected Object[] doInBackground(Void... params) { 459 // Check special Mailboxes 460 int resIdSpecialMailbox = 0; 461 if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) { 462 resIdSpecialMailbox = R.string.account_folder_list_summary_inbox; 463 } else if (mMailboxKey == Mailbox.QUERY_ALL_FAVORITES) { 464 resIdSpecialMailbox = R.string.account_folder_list_summary_starred; 465 } else if (mMailboxKey == Mailbox.QUERY_ALL_DRAFTS) { 466 resIdSpecialMailbox = R.string.account_folder_list_summary_drafts; 467 } else if (mMailboxKey == Mailbox.QUERY_ALL_OUTBOX) { 468 resIdSpecialMailbox = R.string.account_folder_list_summary_outbox; 469 } 470 if (resIdSpecialMailbox != 0) { 471 return new Object[] {null, getString(resIdSpecialMailbox), 0}; 472 } 473 474 String accountName = null; 475 String mailboxName = null; 476 String accountKey = null; 477 Cursor c = MessageList.this.mResolver.query(Mailbox.CONTENT_URI, 478 MAILBOX_NAME_PROJECTION, ID_SELECTION, 479 new String[] { Long.toString(mMailboxKey) }, null); 480 try { 481 if (c.moveToFirst()) { 482 mailboxName = Utility.FolderProperties.getInstance(MessageList.this) 483 .getDisplayName(c.getInt(MAILBOX_NAME_COLUMN_TYPE)); 484 if (mailboxName == null) { 485 mailboxName = c.getString(MAILBOX_NAME_COLUMN_ID); 486 } 487 accountKey = c.getString(MAILBOX_NAME_COLUMN_ACCOUNT_KEY); 488 } 489 } finally { 490 c.close(); 491 } 492 if (accountKey != null) { 493 c = MessageList.this.mResolver.query(Account.CONTENT_URI, 494 ACCOUNT_NAME_PROJECTION, ID_SELECTION, new String[] { accountKey }, 495 null); 496 try { 497 if (c.moveToFirst()) { 498 accountName = c.getString(ACCOUNT_DISPLAY_NAME_COLUMN_ID); 499 } 500 } finally { 501 c.close(); 502 } 503 } 504 int nAccounts = EmailContent.count(MessageList.this, Account.CONTENT_URI, null, null); 505 return new Object[] {accountName, mailboxName, nAccounts}; 506 } 507 508 @Override 509 protected void onPostExecute(Object[] result) { 510 if (result == null) { 511 return; 512 } 513 514 final int nAccounts = (Integer) result[2]; 515 if (result[0] != null) { 516 setTitleAccountName((String) result[0], nAccounts > 1); 517 } 518 519 if (result[1] != null) { 520 mLeftTitle.setText((String) result[1]); 521 } 522 } 523 } 524 525 private void setTitleAccountName(String accountName, boolean showAccountsButton) { 526 TextView accountsButton = (TextView) findViewById(R.id.account_title_button); 527 TextView textPlain = (TextView) findViewById(R.id.title_right_text); 528 if (showAccountsButton) { 529 accountsButton.setVisibility(View.VISIBLE); 530 textPlain.setVisibility(View.GONE); 531 accountsButton.setText(accountName); 532 } else { 533 accountsButton.setVisibility(View.GONE); 534 textPlain.setVisibility(View.VISIBLE); 535 textPlain.setText(accountName); 536 } 537 } 538 539 private void showProgressIcon(boolean show) { 540 int visibility = show ? View.VISIBLE : View.GONE; 541 mProgressIcon.setVisibility(visibility); 542 } 543 544 private void showErrorBanner(String message) { 545 boolean isVisible = mErrorBanner.getVisibility() == View.VISIBLE; 546 if (message != null) { 547 mErrorBanner.setText(message); 548 if (!isVisible) { 549 mErrorBanner.setVisibility(View.VISIBLE); 550 mErrorBanner.startAnimation( 551 AnimationUtils.loadAnimation( 552 MessageList.this, R.anim.header_appear)); 553 } 554 } else { 555 if (isVisible) { 556 mErrorBanner.setVisibility(View.GONE); 557 mErrorBanner.startAnimation( 558 AnimationUtils.loadAnimation( 559 MessageList.this, R.anim.header_disappear)); 560 } 561 } 562 } 563 564 /** 565 * Controller results listener. We wrap it with {@link ControllerResultUiThreadWrapper}, 566 * so all methods are called on the UI thread. 567 */ 568 private class ControllerResults extends Controller.Result { 569 570 // This is used to alter the connection banner operation for sending messages 571 private MessagingException mSendMessageException; 572 573 // TODO check accountKey and only react to relevant notifications 574 @Override 575 public void updateMailboxCallback(MessagingException result, long accountKey, 576 long mailboxKey, int progress, int numNewMessages) { 577 updateBanner(result, progress, mailboxKey); 578 updateProgress(result, progress); 579 } 580 581 /** 582 * We alter the updateBanner hysteresis here to capture any failures and handle 583 * them just once at the end. This callback is overly overloaded: 584 * result == null, messageId == -1, progress == 0: start batch send 585 * result == null, messageId == xx, progress == 0: start sending one message 586 * result == xxxx, messageId == xx, progress == 0; failed sending one message 587 * result == null, messageId == -1, progres == 100; finish sending batch 588 */ 589 @Override 590 public void sendMailCallback(MessagingException result, long accountId, long messageId, 591 int progress) { 592 if (mListFragment.isOutbox()) { 593 // reset captured error when we start sending one or more messages 594 if (messageId == -1 && result == null && progress == 0) { 595 mSendMessageException = null; 596 } 597 // capture first exception that comes along 598 if (result != null && mSendMessageException == null) { 599 mSendMessageException = result; 600 } 601 // if we're completing the sequence, change the banner state 602 if (messageId == -1 && progress == 100) { 603 updateBanner(mSendMessageException, progress, mListFragment.getMailboxId()); 604 } 605 // always update the spinner, which has less state to worry about 606 updateProgress(result, progress); 607 } 608 } 609 610 private void updateProgress(MessagingException result, int progress) { 611 showProgressIcon(result == null && progress < 100); 612 } 613 614 /** 615 * Show or hide the connection error banner, and convert the various MessagingException 616 * variants into localizable text. There is hysteresis in the show/hide logic: Once shown, 617 * the banner will remain visible until some progress is made on the connection. The 618 * goal is to keep it from flickering during retries in a bad connection state. 619 * 620 * @param result 621 * @param progress 622 */ 623 private void updateBanner(MessagingException result, int progress, long mailboxKey) { 624 if (mailboxKey != mListFragment.getMailboxId()) { 625 return; 626 } 627 if (result != null) { 628 showErrorBanner(result.getUiErrorMessage(MessageList.this)); 629 } else if (progress > 0) { 630 showErrorBanner(null); 631 } 632 } 633 } 634 635 private class MailboxFinderCallback implements MailboxFinder.Callback { 636 @Override 637 public void onMailboxFound(long accountId, long mailboxId) { 638 mSetTitleTask = new SetTitleTask(mailboxId); 639 mSetTitleTask.execute(); 640 mListFragment.openMailbox(mailboxId); 641 } 642 643 @Override 644 public void onAccountNotFound() { 645 // Let the Welcome activity show the default screen. 646 launchWelcomeAndFinish(); 647 } 648 649 @Override 650 public void onMailboxNotFound(long accountId) { 651 // Let the Welcome activity show the default screen. 652 launchWelcomeAndFinish(); 653 } 654 655 @Override 656 public void onAccountSecurityHold(long accountId) { 657 // launch the security setup activity 658 Intent i = AccountSecurity.actionUpdateSecurityIntent( 659 MessageList.this, accountId); 660 MessageList.this.startActivityForResult(i, REQUEST_SECURITY); 661 } 662 } 663} 664