MessageList.java revision b8ee27f013766d6c89befa36bd265c6104ffb036
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 onPrepareOptionsMenu(Menu menu) { 417 boolean showDeselect = mListAdapter.getSelectedSet().size() > 0; 418 menu.setGroupVisible(R.id.deselect_all_group, showDeselect); 419 return true; 420 } 421 422 @Override 423 public boolean onOptionsItemSelected(MenuItem item) { 424 switch (item.getItemId()) { 425 case R.id.refresh: 426 onRefresh(); 427 return true; 428 case R.id.folders: 429 onFolders(); 430 return true; 431 case R.id.accounts: 432 onAccounts(); 433 return true; 434 case R.id.compose: 435 onCompose(); 436 return true; 437 case R.id.account_settings: 438 onEditAccount(); 439 return true; 440 case R.id.deselect_all: 441 onDeselectAll(); 442 return true; 443 default: 444 return super.onOptionsItemSelected(item); 445 } 446 } 447 448 @Override 449 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 450 super.onCreateContextMenu(menu, v, menuInfo); 451 452 AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; 453 // There is no context menu for the list footer 454 if (info.targetView == mListFooterView) { 455 return; 456 } 457 MessageListItem itemView = (MessageListItem) info.targetView; 458 459 Cursor c = (Cursor) mListView.getItemAtPosition(info.position); 460 String messageName = c.getString(MessageListAdapter.COLUMN_SUBJECT); 461 462 menu.setHeaderTitle(messageName); 463 464 // TODO: There is probably a special context menu for the trash 465 Mailbox mailbox = Mailbox.restoreMailboxWithId(this, itemView.mMailboxId); 466 467 switch (mailbox.mType) { 468 case EmailContent.Mailbox.TYPE_DRAFTS: 469 getMenuInflater().inflate(R.menu.message_list_context_drafts, menu); 470 break; 471 case EmailContent.Mailbox.TYPE_OUTBOX: 472 getMenuInflater().inflate(R.menu.message_list_context_outbox, menu); 473 break; 474 case EmailContent.Mailbox.TYPE_TRASH: 475 getMenuInflater().inflate(R.menu.message_list_context_trash, menu); 476 break; 477 default: 478 getMenuInflater().inflate(R.menu.message_list_context, menu); 479 // The default menu contains "mark as read". If the message is read, change 480 // the menu text to "mark as unread." 481 if (itemView.mRead) { 482 menu.findItem(R.id.mark_as_read).setTitle(R.string.mark_as_unread_action); 483 } 484 break; 485 } 486 } 487 488 @Override 489 public boolean onContextItemSelected(MenuItem item) { 490 AdapterView.AdapterContextMenuInfo info = 491 (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 492 MessageListItem itemView = (MessageListItem) info.targetView; 493 494 switch (item.getItemId()) { 495 case R.id.open: 496 onOpenMessage(info.id, itemView.mMailboxId); 497 break; 498 case R.id.delete: 499 onDelete(info.id, itemView.mAccountId); 500 break; 501 case R.id.reply: 502 onReply(itemView.mMessageId); 503 break; 504 case R.id.reply_all: 505 onReplyAll(itemView.mMessageId); 506 break; 507 case R.id.forward: 508 onForward(itemView.mMessageId); 509 break; 510 case R.id.mark_as_read: 511 onSetMessageRead(info.id, !itemView.mRead); 512 break; 513 } 514 return super.onContextItemSelected(item); 515 } 516 517 private void onRefresh() { 518 // TODO: Should not be reading from DB in UI thread - need a cleaner way to get accountId 519 if (mMailboxId >= 0) { 520 Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mMailboxId); 521 mController.updateMailbox(mailbox.mAccountKey, mMailboxId, mControllerCallback); 522 } 523 } 524 525 private void onFolders() { 526 if (mMailboxId >= 0) { 527 // TODO smaller projection 528 Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mMailboxId); 529 MailboxList.actionHandleAccount(this, mailbox.mAccountKey); 530 finish(); 531 } 532 } 533 534 private void onAccounts() { 535 AccountFolderList.actionShowAccounts(this); 536 finish(); 537 } 538 539 private long lookupAccountIdFromMailboxId(long mailboxId) { 540 // TODO: Select correct account to send from when there are multiple mailboxes 541 // TODO: Should not be reading from DB in UI thread 542 if (mailboxId < 0) { 543 return -1; // no info, default account 544 } 545 EmailContent.Mailbox mailbox = 546 EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId); 547 return mailbox.mAccountKey; 548 } 549 550 private void onCompose() { 551 MessageCompose.actionCompose(this, lookupAccountIdFromMailboxId(mMailboxId)); 552 } 553 554 private void onEditAccount() { 555 AccountSettings.actionSettings(this, lookupAccountIdFromMailboxId(mMailboxId)); 556 } 557 558 private void onDeselectAll() { 559 mListAdapter.getSelectedSet().clear(); 560 mListView.invalidateViews(); 561 showMultiPanel(false); 562 } 563 564 private void onOpenMessage(long messageId, long mailboxId) { 565 // TODO: Should not be reading from DB in UI thread 566 EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId); 567 568 if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) { 569 MessageCompose.actionEditDraft(this, messageId); 570 } else { 571 // WARNING: here we pass mMailboxId, which can be the negative id of a compound 572 // mailbox, instead of the mailboxId of the particular message that is opened 573 MessageView.actionView(this, messageId, mMailboxId); 574 } 575 } 576 577 private void onReply(long messageId) { 578 MessageCompose.actionReply(this, messageId, false); 579 } 580 581 private void onReplyAll(long messageId) { 582 MessageCompose.actionReply(this, messageId, true); 583 } 584 585 private void onForward(long messageId) { 586 MessageCompose.actionForward(this, messageId); 587 } 588 589 private void onLoadMoreMessages() { 590 if (mMailboxId >= 0) { 591 mController.loadMoreMessages(mMailboxId, mControllerCallback); 592 } 593 } 594 595 private void onSendPendingMessages() { 596 long accountId = lookupAccountIdFromMailboxId(mMailboxId); 597 mController.sendPendingMessages(accountId, mControllerCallback); 598 } 599 600 private void onDelete(long messageId, long accountId) { 601 mController.deleteMessage(messageId, accountId); 602 Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show(); 603 } 604 605 private void onSetMessageRead(long messageId, boolean newRead) { 606 mController.setMessageRead(messageId, newRead); 607 } 608 609 private void onSetMessageFavorite(long messageId, boolean newFavorite) { 610 mController.setMessageFavorite(messageId, newFavorite); 611 } 612 613 /** 614 * Toggles a set read/unread states. Note, the default behavior is "mark unread", so the 615 * sense of the helper methods is "true=unread". 616 * 617 * @param selectedSet The current list of selected items 618 */ 619 private void onMultiToggleRead(Set<Long> selectedSet) { 620 toggleMultiple(selectedSet, new MultiToggleHelper() { 621 622 public boolean getField(long messageId, Cursor c) { 623 return c.getInt(MessageListAdapter.COLUMN_READ) == 0; 624 } 625 626 public boolean setField(long messageId, Cursor c, boolean newValue) { 627 boolean oldValue = getField(messageId, c); 628 if (oldValue != newValue) { 629 onSetMessageRead(messageId, !newValue); 630 return true; 631 } 632 return false; 633 } 634 }); 635 } 636 637 /** 638 * Toggles a set of favorites (stars) 639 * 640 * @param selectedSet The current list of selected items 641 */ 642 private void onMultiToggleFavorite(Set<Long> selectedSet) { 643 toggleMultiple(selectedSet, new MultiToggleHelper() { 644 645 public boolean getField(long messageId, Cursor c) { 646 return c.getInt(MessageListAdapter.COLUMN_FAVORITE) != 0; 647 } 648 649 public boolean setField(long messageId, Cursor c, boolean newValue) { 650 boolean oldValue = getField(messageId, c); 651 if (oldValue != newValue) { 652 onSetMessageFavorite(messageId, newValue); 653 return true; 654 } 655 return false; 656 } 657 }); 658 } 659 660 private void onMultiDelete(Set<Long> selectedSet) { 661 // Clone the set, because deleting is going to thrash things 662 HashSet<Long> cloneSet = new HashSet<Long>(selectedSet); 663 for (Long id : cloneSet) { 664 mController.deleteMessage(id, -1); 665 } 666 // TODO: count messages and show "n messages deleted" 667 Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show(); 668 selectedSet.clear(); 669 showMultiPanel(false); 670 } 671 672 private interface MultiToggleHelper { 673 /** 674 * Return true if the field of interest is "set". If one or more are false, then our 675 * bulk action will be to "set". If all are set, our bulk action will be to "clear". 676 * @param messageId the message id of the current message 677 * @param c the cursor, positioned to the item of interest 678 * @return true if the field at this row is "set" 679 */ 680 public boolean getField(long messageId, Cursor c); 681 682 /** 683 * Set or clear the field of interest. Return true if a change was made. 684 * @param messageId the message id of the current message 685 * @param c the cursor, positioned to the item of interest 686 * @param newValue the new value to be set at this row 687 * @return true if a change was actually made 688 */ 689 public boolean setField(long messageId, Cursor c, boolean newValue); 690 } 691 692 /** 693 * Toggle multiple fields in a message, using the following logic: If one or more fields 694 * are "clear", then "set" them. If all fields are "set", then "clear" them all. 695 * 696 * @param selectedSet the set of messages that are selected 697 * @param helper functions to implement the specific getter & setter 698 * @return the number of messages that were updated 699 */ 700 private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) { 701 Cursor c = mListAdapter.getCursor(); 702 boolean anyWereFound = false; 703 boolean allWereSet = true; 704 705 c.moveToPosition(-1); 706 while (c.moveToNext()) { 707 long id = c.getInt(MessageListAdapter.COLUMN_ID); 708 if (selectedSet.contains(Long.valueOf(id))) { 709 anyWereFound = true; 710 if (!helper.getField(id, c)) { 711 allWereSet = false; 712 break; 713 } 714 } 715 } 716 717 int numChanged = 0; 718 719 if (anyWereFound) { 720 boolean newValue = !allWereSet; 721 c.moveToPosition(-1); 722 while (c.moveToNext()) { 723 long id = c.getInt(MessageListAdapter.COLUMN_ID); 724 if (selectedSet.contains(Long.valueOf(id))) { 725 if (helper.setField(id, c, newValue)) { 726 ++numChanged; 727 } 728 } 729 } 730 } 731 732 return numChanged; 733 } 734 735 /** 736 * Test selected messages for showing appropriate labels 737 * @param selectedSet 738 * @param column_id 739 * @param defaultflag 740 * @return true when the specified flagged message is selected 741 */ 742 private boolean testMultiple(Set<Long> selectedSet, int column_id, boolean defaultflag) { 743 Cursor c = mListAdapter.getCursor(); 744 c.moveToPosition(-1); 745 while (c.moveToNext()) { 746 long id = c.getInt(MessageListAdapter.COLUMN_ID); 747 if (selectedSet.contains(Long.valueOf(id))) { 748 if (c.getInt(column_id) == (defaultflag? 1 : 0)) { 749 return true; 750 } 751 } 752 } 753 return false; 754 } 755 756 private void autoRefreshStaleMailbox() { 757 if ((mListAdapter.getCursor() == null) // Check if messages info is loaded 758 || (mPushModeMailbox != null && mPushModeMailbox) // Check the push mode 759 || (mMailboxId < 0) // Check if this mailbox is synthetic/combined 760 || !Email.mailboxRequiresRefresh(mMailboxId)) { 761 return; 762 } 763 onRefresh(); 764 } 765 766 private void updateFooterButtonNames () { 767 // Show "unread_action" when one or more read messages are selected. 768 if (testMultiple(mListAdapter.getSelectedSet(), MessageListAdapter.COLUMN_READ, true)) { 769 mReadUnreadButton.setText(R.string.unread_action); 770 } else { 771 mReadUnreadButton.setText(R.string.read_action); 772 } 773 // Show "set_star_action" when one or more un-starred messages are selected. 774 if (testMultiple(mListAdapter.getSelectedSet(), 775 MessageListAdapter.COLUMN_FAVORITE, false)) { 776 mFavoriteButton.setText(R.string.set_star_action); 777 } else { 778 mFavoriteButton.setText(R.string.remove_star_action); 779 } 780 } 781 782 /** 783 * Show or hide the panel of multi-select options 784 */ 785 private void showMultiPanel(boolean show) { 786 if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) { 787 mMultiSelectPanel.setVisibility(View.VISIBLE); 788 mMultiSelectPanel.startAnimation( 789 AnimationUtils.loadAnimation(this, R.anim.footer_appear)); 790 } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) { 791 mMultiSelectPanel.setVisibility(View.GONE); 792 mMultiSelectPanel.startAnimation( 793 AnimationUtils.loadAnimation(this, R.anim.footer_disappear)); 794 } 795 if (show) { 796 updateFooterButtonNames(); 797 } 798 } 799 800 /** 801 * Add the fixed footer view if appropriate (not always - not all accounts & mailboxes). 802 * 803 * Here are some rules (finish this list): 804 * 805 * Any merged, synced box (except send): refresh 806 * Any push-mode account: refresh 807 * Any non-push-mode account: load more 808 * Any outbox (send again): 809 * 810 * @param mailboxId the ID of the mailbox 811 */ 812 private void addFooterView(long mailboxId, long accountId, int mailboxType) { 813 // first, look for shortcuts that don't need us to spin up a DB access task 814 if (mailboxId == Mailbox.QUERY_ALL_INBOXES 815 || mailboxId == Mailbox.QUERY_ALL_UNREAD 816 || mailboxId == Mailbox.QUERY_ALL_FAVORITES) { 817 finishFooterView(LIST_FOOTER_MODE_REFRESH); 818 return; 819 } 820 if (mailboxId == Mailbox.QUERY_ALL_DRAFTS || mailboxType == Mailbox.TYPE_DRAFTS) { 821 finishFooterView(LIST_FOOTER_MODE_NONE); 822 return; 823 } 824 if (mailboxId == Mailbox.QUERY_ALL_OUTBOX || mailboxType == Mailbox.TYPE_OUTBOX) { 825 finishFooterView(LIST_FOOTER_MODE_SEND); 826 return; 827 } 828 829 // We don't know enough to select the footer command type (yet), so we'll 830 // launch an async task to do the remaining lookups and decide what to do 831 mSetFooterTask = new SetFooterTask(); 832 mSetFooterTask.execute(mailboxId, accountId); 833 } 834 835 private final static String[] MAILBOX_ACCOUNT_AND_TYPE_PROJECTION = 836 new String[] { MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE }; 837 838 private class SetFooterTask extends AsyncTask<Long, Void, Integer> { 839 /** 840 * There are two operational modes here, requiring different lookup. 841 * mailboxIs != -1: A specific mailbox - check its type, then look up its account 842 * accountId != -1: A specific account - look up the account 843 */ 844 @Override 845 protected Integer doInBackground(Long... params) { 846 long mailboxId = params[0]; 847 long accountId = params[1]; 848 int mailboxType = -1; 849 if (mailboxId != -1) { 850 try { 851 Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId); 852 Cursor c = mResolver.query(uri, MAILBOX_ACCOUNT_AND_TYPE_PROJECTION, 853 null, null, null); 854 if (c.moveToFirst()) { 855 try { 856 accountId = c.getLong(0); 857 mailboxType = c.getInt(1); 858 } finally { 859 c.close(); 860 } 861 } 862 } catch (IllegalArgumentException iae) { 863 // can't do any more here 864 return LIST_FOOTER_MODE_NONE; 865 } 866 } 867 switch (mailboxType) { 868 case Mailbox.TYPE_OUTBOX: 869 return LIST_FOOTER_MODE_SEND; 870 case Mailbox.TYPE_DRAFTS: 871 return LIST_FOOTER_MODE_NONE; 872 } 873 if (accountId != -1) { 874 // This is inefficient but the best fix is not here but in isMessagingController 875 Account account = Account.restoreAccountWithId(MessageList.this, accountId); 876 if (account != null) { 877 mPushModeMailbox = account.mSyncInterval == Account.CHECK_INTERVAL_PUSH; 878 if (MessageList.this.mController.isMessagingController(account)) { 879 return LIST_FOOTER_MODE_MORE; // IMAP or POP 880 } else { 881 return LIST_FOOTER_MODE_NONE; // EAS 882 } 883 } 884 } 885 return LIST_FOOTER_MODE_NONE; 886 } 887 888 @Override 889 protected void onPostExecute(Integer listFooterMode) { 890 finishFooterView(listFooterMode); 891 } 892 } 893 894 /** 895 * Add the fixed footer view as specified, and set up the test as well. 896 * 897 * @param listFooterMode the footer mode we've determined should be used for this list 898 */ 899 private void finishFooterView(int listFooterMode) { 900 mListFooterMode = listFooterMode; 901 if (mListFooterMode != LIST_FOOTER_MODE_NONE) { 902 mListFooterView = ((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE)) 903 .inflate(R.layout.message_list_item_footer, mListView, false); 904 mList.addFooterView(mListFooterView); 905 setListAdapter(mListAdapter); 906 907 mListFooterProgress = mListFooterView.findViewById(R.id.progress); 908 mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text); 909 setListFooterText(false); 910 } 911 } 912 913 /** 914 * Set the list footer text based on mode and "active" status 915 */ 916 private void setListFooterText(boolean active) { 917 if (mListFooterMode != LIST_FOOTER_MODE_NONE) { 918 int footerTextId = 0; 919 switch (mListFooterMode) { 920 case LIST_FOOTER_MODE_REFRESH: 921 footerTextId = active ? R.string.status_loading_more 922 : R.string.refresh_action; 923 break; 924 case LIST_FOOTER_MODE_MORE: 925 footerTextId = active ? R.string.status_loading_more 926 : R.string.message_list_load_more_messages_action; 927 break; 928 case LIST_FOOTER_MODE_SEND: 929 footerTextId = active ? R.string.status_sending_messages 930 : R.string.message_list_send_pending_messages_action; 931 break; 932 } 933 mListFooterText.setText(footerTextId); 934 } 935 } 936 937 /** 938 * Handle a click in the list footer, which changes meaning depending on what we're looking at. 939 */ 940 private void doFooterClick() { 941 switch (mListFooterMode) { 942 case LIST_FOOTER_MODE_NONE: // should never happen 943 break; 944 case LIST_FOOTER_MODE_REFRESH: 945 onRefresh(); 946 break; 947 case LIST_FOOTER_MODE_MORE: 948 onLoadMoreMessages(); 949 break; 950 case LIST_FOOTER_MODE_SEND: 951 onSendPendingMessages(); 952 break; 953 } 954 } 955 956 /** 957 * Async task for finding a single mailbox by type (possibly even going to the network). 958 * 959 * This is much too complex, as implemented. It uses this AsyncTask to check for a mailbox, 960 * then (if not found) a Controller call to refresh mailboxes from the server, and a handler 961 * to relaunch this task (a 2nd time) to read the results of the network refresh. The core 962 * problem is that we have two different non-UI-thread jobs (reading DB and reading network) 963 * and two different paradigms for dealing with them. Some unification would be needed here 964 * to make this cleaner. 965 * 966 * TODO: If this problem spreads to other operations, find a cleaner way to handle it. 967 */ 968 private class FindMailboxTask extends AsyncTask<Void, Void, Long> { 969 970 private long mAccountId; 971 private int mMailboxType; 972 private boolean mOkToRecurse; 973 974 /** 975 * Special constructor to cache some local info 976 */ 977 public FindMailboxTask(long accountId, int mailboxType, boolean okToRecurse) { 978 mAccountId = accountId; 979 mMailboxType = mailboxType; 980 mOkToRecurse = okToRecurse; 981 } 982 983 @Override 984 protected Long doInBackground(Void... params) { 985 // See if we can find the requested mailbox in the DB. 986 long mailboxId = Mailbox.findMailboxOfType(MessageList.this, mAccountId, mMailboxType); 987 if (mailboxId == Mailbox.NO_MAILBOX && mOkToRecurse) { 988 // Not found - launch network lookup 989 mControllerCallback.mWaitForMailboxType = mMailboxType; 990 mController.updateMailboxList(mAccountId, mControllerCallback); 991 } 992 return mailboxId; 993 } 994 995 @Override 996 protected void onPostExecute(Long mailboxId) { 997 if (mailboxId != Mailbox.NO_MAILBOX) { 998 mMailboxId = mailboxId; 999 mSetTitleTask = new SetTitleTask(mMailboxId); 1000 mSetTitleTask.execute(); 1001 mLoadMessagesTask = new LoadMessagesTask(mMailboxId, mAccountId); 1002 mLoadMessagesTask.execute(); 1003 } 1004 } 1005 } 1006 1007 /** 1008 * Async task for loading a single folder out of the UI thread 1009 * 1010 * The code here (for merged boxes) is a placeholder/hack and should be replaced. Some 1011 * specific notes: 1012 * TODO: Move the double query into a specialized URI that returns all inbox messages 1013 * and do the dirty work in raw SQL in the provider. 1014 * TODO: Generalize the query generation so we can reuse it in MessageView (for next/prev) 1015 */ 1016 private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> { 1017 1018 private long mMailboxKey; 1019 private long mAccountKey; 1020 1021 /** 1022 * Special constructor to cache some local info 1023 */ 1024 public LoadMessagesTask(long mailboxKey, long accountKey) { 1025 mMailboxKey = mailboxKey; 1026 mAccountKey = accountKey; 1027 } 1028 1029 @Override 1030 protected Cursor doInBackground(Void... params) { 1031 String selection = 1032 Utility.buildMailboxIdSelection(MessageList.this.mResolver, mMailboxKey); 1033 Cursor c = MessageList.this.managedQuery( 1034 EmailContent.Message.CONTENT_URI, 1035 MessageList.this.mListAdapter.PROJECTION, 1036 selection, null, 1037 EmailContent.MessageColumns.TIMESTAMP + " DESC"); 1038 return c; 1039 } 1040 1041 @Override 1042 protected void onPostExecute(Cursor cursor) { 1043 if (cursor.isClosed()) { 1044 return; 1045 } 1046 MessageList.this.mListAdapter.changeCursor(cursor); 1047 // changeCursor occurs the jumping of position in ListView, so it's need to restore 1048 // the position; 1049 restoreListPosition(); 1050 autoRefreshStaleMailbox(); 1051 // Reset the "new messages" count in the service, since we're seeing them now 1052 if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) { 1053 MailService.resetNewMessageCount(MessageList.this, -1); 1054 } else if (mMailboxKey >= 0 && mAccountKey != -1) { 1055 MailService.resetNewMessageCount(MessageList.this, mAccountKey); 1056 } 1057 } 1058 } 1059 1060 private class SetTitleTask extends AsyncTask<Void, Void, String[]> { 1061 1062 private long mMailboxKey; 1063 1064 public SetTitleTask(long mailboxKey) { 1065 mMailboxKey = mailboxKey; 1066 } 1067 1068 @Override 1069 protected String[] doInBackground(Void... params) { 1070 // Check special Mailboxes 1071 if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) { 1072 return new String[] {null, 1073 getString(R.string.account_folder_list_summary_inbox)}; 1074 } else if (mMailboxKey == Mailbox.QUERY_ALL_FAVORITES) { 1075 return new String[] {null, 1076 getString(R.string.account_folder_list_summary_starred)}; 1077 } else if (mMailboxKey == Mailbox.QUERY_ALL_DRAFTS) { 1078 return new String[] {null, 1079 getString(R.string.account_folder_list_summary_drafts)}; 1080 } else if (mMailboxKey == Mailbox.QUERY_ALL_OUTBOX) { 1081 return new String[] {null, 1082 getString(R.string.account_folder_list_summary_outbox)}; 1083 } 1084 String accountName = null; 1085 String mailboxName = null; 1086 String accountKey = null; 1087 Cursor c = MessageList.this.mResolver.query(Mailbox.CONTENT_URI, 1088 MAILBOX_NAME_PROJECTION, ID_SELECTION, 1089 new String[] { Long.toString(mMailboxKey) }, null); 1090 try { 1091 if (c.moveToFirst()) { 1092 mailboxName = Utility.FolderProperties.getInstance(MessageList.this) 1093 .getDisplayName(c.getInt(MAILBOX_NAME_COLUMN_TYPE)); 1094 if (mailboxName == null) { 1095 mailboxName = c.getString(MAILBOX_NAME_COLUMN_ID); 1096 } 1097 accountKey = c.getString(MAILBOX_NAME_COLUMN_ACCOUNT_KEY); 1098 } 1099 } finally { 1100 c.close(); 1101 } 1102 if (accountKey != null) { 1103 c = MessageList.this.mResolver.query(Account.CONTENT_URI, 1104 ACCOUNT_NAME_PROJECTION, ID_SELECTION, new String[] { accountKey }, 1105 null); 1106 try { 1107 if (c.moveToFirst()) { 1108 accountName = c.getString(ACCOUNT_DISPLAY_NAME_COLUMN_ID); 1109 } 1110 } finally { 1111 c.close(); 1112 } 1113 } 1114 return new String[] {accountName, mailboxName}; 1115 } 1116 1117 @Override 1118 protected void onPostExecute(String[] names) { 1119 if (names[0] != null) { 1120 mRightTitle.setText(names[0]); 1121 } 1122 if (names[1] != null) { 1123 mLeftTitle.setText(names[1]); 1124 } 1125 } 1126 } 1127 1128 /** 1129 * Handler for UI-thread operations (when called from callbacks or any other threads) 1130 */ 1131 class MessageListHandler extends Handler { 1132 private static final int MSG_PROGRESS = 1; 1133 private static final int MSG_LOOKUP_MAILBOX_TYPE = 2; 1134 private static final int MSG_ERROR_BANNER = 3; 1135 1136 @Override 1137 public void handleMessage(android.os.Message msg) { 1138 switch (msg.what) { 1139 case MSG_PROGRESS: 1140 boolean visible = (msg.arg1 != 0); 1141 if (visible) { 1142 mProgressIcon.setVisibility(View.VISIBLE); 1143 } else { 1144 mProgressIcon.setVisibility(View.GONE); 1145 } 1146 if (mListFooterProgress != null) { 1147 mListFooterProgress.setVisibility(visible ? View.VISIBLE : View.GONE); 1148 } 1149 setListFooterText(visible); 1150 break; 1151 case MSG_LOOKUP_MAILBOX_TYPE: 1152 // kill running async task, if any 1153 if (mFindMailboxTask != null && 1154 mFindMailboxTask.getStatus() != FindMailboxTask.Status.FINISHED) { 1155 mFindMailboxTask.cancel(true); 1156 mFindMailboxTask = null; 1157 } 1158 // start new one. do not recurse back to controller. 1159 long accountId = ((Long)msg.obj).longValue(); 1160 int mailboxType = msg.arg1; 1161 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false); 1162 mFindMailboxTask.execute(); 1163 break; 1164 case MSG_ERROR_BANNER: 1165 String message = (String) msg.obj; 1166 boolean isVisible = mErrorBanner.getVisibility() == View.VISIBLE; 1167 if (message != null) { 1168 mErrorBanner.setText(message); 1169 if (!isVisible) { 1170 mErrorBanner.setVisibility(View.VISIBLE); 1171 mErrorBanner.startAnimation( 1172 AnimationUtils.loadAnimation( 1173 MessageList.this, R.anim.header_appear)); 1174 } 1175 } else { 1176 if (isVisible) { 1177 mErrorBanner.setVisibility(View.GONE); 1178 mErrorBanner.startAnimation( 1179 AnimationUtils.loadAnimation( 1180 MessageList.this, R.anim.header_disappear)); 1181 } 1182 } 1183 break; 1184 default: 1185 super.handleMessage(msg); 1186 } 1187 } 1188 1189 /** 1190 * Call from any thread to start/stop progress indicator(s) 1191 * @param progress true to start, false to stop 1192 */ 1193 public void progress(boolean progress) { 1194 android.os.Message msg = android.os.Message.obtain(); 1195 msg.what = MSG_PROGRESS; 1196 msg.arg1 = progress ? 1 : 0; 1197 sendMessage(msg); 1198 } 1199 1200 /** 1201 * Called from any thread to look for a mailbox of a specific type. This is designed 1202 * to be called from the Controller's MailboxList callback; It instructs the async task 1203 * not to recurse, in case the mailbox is not found after this. 1204 * 1205 * See FindMailboxTask for more notes on this handler. 1206 */ 1207 public void lookupMailboxType(long accountId, int mailboxType) { 1208 android.os.Message msg = android.os.Message.obtain(); 1209 msg.what = MSG_LOOKUP_MAILBOX_TYPE; 1210 msg.arg1 = mailboxType; 1211 msg.obj = Long.valueOf(accountId); 1212 sendMessage(msg); 1213 } 1214 1215 /** 1216 * Called from any thread to show or hide the connection error banner. 1217 * @param message error text or null to hide the box 1218 */ 1219 public void showErrorBanner(String message) { 1220 android.os.Message msg = android.os.Message.obtain(); 1221 msg.what = MSG_ERROR_BANNER; 1222 msg.obj = message; 1223 sendMessage(msg); 1224 } 1225 } 1226 1227 /** 1228 * Callback for async Controller results. 1229 */ 1230 private class ControllerResults implements Controller.Result { 1231 1232 // This is used to alter the connection banner operation for sending messages 1233 MessagingException mSendMessageException; 1234 1235 // These are preset for use by updateMailboxListCallback 1236 int mWaitForMailboxType = -1; 1237 1238 // TODO check accountKey and only react to relevant notifications 1239 public void updateMailboxListCallback(MessagingException result, long accountKey, 1240 int progress) { 1241 // no updateBanner here, we are only listing a single mailbox 1242 updateProgress(result, progress); 1243 if (progress == 100) { 1244 mHandler.lookupMailboxType(accountKey, mWaitForMailboxType); 1245 } 1246 } 1247 1248 // TODO check accountKey and only react to relevant notifications 1249 public void updateMailboxCallback(MessagingException result, long accountKey, 1250 long mailboxKey, int progress, int numNewMessages) { 1251 updateBanner(result, progress, mailboxKey); 1252 if (result != null || progress == 100) { 1253 Email.updateMailboxRefreshTime(mMailboxId); 1254 } 1255 updateProgress(result, progress); 1256 } 1257 1258 public void loadMessageForViewCallback(MessagingException result, long messageId, 1259 int progress) { 1260 } 1261 1262 public void loadAttachmentCallback(MessagingException result, long messageId, 1263 long attachmentId, int progress) { 1264 } 1265 1266 public void serviceCheckMailCallback(MessagingException result, long accountId, 1267 long mailboxId, int progress, long tag) { 1268 } 1269 1270 /** 1271 * We alter the updateBanner hysteresis here to capture any failures and handle 1272 * them just once at the end. This callback is overly overloaded: 1273 * result == null, messageId == -1, progress == 0: start batch send 1274 * result == null, messageId == xx, progress == 0: start sending one message 1275 * result == xxxx, messageId == xx, progress == 0; failed sending one message 1276 * result == null, messageId == -1, progres == 100; finish sending batch 1277 */ 1278 public void sendMailCallback(MessagingException result, long accountId, long messageId, 1279 int progress) { 1280 if (mListFooterMode == LIST_FOOTER_MODE_SEND) { 1281 // reset captured error when we start sending one or more messages 1282 if (messageId == -1 && result == null && progress == 0) { 1283 mSendMessageException = null; 1284 } 1285 // capture first exception that comes along 1286 if (result != null && mSendMessageException == null) { 1287 mSendMessageException = result; 1288 } 1289 // if we're completing the sequence, change the banner state 1290 if (messageId == -1 && progress == 100) { 1291 updateBanner(mSendMessageException, progress, mMailboxId); 1292 } 1293 // always update the spinner, which has less state to worry about 1294 updateProgress(result, progress); 1295 } 1296 } 1297 1298 private void updateProgress(MessagingException result, int progress) { 1299 if (result != null || progress == 100) { 1300 mHandler.progress(false); 1301 } else if (progress == 0) { 1302 mHandler.progress(true); 1303 } 1304 } 1305 1306 /** 1307 * Show or hide the connection error banner, and convert the various MessagingException 1308 * variants into localizable text. There is hysteresis in the show/hide logic: Once shown, 1309 * the banner will remain visible until some progress is made on the connection. The 1310 * goal is to keep it from flickering during retries in a bad connection state. 1311 * 1312 * @param result 1313 * @param progress 1314 */ 1315 private void updateBanner(MessagingException result, int progress, long mailboxKey) { 1316 if (mailboxKey != mMailboxId) { 1317 return; 1318 } 1319 if (result != null) { 1320 int id = R.string.status_network_error; 1321 if (result instanceof AuthenticationFailedException) { 1322 id = R.string.account_setup_failed_dlg_auth_message; 1323 } else if (result instanceof CertificateValidationException) { 1324 id = R.string.account_setup_failed_dlg_certificate_message; 1325 } else { 1326 switch (result.getExceptionType()) { 1327 case MessagingException.IOERROR: 1328 id = R.string.account_setup_failed_ioerror; 1329 break; 1330 case MessagingException.TLS_REQUIRED: 1331 id = R.string.account_setup_failed_tls_required; 1332 break; 1333 case MessagingException.AUTH_REQUIRED: 1334 id = R.string.account_setup_failed_auth_required; 1335 break; 1336 case MessagingException.GENERAL_SECURITY: 1337 id = R.string.account_setup_failed_security; 1338 break; 1339 } 1340 } 1341 mHandler.showErrorBanner(getString(id)); 1342 } else if (progress > 0) { 1343 mHandler.showErrorBanner(null); 1344 } 1345 } 1346 } 1347 1348 /** 1349 * This class implements the adapter for displaying messages based on cursors. 1350 */ 1351 /* package */ class MessageListAdapter extends CursorAdapter { 1352 1353 public static final int COLUMN_ID = 0; 1354 public static final int COLUMN_MAILBOX_KEY = 1; 1355 public static final int COLUMN_ACCOUNT_KEY = 2; 1356 public static final int COLUMN_DISPLAY_NAME = 3; 1357 public static final int COLUMN_SUBJECT = 4; 1358 public static final int COLUMN_DATE = 5; 1359 public static final int COLUMN_READ = 6; 1360 public static final int COLUMN_FAVORITE = 7; 1361 public static final int COLUMN_ATTACHMENTS = 8; 1362 1363 public final String[] PROJECTION = new String[] { 1364 EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, 1365 MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP, 1366 MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT, 1367 }; 1368 1369 Context mContext; 1370 private LayoutInflater mInflater; 1371 private Drawable mAttachmentIcon; 1372 private Drawable mFavoriteIconOn; 1373 private Drawable mFavoriteIconOff; 1374 private Drawable mSelectedIconOn; 1375 private Drawable mSelectedIconOff; 1376 1377 private java.text.DateFormat mDateFormat; 1378 private java.text.DateFormat mDayFormat; 1379 private java.text.DateFormat mTimeFormat; 1380 1381 private HashSet<Long> mChecked = new HashSet<Long>(); 1382 1383 public MessageListAdapter(Context context) { 1384 super(context, null); 1385 mContext = context; 1386 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 1387 1388 Resources resources = context.getResources(); 1389 mAttachmentIcon = resources.getDrawable(R.drawable.ic_mms_attachment_small); 1390 mFavoriteIconOn = resources.getDrawable(android.R.drawable.star_on); 1391 mFavoriteIconOff = resources.getDrawable(android.R.drawable.star_off); 1392 mSelectedIconOn = resources.getDrawable(R.drawable.btn_check_buttonless_on); 1393 mSelectedIconOff = resources.getDrawable(R.drawable.btn_check_buttonless_off); 1394 1395 mDateFormat = android.text.format.DateFormat.getDateFormat(context); // short date 1396 mDayFormat = android.text.format.DateFormat.getDateFormat(context); // TODO: day 1397 mTimeFormat = android.text.format.DateFormat.getTimeFormat(context); // 12/24 time 1398 } 1399 1400 public Set<Long> getSelectedSet() { 1401 return mChecked; 1402 } 1403 1404 @Override 1405 public void bindView(View view, Context context, Cursor cursor) { 1406 // Reset the view (in case it was recycled) and prepare for binding 1407 MessageListItem itemView = (MessageListItem) view; 1408 itemView.bindViewInit(this, true); 1409 1410 // Load the public fields in the view (for later use) 1411 itemView.mMessageId = cursor.getLong(COLUMN_ID); 1412 itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY); 1413 itemView.mAccountId = cursor.getLong(COLUMN_ACCOUNT_KEY); 1414 itemView.mRead = cursor.getInt(COLUMN_READ) != 0; 1415 itemView.mFavorite = cursor.getInt(COLUMN_FAVORITE) != 0; 1416 itemView.mSelected = mChecked.contains(Long.valueOf(itemView.mMessageId)); 1417 1418 // Load the UI 1419 View chipView = view.findViewById(R.id.chip); 1420 int chipResId = mColorChipResIds[(int)itemView.mAccountId % mColorChipResIds.length]; 1421 chipView.setBackgroundResource(chipResId); 1422 1423 TextView fromView = (TextView) view.findViewById(R.id.from); 1424 String text = cursor.getString(COLUMN_DISPLAY_NAME); 1425 fromView.setText(text); 1426 1427 TextView subjectView = (TextView) view.findViewById(R.id.subject); 1428 text = cursor.getString(COLUMN_SUBJECT); 1429 subjectView.setText(text); 1430 1431 boolean hasAttachments = cursor.getInt(COLUMN_ATTACHMENTS) != 0; 1432 subjectView.setCompoundDrawablesWithIntrinsicBounds(null, null, 1433 hasAttachments ? mAttachmentIcon : null, null); 1434 1435 // TODO ui spec suggests "time", "day", "date" - implement "day" 1436 TextView dateView = (TextView) view.findViewById(R.id.date); 1437 long timestamp = cursor.getLong(COLUMN_DATE); 1438 Date date = new Date(timestamp); 1439 if (Utility.isDateToday(date)) { 1440 text = mTimeFormat.format(date); 1441 } else { 1442 text = mDateFormat.format(date); 1443 } 1444 dateView.setText(text); 1445 1446 if (itemView.mRead) { 1447 subjectView.setTypeface(Typeface.DEFAULT); 1448 fromView.setTypeface(Typeface.DEFAULT); 1449 view.setBackgroundDrawable(context.getResources().getDrawable( 1450 R.drawable.message_list_item_background_read)); 1451 } else { 1452 subjectView.setTypeface(Typeface.DEFAULT_BOLD); 1453 fromView.setTypeface(Typeface.DEFAULT_BOLD); 1454 view.setBackgroundDrawable(context.getResources().getDrawable( 1455 R.drawable.message_list_item_background_unread)); 1456 } 1457 1458 ImageView selectedView = (ImageView) view.findViewById(R.id.selected); 1459 selectedView.setImageDrawable(itemView.mSelected ? mSelectedIconOn : mSelectedIconOff); 1460 1461 ImageView favoriteView = (ImageView) view.findViewById(R.id.favorite); 1462 favoriteView.setImageDrawable(itemView.mFavorite ? mFavoriteIconOn : mFavoriteIconOff); 1463 } 1464 1465 @Override 1466 public View newView(Context context, Cursor cursor, ViewGroup parent) { 1467 return mInflater.inflate(R.layout.message_list_item, parent, false); 1468 } 1469 1470 /** 1471 * This is used as a callback from the list items, to set the selected state 1472 * 1473 * @param itemView the item being changed 1474 * @param newSelected the new value of the selected flag (checkbox state) 1475 */ 1476 public void updateSelected(MessageListItem itemView, boolean newSelected) { 1477 ImageView selectedView = (ImageView) itemView.findViewById(R.id.selected); 1478 selectedView.setImageDrawable(newSelected ? mSelectedIconOn : mSelectedIconOff); 1479 1480 // Set checkbox state in list, and show/hide panel if necessary 1481 Long id = Long.valueOf(itemView.mMessageId); 1482 if (newSelected) { 1483 mChecked.add(id); 1484 } else { 1485 mChecked.remove(id); 1486 } 1487 1488 MessageList.this.showMultiPanel(mChecked.size() > 0); 1489 } 1490 1491 /** 1492 * This is used as a callback from the list items, to set the favorite state 1493 * 1494 * @param itemView the item being changed 1495 * @param newFavorite the new value of the favorite flag (star state) 1496 */ 1497 public void updateFavorite(MessageListItem itemView, boolean newFavorite) { 1498 ImageView favoriteView = (ImageView) itemView.findViewById(R.id.favorite); 1499 favoriteView.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff); 1500 onSetMessageFavorite(itemView.mMessageId, newFavorite); 1501 } 1502 } 1503} 1504