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