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