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