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