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