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