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