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