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