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