MessageList.java revision d2174733b146eeccd5f3b3b95f98e1e1aaafb257
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.MessagingException; 25import com.android.email.provider.EmailContent; 26import com.android.email.provider.EmailContent.Account; 27import com.android.email.provider.EmailContent.AccountColumns; 28import com.android.email.provider.EmailContent.Mailbox; 29import com.android.email.provider.EmailContent.MailboxColumns; 30import com.android.email.provider.EmailContent.MessageColumns; 31import com.android.email.service.MailService; 32 33import android.app.ListActivity; 34import android.app.NotificationManager; 35import android.content.ContentResolver; 36import android.content.ContentUris; 37import android.content.Context; 38import android.content.Intent; 39import android.content.res.Resources; 40import android.database.Cursor; 41import android.graphics.Typeface; 42import android.graphics.drawable.Drawable; 43import android.net.Uri; 44import android.os.AsyncTask; 45import android.os.Bundle; 46import android.os.Handler; 47import android.util.Log; 48import android.view.ContextMenu; 49import android.view.LayoutInflater; 50import android.view.Menu; 51import android.view.MenuItem; 52import android.view.View; 53import android.view.ViewGroup; 54import android.view.Window; 55import android.view.ContextMenu.ContextMenuInfo; 56import android.view.View.OnClickListener; 57import android.view.animation.AnimationUtils; 58import android.widget.AdapterView; 59import android.widget.Button; 60import android.widget.CursorAdapter; 61import android.widget.ImageView; 62import android.widget.ListView; 63import android.widget.ProgressBar; 64import android.widget.TextView; 65import android.widget.Toast; 66import android.widget.AdapterView.OnItemClickListener; 67 68import java.util.Date; 69import java.util.HashSet; 70import java.util.Set; 71 72public class MessageList extends ListActivity implements OnItemClickListener, OnClickListener { 73 // Intent extras (internal to this activity) 74 private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID"; 75 private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE"; 76 private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID"; 77 78 // UI support 79 private ListView mListView; 80 private View mMultiSelectPanel; 81 private Button mReadUnreadButton; 82 private Button mFavoriteButton; 83 private Button mDeleteButton; 84 private View mListFooterView; 85 private TextView mListFooterText; 86 private View mListFooterProgress; 87 88 private static final int LIST_FOOTER_MODE_NONE = 0; 89 private static final int LIST_FOOTER_MODE_REFRESH = 1; 90 private static final int LIST_FOOTER_MODE_MORE = 2; 91 private static final int LIST_FOOTER_MODE_SEND = 3; 92 private int mListFooterMode; 93 94 private MessageListAdapter mListAdapter; 95 private MessageListHandler mHandler = new MessageListHandler(); 96 private Controller mController = Controller.getInstance(getApplication()); 97 private ControllerResults mControllerCallback = new ControllerResults(); 98 private TextView mLeftTitle; 99 private TextView mRightTitle; 100 private ProgressBar mProgressIcon; 101 102 private static final int[] mColorChipResIds = new int[] { 103 R.drawable.appointment_indicator_leftside_1, 104 R.drawable.appointment_indicator_leftside_2, 105 R.drawable.appointment_indicator_leftside_3, 106 R.drawable.appointment_indicator_leftside_4, 107 R.drawable.appointment_indicator_leftside_5, 108 R.drawable.appointment_indicator_leftside_6, 109 R.drawable.appointment_indicator_leftside_7, 110 R.drawable.appointment_indicator_leftside_8, 111 R.drawable.appointment_indicator_leftside_9, 112 R.drawable.appointment_indicator_leftside_10, 113 R.drawable.appointment_indicator_leftside_11, 114 R.drawable.appointment_indicator_leftside_12, 115 R.drawable.appointment_indicator_leftside_13, 116 R.drawable.appointment_indicator_leftside_14, 117 R.drawable.appointment_indicator_leftside_15, 118 R.drawable.appointment_indicator_leftside_16, 119 R.drawable.appointment_indicator_leftside_17, 120 R.drawable.appointment_indicator_leftside_18, 121 R.drawable.appointment_indicator_leftside_19, 122 R.drawable.appointment_indicator_leftside_20, 123 R.drawable.appointment_indicator_leftside_21, 124 }; 125 126 // DB access 127 private ContentResolver mResolver; 128 private long mMailboxId; 129 private LoadMessagesTask mLoadMessagesTask; 130 private FindMailboxTask mFindMailboxTask; 131 private SetTitleTask mSetTitleTask; 132 private SetFooterTask mSetFooterTask; 133 134 public final static String[] MAILBOX_FIND_INBOX_PROJECTION = new String[] { 135 EmailContent.RECORD_ID, MailboxColumns.TYPE, MailboxColumns.FLAG_VISIBLE 136 }; 137 138 private static final int MAILBOX_NAME_COLUMN_ID = 0; 139 private static final int MAILBOX_NAME_COLUMN_ACCOUNT_KEY = 1; 140 private static final int MAILBOX_NAME_COLUMN_TYPE = 2; 141 private static final String[] MAILBOX_NAME_PROJECTION = new String[] { 142 MailboxColumns.DISPLAY_NAME, MailboxColumns.ACCOUNT_KEY, 143 MailboxColumns.TYPE}; 144 145 private static final int ACCOUNT_DISPLAY_NAME_COLUMN_ID = 0; 146 private static final String[] ACCOUNT_NAME_PROJECTION = new String[] { 147 AccountColumns.DISPLAY_NAME }; 148 149 private static final String ID_SELECTION = EmailContent.RECORD_ID + "=?"; 150 151 private Boolean mPushModeMailbox = null; 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.putExtra(EXTRA_MAILBOX_ID, id); 165 context.startActivity(intent); 166 } 167 168 /** 169 * Open a specific mailbox by account & type 170 * 171 * @param context The caller's context (for generating an intent) 172 * @param accountId The account to open 173 * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX) 174 */ 175 public static void actionHandleAccount(Context context, long accountId, int mailboxType) { 176 Intent intent = new Intent(context, MessageList.class); 177 intent.putExtra(EXTRA_ACCOUNT_ID, accountId); 178 intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType); 179 context.startActivity(intent); 180 } 181 182 /** 183 * Return an intent to open a specific mailbox by account & type. It will also clear 184 * notifications. 185 * 186 * @param context The caller's context (for generating an intent) 187 * @param accountId The account to open, or -1 188 * @param mailboxId the ID of the mailbox to open, or -1 189 * @param mailboxType the type of mailbox to open (e.g. @see Mailbox.TYPE_INBOX) or -1 190 */ 191 public static Intent actionHandleAccountIntent(Context context, long accountId, 192 long mailboxId, int mailboxType) { 193 Intent intent = new Intent(context, MessageList.class); 194 intent.putExtra(EXTRA_ACCOUNT_ID, accountId); 195 intent.putExtra(EXTRA_MAILBOX_ID, mailboxId); 196 intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType); 197 return intent; 198 } 199 200 /** 201 * Used for generating lightweight (Uri-only) intents. 202 * 203 * @param context Calling context for building the intent 204 * @param accountId The account of interest 205 * @param mailboxType The folder name to open (typically Mailbox.TYPE_INBOX) 206 * @return an Intent which can be used to view that account 207 */ 208 public static Intent actionHandleAccountUriIntent(Context context, long accountId, 209 int mailboxType) { 210 Intent i = actionHandleAccountIntent(context, accountId, -1, mailboxType); 211 i.removeExtra(EXTRA_ACCOUNT_ID); 212 Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 213 i.setData(uri); 214 return i; 215 } 216 217 @Override 218 public void onCreate(Bundle icicle) { 219 super.onCreate(icicle); 220 221 requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); 222 setContentView(R.layout.message_list); 223 getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, 224 R.layout.list_title); 225 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 232 mLeftTitle = (TextView) findViewById(R.id.title_left_text); 233 mRightTitle = (TextView) findViewById(R.id.title_right_text); 234 mProgressIcon = (ProgressBar) findViewById(R.id.title_progress_icon); 235 236 mReadUnreadButton.setOnClickListener(this); 237 mFavoriteButton.setOnClickListener(this); 238 mDeleteButton.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 284 // TODO set title to "account > mailbox (#unread)" 285 } 286 287 @Override 288 public void onPause() { 289 super.onPause(); 290 mController.removeResultCallback(mControllerCallback); 291 } 292 293 @Override 294 public void onResume() { 295 super.onResume(); 296 mController.addResultCallback(mControllerCallback); 297 298 // clear notifications here 299 NotificationManager notificationManager = (NotificationManager) 300 getSystemService(Context.NOTIFICATION_SERVICE); 301 notificationManager.cancel(MailService.NEW_MESSAGE_NOTIFICATION_ID); 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 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 332 if (view != mListFooterView) { 333 MessageListItem itemView = (MessageListItem) view; 334 onOpenMessage(id, itemView.mMailboxId); 335 } else { 336 doFooterClick(); 337 } 338 } 339 340 public void onClick(View v) { 341 switch (v.getId()) { 342 case R.id.btn_read_unread: 343 onMultiToggleRead(mListAdapter.getSelectedSet()); 344 break; 345 case R.id.btn_multi_favorite: 346 onMultiToggleFavorite(mListAdapter.getSelectedSet()); 347 break; 348 case R.id.btn_multi_delete: 349 onMultiDelete(mListAdapter.getSelectedSet()); 350 break; 351 } 352 } 353 354 @Override 355 public boolean onCreateOptionsMenu(Menu menu) { 356 super.onCreateOptionsMenu(menu); 357 if (mMailboxId < 0) { 358 getMenuInflater().inflate(R.menu.message_list_option_smart_folder, menu); 359 } else { 360 getMenuInflater().inflate(R.menu.message_list_option, menu); 361 } 362 return true; 363 } 364 365 @Override 366 public boolean onOptionsItemSelected(MenuItem item) { 367 switch (item.getItemId()) { 368 case R.id.refresh: 369 onRefresh(); 370 return true; 371 case R.id.folders: 372 onFolders(); 373 return true; 374 case R.id.accounts: 375 onAccounts(); 376 return true; 377 case R.id.compose: 378 onCompose(); 379 return true; 380 case R.id.account_settings: 381 onEditAccount(); 382 return true; 383 default: 384 return super.onOptionsItemSelected(item); 385 } 386 } 387 388 @Override 389 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 390 super.onCreateContextMenu(menu, v, menuInfo); 391 392 AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; 393 // There is no context menu for the list footer 394 if (info.targetView == mListFooterView) { 395 return; 396 } 397 MessageListItem itemView = (MessageListItem) info.targetView; 398 399 Cursor c = (Cursor) mListView.getItemAtPosition(info.position); 400 String messageName = c.getString(MessageListAdapter.COLUMN_SUBJECT); 401 402 menu.setHeaderTitle(messageName); 403 404 // TODO: There is probably a special context menu for the trash 405 Mailbox mailbox = Mailbox.restoreMailboxWithId(this, itemView.mMailboxId); 406 407 switch (mailbox.mType) { 408 case EmailContent.Mailbox.TYPE_DRAFTS: 409 getMenuInflater().inflate(R.menu.message_list_context_drafts, menu); 410 break; 411 case EmailContent.Mailbox.TYPE_OUTBOX: 412 getMenuInflater().inflate(R.menu.message_list_context_outbox, menu); 413 break; 414 case EmailContent.Mailbox.TYPE_TRASH: 415 getMenuInflater().inflate(R.menu.message_list_context_trash, menu); 416 break; 417 default: 418 getMenuInflater().inflate(R.menu.message_list_context, menu); 419 // The default menu contains "mark as read". If the message is read, change 420 // the menu text to "mark as unread." 421 if (itemView.mRead) { 422 menu.findItem(R.id.mark_as_read).setTitle(R.string.mark_as_unread_action); 423 } 424 break; 425 } 426 } 427 428 @Override 429 public boolean onContextItemSelected(MenuItem item) { 430 AdapterView.AdapterContextMenuInfo info = 431 (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 432 MessageListItem itemView = (MessageListItem) info.targetView; 433 434 switch (item.getItemId()) { 435 case R.id.open: 436 onOpenMessage(info.id, itemView.mMailboxId); 437 break; 438 case R.id.delete: 439 onDelete(info.id, itemView.mAccountId); 440 break; 441 case R.id.reply: 442 onReply(itemView.mMessageId); 443 break; 444 case R.id.reply_all: 445 onReplyAll(itemView.mMessageId); 446 break; 447 case R.id.forward: 448 onForward(itemView.mMessageId); 449 break; 450 case R.id.mark_as_read: 451 onSetMessageRead(info.id, !itemView.mRead); 452 break; 453 } 454 return super.onContextItemSelected(item); 455 } 456 457 private void onRefresh() { 458 // TODO: Should not be reading from DB in UI thread - need a cleaner way to get accountId 459 if (mMailboxId >= 0) { 460 Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mMailboxId); 461 mController.updateMailbox(mailbox.mAccountKey, mMailboxId, mControllerCallback); 462 } 463 } 464 465 private void onFolders() { 466 if (mMailboxId >= 0) { 467 // TODO smaller projection 468 Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mMailboxId); 469 MailboxList.actionHandleAccount(this, mailbox.mAccountKey); 470 finish(); 471 } 472 } 473 474 private void onAccounts() { 475 AccountFolderList.actionShowAccounts(this); 476 finish(); 477 } 478 479 private long lookupAccountIdFromMailboxId(long mailboxId) { 480 // TODO: Select correct account to send from when there are multiple mailboxes 481 // TODO: Should not be reading from DB in UI thread 482 if (mailboxId < 0) { 483 return -1; // no info, default account 484 } 485 EmailContent.Mailbox mailbox = 486 EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId); 487 return mailbox.mAccountKey; 488 } 489 490 private void onCompose() { 491 MessageCompose.actionCompose(this, lookupAccountIdFromMailboxId(mMailboxId)); 492 } 493 494 private void onEditAccount() { 495 AccountSettings.actionSettings(this, lookupAccountIdFromMailboxId(mMailboxId)); 496 } 497 498 private void onOpenMessage(long messageId, long mailboxId) { 499 // TODO: Should not be reading from DB in UI thread 500 EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId); 501 502 if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) { 503 MessageCompose.actionEditDraft(this, messageId); 504 } else { 505 MessageView.actionView(this, messageId, mailboxId); 506 } 507 } 508 509 private void onReply(long messageId) { 510 MessageCompose.actionReply(this, messageId, false); 511 } 512 513 private void onReplyAll(long messageId) { 514 MessageCompose.actionReply(this, messageId, true); 515 } 516 517 private void onForward(long messageId) { 518 MessageCompose.actionForward(this, messageId); 519 } 520 521 private void onLoadMoreMessages() { 522 if (mMailboxId >= 0) { 523 mController.loadMoreMessages(mMailboxId, mControllerCallback); 524 } 525 } 526 527 private void onSendPendingMessages() { 528 long accountId = lookupAccountIdFromMailboxId(mMailboxId); 529 mController.sendPendingMessages(accountId, mControllerCallback); 530 } 531 532 private void onDelete(long messageId, long accountId) { 533 mController.deleteMessage(messageId, accountId); 534 Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show(); 535 } 536 537 private void onSetMessageRead(long messageId, boolean newRead) { 538 mController.setMessageRead(messageId, newRead); 539 } 540 541 private void onSetMessageFavorite(long messageId, boolean newFavorite) { 542 mController.setMessageFavorite(messageId, newFavorite); 543 } 544 545 /** 546 * Toggles a set read/unread states. Note, the default behavior is "mark unread", so the 547 * sense of the helper methods is "true=unread". 548 * 549 * @param selectedSet The current list of selected items 550 */ 551 private void onMultiToggleRead(Set<Long> selectedSet) { 552 toggleMultiple(selectedSet, new MultiToggleHelper() { 553 554 public boolean getField(long messageId, Cursor c) { 555 return c.getInt(MessageListAdapter.COLUMN_READ) == 0; 556 } 557 558 public boolean setField(long messageId, Cursor c, boolean newValue) { 559 boolean oldValue = getField(messageId, c); 560 if (oldValue != newValue) { 561 onSetMessageRead(messageId, !newValue); 562 return true; 563 } 564 return false; 565 } 566 }); 567 } 568 569 /** 570 * Toggles a set of favorites (stars) 571 * 572 * @param selectedSet The current list of selected items 573 */ 574 private void onMultiToggleFavorite(Set<Long> selectedSet) { 575 toggleMultiple(selectedSet, new MultiToggleHelper() { 576 577 public boolean getField(long messageId, Cursor c) { 578 return c.getInt(MessageListAdapter.COLUMN_FAVORITE) != 0; 579 } 580 581 public boolean setField(long messageId, Cursor c, boolean newValue) { 582 boolean oldValue = getField(messageId, c); 583 if (oldValue != newValue) { 584 onSetMessageFavorite(messageId, newValue); 585 return true; 586 } 587 return false; 588 } 589 }); 590 } 591 592 private void onMultiDelete(Set<Long> selectedSet) { 593 // Clone the set, because deleting is going to thrash things 594 HashSet<Long> cloneSet = new HashSet<Long>(selectedSet); 595 for (Long id : cloneSet) { 596 mController.deleteMessage(id, -1); 597 } 598 // TODO: count messages and show "n messages deleted" 599 Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show(); 600 selectedSet.clear(); 601 showMultiPanel(false); 602 } 603 604 private interface MultiToggleHelper { 605 /** 606 * Return true if the field of interest is "set". If one or more are false, then our 607 * bulk action will be to "set". If all are set, our bulk action will be to "clear". 608 * @param messageId the message id of the current message 609 * @param c the cursor, positioned to the item of interest 610 * @return true if the field at this row is "set" 611 */ 612 public boolean getField(long messageId, Cursor c); 613 614 /** 615 * Set or clear the field of interest. Return true if a change was made. 616 * @param messageId the message id of the current message 617 * @param c the cursor, positioned to the item of interest 618 * @param newValue the new value to be set at this row 619 * @return true if a change was actually made 620 */ 621 public boolean setField(long messageId, Cursor c, boolean newValue); 622 } 623 624 /** 625 * Toggle multiple fields in a message, using the following logic: If one or more fields 626 * are "clear", then "set" them. If all fields are "set", then "clear" them all. 627 * 628 * @param selectedSet the set of messages that are selected 629 * @param helper functions to implement the specific getter & setter 630 * @return the number of messages that were updated 631 */ 632 private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) { 633 Cursor c = mListAdapter.getCursor(); 634 boolean anyWereFound = false; 635 boolean allWereSet = true; 636 637 c.moveToPosition(-1); 638 while (c.moveToNext()) { 639 long id = c.getInt(MessageListAdapter.COLUMN_ID); 640 if (selectedSet.contains(Long.valueOf(id))) { 641 anyWereFound = true; 642 if (!helper.getField(id, c)) { 643 allWereSet = false; 644 break; 645 } 646 } 647 } 648 649 int numChanged = 0; 650 651 if (anyWereFound) { 652 boolean newValue = !allWereSet; 653 c.moveToPosition(-1); 654 while (c.moveToNext()) { 655 long id = c.getInt(MessageListAdapter.COLUMN_ID); 656 if (selectedSet.contains(Long.valueOf(id))) { 657 if (helper.setField(id, c, newValue)) { 658 ++numChanged; 659 } 660 } 661 } 662 } 663 664 return numChanged; 665 } 666 667 /** 668 * Test selected messages for showing appropriate labels 669 * @param selectedSet 670 * @param column_id 671 * @param defaultflag 672 * @return true when the specified flagged message is selected 673 */ 674 private boolean testMultiple(Set<Long> selectedSet, int column_id, boolean defaultflag) { 675 Cursor c = mListAdapter.getCursor(); 676 c.moveToPosition(-1); 677 while (c.moveToNext()) { 678 long id = c.getInt(MessageListAdapter.COLUMN_ID); 679 if (selectedSet.contains(Long.valueOf(id))) { 680 if (c.getInt(column_id) == (defaultflag? 1 : 0)) { 681 return true; 682 } 683 } 684 } 685 return false; 686 } 687 688 private void autoRefreshStaleMailbox() { 689 if ((mListAdapter.getCursor() == null) // Check if messages info is loaded 690 || (mPushModeMailbox != null && mPushModeMailbox) // Check the push mode 691 || (mMailboxId < 0) // Check if this mailbox is synthetic/combined 692 || !Email.mailboxRequiresRefresh(mMailboxId)) { 693 return; 694 } 695 onRefresh(); 696 } 697 698 private void updateFooterButtonNames () { 699 // Show "unread_action" when one or more read messages are selected. 700 if (testMultiple(mListAdapter.getSelectedSet(), MessageListAdapter.COLUMN_READ, true)) { 701 mReadUnreadButton.setText(R.string.unread_action); 702 } else { 703 mReadUnreadButton.setText(R.string.read_action); 704 } 705 // Show "set_star_action" when one or more un-starred messages are selected. 706 if (testMultiple(mListAdapter.getSelectedSet(), 707 MessageListAdapter.COLUMN_FAVORITE, false)) { 708 mFavoriteButton.setText(R.string.set_star_action); 709 } else { 710 mFavoriteButton.setText(R.string.remove_star_action); 711 } 712 } 713 714 /** 715 * Show or hide the panel of multi-select options 716 */ 717 private void showMultiPanel(boolean show) { 718 if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) { 719 mMultiSelectPanel.setVisibility(View.VISIBLE); 720 mMultiSelectPanel.startAnimation( 721 AnimationUtils.loadAnimation(this, R.anim.footer_appear)); 722 } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) { 723 mMultiSelectPanel.setVisibility(View.GONE); 724 mMultiSelectPanel.startAnimation( 725 AnimationUtils.loadAnimation(this, R.anim.footer_disappear)); 726 } 727 if (show) { 728 updateFooterButtonNames(); 729 } 730 } 731 732 /** 733 * Add the fixed footer view if appropriate (not always - not all accounts & mailboxes). 734 * 735 * Here are some rules (finish this list): 736 * 737 * Any merged box (except send): refresh 738 * Any push-mode account: refresh 739 * Any non-push-mode account: load more 740 * Any outbox (send again): 741 * 742 * @param mailboxId the ID of the mailbox 743 */ 744 private void addFooterView(long mailboxId, long accountId, int mailboxType) { 745 // first, look for shortcuts that don't need us to spin up a DB access task 746 if (mailboxId == Mailbox.QUERY_ALL_INBOXES 747 || mailboxId == Mailbox.QUERY_ALL_UNREAD 748 || mailboxId == Mailbox.QUERY_ALL_FAVORITES 749 || mailboxId == Mailbox.QUERY_ALL_DRAFTS) { 750 finishFooterView(LIST_FOOTER_MODE_REFRESH); 751 return; 752 } 753 if (mailboxId == Mailbox.QUERY_ALL_OUTBOX || mailboxType == Mailbox.TYPE_OUTBOX) { 754 finishFooterView(LIST_FOOTER_MODE_SEND); 755 return; 756 } 757 758 // We don't know enough to select the footer command type (yet), so we'll 759 // launch an async task to do the remaining lookups and decide what to do 760 mSetFooterTask = new SetFooterTask(); 761 mSetFooterTask.execute(mailboxId, accountId); 762 } 763 764 private final static String[] MAILBOX_ACCOUNT_AND_TYPE_PROJECTION = 765 new String[] { MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE }; 766 767 private class SetFooterTask extends AsyncTask<Long, Void, Integer> { 768 /** 769 * There are two operational modes here, requiring different lookup. 770 * mailboxIs != -1: A specific mailbox - check its type, then look up its account 771 * accountId != -1: A specific account - look up the account 772 */ 773 @Override 774 protected Integer doInBackground(Long... params) { 775 long mailboxId = params[0]; 776 long accountId = params[1]; 777 int mailboxType = -1; 778 if (mailboxId != -1) { 779 try { 780 Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId); 781 Cursor c = mResolver.query(uri, MAILBOX_ACCOUNT_AND_TYPE_PROJECTION, 782 null, null, null); 783 if (c.moveToFirst()) { 784 try { 785 accountId = c.getLong(0); 786 mailboxType = c.getInt(1); 787 } finally { 788 c.close(); 789 } 790 } 791 } catch (IllegalArgumentException iae) { 792 // can't do any more here 793 return LIST_FOOTER_MODE_NONE; 794 } 795 } 796 if (mailboxType == Mailbox.TYPE_OUTBOX) { 797 return LIST_FOOTER_MODE_SEND; 798 } 799 if (accountId != -1) { 800 // This is inefficient but the best fix is not here but in isMessagingController 801 Account account = Account.restoreAccountWithId(MessageList.this, accountId); 802 if (account != null) { 803 mPushModeMailbox = account.mSyncInterval == Account.CHECK_INTERVAL_PUSH; 804 if (MessageList.this.mController.isMessagingController(account)) { 805 return LIST_FOOTER_MODE_MORE; // IMAP or POP 806 } else { 807 return LIST_FOOTER_MODE_REFRESH; // EAS 808 } 809 } 810 } 811 return LIST_FOOTER_MODE_NONE; 812 } 813 814 @Override 815 protected void onPostExecute(Integer listFooterMode) { 816 finishFooterView(listFooterMode); 817 } 818 } 819 820 /** 821 * Add the fixed footer view as specified, and set up the test as well. 822 * 823 * @param listFooterMode the footer mode we've determined should be used for this list 824 */ 825 private void finishFooterView(int listFooterMode) { 826 mListFooterMode = listFooterMode; 827 if (mListFooterMode != LIST_FOOTER_MODE_NONE) { 828 mListFooterView = ((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE)) 829 .inflate(R.layout.message_list_item_footer, mListView, false); 830 mList.addFooterView(mListFooterView); 831 setListAdapter(mListAdapter); 832 833 mListFooterProgress = mListFooterView.findViewById(R.id.progress); 834 mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text); 835 setListFooterText(false); 836 } 837 } 838 839 /** 840 * Set the list footer text based on mode and "active" status 841 */ 842 private void setListFooterText(boolean active) { 843 if (mListFooterMode != LIST_FOOTER_MODE_NONE) { 844 int footerTextId = 0; 845 switch (mListFooterMode) { 846 case LIST_FOOTER_MODE_REFRESH: 847 footerTextId = active ? R.string.status_loading_more 848 : R.string.refresh_action; 849 break; 850 case LIST_FOOTER_MODE_MORE: 851 footerTextId = active ? R.string.status_loading_more 852 : R.string.message_list_load_more_messages_action; 853 break; 854 case LIST_FOOTER_MODE_SEND: 855 footerTextId = active ? R.string.status_sending_messages 856 : R.string.message_list_send_pending_messages_action; 857 break; 858 } 859 mListFooterText.setText(footerTextId); 860 } 861 } 862 863 /** 864 * Handle a click in the list footer, which changes meaning depending on what we're looking at. 865 */ 866 private void doFooterClick() { 867 switch (mListFooterMode) { 868 case LIST_FOOTER_MODE_NONE: // should never happen 869 break; 870 case LIST_FOOTER_MODE_REFRESH: 871 onRefresh(); 872 break; 873 case LIST_FOOTER_MODE_MORE: 874 onLoadMoreMessages(); 875 break; 876 case LIST_FOOTER_MODE_SEND: 877 onSendPendingMessages(); 878 break; 879 } 880 } 881 882 /** 883 * Async task for finding a single mailbox by type (possibly even going to the network). 884 * 885 * This is much too complex, as implemented. It uses this AsyncTask to check for a mailbox, 886 * then (if not found) a Controller call to refresh mailboxes from the server, and a handler 887 * to relaunch this task (a 2nd time) to read the results of the network refresh. The core 888 * problem is that we have two different non-UI-thread jobs (reading DB and reading network) 889 * and two different paradigms for dealing with them. Some unification would be needed here 890 * to make this cleaner. 891 * 892 * TODO: If this problem spreads to other operations, find a cleaner way to handle it. 893 */ 894 private class FindMailboxTask extends AsyncTask<Void, Void, Long> { 895 896 private long mAccountId; 897 private int mMailboxType; 898 private boolean mOkToRecurse; 899 900 /** 901 * Special constructor to cache some local info 902 */ 903 public FindMailboxTask(long accountId, int mailboxType, boolean okToRecurse) { 904 mAccountId = accountId; 905 mMailboxType = mailboxType; 906 mOkToRecurse = okToRecurse; 907 } 908 909 @Override 910 protected Long doInBackground(Void... params) { 911 // See if we can find the requested mailbox in the DB. 912 long mailboxId = Mailbox.findMailboxOfType(MessageList.this, mAccountId, mMailboxType); 913 if (mailboxId == -1 && mOkToRecurse) { 914 // Not found - launch network lookup 915 mControllerCallback.mWaitForMailboxType = mMailboxType; 916 mController.updateMailboxList(mAccountId, mControllerCallback); 917 } 918 return mailboxId; 919 } 920 921 @Override 922 protected void onPostExecute(Long mailboxId) { 923 if (mailboxId != -1) { 924 mMailboxId = mailboxId; 925 mSetTitleTask = new SetTitleTask(mMailboxId); 926 mSetTitleTask.execute(); 927 mLoadMessagesTask = new LoadMessagesTask(mMailboxId, mAccountId); 928 mLoadMessagesTask.execute(); 929 } 930 } 931 } 932 933 /** 934 * Async task for loading a single folder out of the UI thread 935 * 936 * The code here (for merged boxes) is a placeholder/hack and should be replaced. Some 937 * specific notes: 938 * TODO: Move the double query into a specialized URI that returns all inbox messages 939 * and do the dirty work in raw SQL in the provider. 940 * TODO: Generalize the query generation so we can reuse it in MessageView (for next/prev) 941 */ 942 private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> { 943 944 private long mMailboxKey; 945 private long mAccountKey; 946 947 /** 948 * Special constructor to cache some local info 949 */ 950 public LoadMessagesTask(long mailboxKey, long accountKey) { 951 mMailboxKey = mailboxKey; 952 mAccountKey = accountKey; 953 } 954 955 @Override 956 protected Cursor doInBackground(Void... params) { 957 String selection = 958 Utility.buildMailboxIdSelection(MessageList.this.mResolver, mMailboxKey); 959 Cursor c = MessageList.this.managedQuery( 960 EmailContent.Message.CONTENT_URI, 961 MessageList.this.mListAdapter.PROJECTION, 962 selection, null, 963 EmailContent.MessageColumns.TIMESTAMP + " DESC"); 964 return c; 965 } 966 967 @Override 968 protected void onPostExecute(Cursor cursor) { 969 if (cursor.isClosed()) { 970 return; 971 } 972 MessageList.this.mListAdapter.changeCursor(cursor); 973 autoRefreshStaleMailbox(); 974 // Reset the "new messages" count in the service, since we're seeing them now 975 if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) { 976 MailService.resetNewMessageCount(MessageList.this, -1); 977 } else if (mMailboxKey >= 0 && mAccountKey != -1) { 978 MailService.resetNewMessageCount(MessageList.this, mAccountKey); 979 } 980 } 981 } 982 983 private class SetTitleTask extends AsyncTask<Void, Void, String[]> { 984 985 private long mMailboxKey; 986 987 public SetTitleTask(long mailboxKey) { 988 mMailboxKey = mailboxKey; 989 } 990 991 @Override 992 protected String[] doInBackground(Void... params) { 993 // Check special Mailboxes 994 if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) { 995 return new String[] {null, 996 getString(R.string.account_folder_list_summary_inbox)}; 997 } else if (mMailboxKey == Mailbox.QUERY_ALL_FAVORITES) { 998 return new String[] {null, 999 getString(R.string.account_folder_list_summary_favorite)}; 1000 } else if (mMailboxKey == Mailbox.QUERY_ALL_DRAFTS) { 1001 return new String[] {null, 1002 getString(R.string.account_folder_list_summary_drafts)}; 1003 } else if (mMailboxKey == Mailbox.QUERY_ALL_OUTBOX) { 1004 return new String[] {null, 1005 getString(R.string.account_folder_list_summary_outbox)}; 1006 } 1007 String accountName = null; 1008 String mailboxName = null; 1009 String accountKey = null; 1010 Cursor c = MessageList.this.mResolver.query(Mailbox.CONTENT_URI, 1011 MAILBOX_NAME_PROJECTION, ID_SELECTION, 1012 new String[] { Long.toString(mMailboxKey) }, null); 1013 try { 1014 if (c.moveToFirst()) { 1015 mailboxName = Utility.FolderProperties.getInstance(MessageList.this) 1016 .getDisplayName(c.getInt(MAILBOX_NAME_COLUMN_TYPE)); 1017 if (mailboxName == null) { 1018 mailboxName = c.getString(MAILBOX_NAME_COLUMN_ID); 1019 } 1020 accountKey = c.getString(MAILBOX_NAME_COLUMN_ACCOUNT_KEY); 1021 } 1022 } finally { 1023 c.close(); 1024 } 1025 if (accountKey != null) { 1026 c = MessageList.this.mResolver.query(Account.CONTENT_URI, 1027 ACCOUNT_NAME_PROJECTION, ID_SELECTION, new String[] { accountKey }, 1028 null); 1029 try { 1030 if (c.moveToFirst()) { 1031 accountName = c.getString(ACCOUNT_DISPLAY_NAME_COLUMN_ID); 1032 } 1033 } finally { 1034 c.close(); 1035 } 1036 } 1037 return new String[] {accountName, mailboxName}; 1038 } 1039 1040 @Override 1041 protected void onPostExecute(String[] names) { 1042 Log.d("MessageList", "ACCOUNT:" + names[0] + "MAILBOX" + names[1]); 1043 if (names[0] != null) { 1044 mRightTitle.setText(names[0]); 1045 } 1046 if (names[1] != null) { 1047 mLeftTitle.setText(names[1]); 1048 } 1049 } 1050 } 1051 1052 /** 1053 * Handler for UI-thread operations (when called from callbacks or any other threads) 1054 */ 1055 class MessageListHandler extends Handler { 1056 private static final int MSG_PROGRESS = 1; 1057 private static final int MSG_LOOKUP_MAILBOX_TYPE = 2; 1058 1059 @Override 1060 public void handleMessage(android.os.Message msg) { 1061 switch (msg.what) { 1062 case MSG_PROGRESS: 1063 boolean visible = (msg.arg1 != 0); 1064 if (visible) { 1065 mProgressIcon.setVisibility(View.VISIBLE); 1066 } else { 1067 mProgressIcon.setVisibility(View.GONE); 1068 } 1069 if (mListFooterProgress != null) { 1070 mListFooterProgress.setVisibility(visible ? View.VISIBLE : View.GONE); 1071 } 1072 setListFooterText(visible); 1073 break; 1074 case MSG_LOOKUP_MAILBOX_TYPE: 1075 // kill running async task, if any 1076 if (mFindMailboxTask != null && 1077 mFindMailboxTask.getStatus() != FindMailboxTask.Status.FINISHED) { 1078 mFindMailboxTask.cancel(true); 1079 mFindMailboxTask = null; 1080 } 1081 // start new one. do not recurse back to controller. 1082 long accountId = ((Long)msg.obj).longValue(); 1083 int mailboxType = msg.arg1; 1084 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false); 1085 mFindMailboxTask.execute(); 1086 break; 1087 default: 1088 super.handleMessage(msg); 1089 } 1090 } 1091 1092 /** 1093 * Call from any thread to start/stop progress indicator(s) 1094 * @param progress true to start, false to stop 1095 */ 1096 public void progress(boolean progress) { 1097 android.os.Message msg = android.os.Message.obtain(); 1098 msg.what = MSG_PROGRESS; 1099 msg.arg1 = progress ? 1 : 0; 1100 sendMessage(msg); 1101 } 1102 1103 /** 1104 * Called from any thread to look for a mailbox of a specific type. This is designed 1105 * to be called from the Controller's MailboxList callback; It instructs the async task 1106 * not to recurse, in case the mailbox is not found after this. 1107 * 1108 * See FindMailboxTask for more notes on this handler. 1109 */ 1110 public void lookupMailboxType(long accountId, int mailboxType) { 1111 android.os.Message msg = android.os.Message.obtain(); 1112 msg.what = MSG_LOOKUP_MAILBOX_TYPE; 1113 msg.arg1 = mailboxType; 1114 msg.obj = Long.valueOf(accountId); 1115 sendMessage(msg); 1116 } 1117 } 1118 1119 /** 1120 * Callback for async Controller results. 1121 */ 1122 private class ControllerResults implements Controller.Result { 1123 1124 // These are preset for use by updateMailboxListCallback 1125 int mWaitForMailboxType = -1; 1126 1127 // TODO report errors into UI 1128 // TODO check accountKey and only react to relevant notifications 1129 public void updateMailboxListCallback(MessagingException result, long accountKey, 1130 int progress) { 1131 updateProgress(result, progress); 1132 if (progress == 100) { 1133 mHandler.lookupMailboxType(accountKey, mWaitForMailboxType); 1134 } 1135 } 1136 1137 // TODO report errors into UI 1138 // TODO check accountKey and only react to relevant notifications 1139 public void updateMailboxCallback(MessagingException result, long accountKey, 1140 long mailboxKey, int progress, int numNewMessages) { 1141 if (result != null || progress == 100) { 1142 Email.updateMailboxRefreshTime(mMailboxId); 1143 } 1144 updateProgress(result, progress); 1145 } 1146 1147 public void loadMessageForViewCallback(MessagingException result, long messageId, 1148 int progress) { 1149 } 1150 1151 public void loadAttachmentCallback(MessagingException result, long messageId, 1152 long attachmentId, int progress) { 1153 } 1154 1155 public void serviceCheckMailCallback(MessagingException result, long accountId, 1156 long mailboxId, int progress, long tag) { 1157 } 1158 1159 // TODO report errors into UI 1160 public void sendMailCallback(MessagingException result, long accountId, long messageId, 1161 int progress) { 1162 if (mListFooterMode == LIST_FOOTER_MODE_SEND) { 1163 updateProgress(result, progress); 1164 } 1165 } 1166 1167 private void updateProgress(MessagingException result, int progress) { 1168 if (result != null || progress == 100) { 1169 mHandler.progress(false); 1170 } else if (progress == 0) { 1171 mHandler.progress(true); 1172 } 1173 } 1174 } 1175 1176 /** 1177 * This class implements the adapter for displaying messages based on cursors. 1178 */ 1179 /* package */ class MessageListAdapter extends CursorAdapter { 1180 1181 public static final int COLUMN_ID = 0; 1182 public static final int COLUMN_MAILBOX_KEY = 1; 1183 public static final int COLUMN_ACCOUNT_KEY = 2; 1184 public static final int COLUMN_DISPLAY_NAME = 3; 1185 public static final int COLUMN_SUBJECT = 4; 1186 public static final int COLUMN_DATE = 5; 1187 public static final int COLUMN_READ = 6; 1188 public static final int COLUMN_FAVORITE = 7; 1189 public static final int COLUMN_ATTACHMENTS = 8; 1190 1191 public final String[] PROJECTION = new String[] { 1192 EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, 1193 MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP, 1194 MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT, 1195 }; 1196 1197 Context mContext; 1198 private LayoutInflater mInflater; 1199 private Drawable mAttachmentIcon; 1200 private Drawable mFavoriteIconOn; 1201 private Drawable mFavoriteIconOff; 1202 private Drawable mSelectedIconOn; 1203 private Drawable mSelectedIconOff; 1204 1205 private java.text.DateFormat mDateFormat; 1206 private java.text.DateFormat mDayFormat; 1207 private java.text.DateFormat mTimeFormat; 1208 1209 private HashSet<Long> mChecked = new HashSet<Long>(); 1210 1211 public MessageListAdapter(Context context) { 1212 super(context, null); 1213 mContext = context; 1214 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 1215 1216 Resources resources = context.getResources(); 1217 mAttachmentIcon = resources.getDrawable(R.drawable.ic_mms_attachment_small); 1218 mFavoriteIconOn = resources.getDrawable(android.R.drawable.star_on); 1219 mFavoriteIconOff = resources.getDrawable(android.R.drawable.star_off); 1220 mSelectedIconOn = resources.getDrawable(R.drawable.btn_check_buttonless_on); 1221 mSelectedIconOff = resources.getDrawable(R.drawable.btn_check_buttonless_off); 1222 1223 mDateFormat = android.text.format.DateFormat.getDateFormat(context); // short date 1224 mDayFormat = android.text.format.DateFormat.getDateFormat(context); // TODO: day 1225 mTimeFormat = android.text.format.DateFormat.getTimeFormat(context); // 12/24 time 1226 } 1227 1228 public Set<Long> getSelectedSet() { 1229 return mChecked; 1230 } 1231 1232 @Override 1233 public void bindView(View view, Context context, Cursor cursor) { 1234 // Reset the view (in case it was recycled) and prepare for binding 1235 MessageListItem itemView = (MessageListItem) view; 1236 itemView.bindViewInit(this, true); 1237 1238 // Load the public fields in the view (for later use) 1239 itemView.mMessageId = cursor.getLong(COLUMN_ID); 1240 itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY); 1241 itemView.mAccountId = cursor.getLong(COLUMN_ACCOUNT_KEY); 1242 itemView.mRead = cursor.getInt(COLUMN_READ) != 0; 1243 itemView.mFavorite = cursor.getInt(COLUMN_FAVORITE) != 0; 1244 itemView.mSelected = mChecked.contains(Long.valueOf(itemView.mMessageId)); 1245 1246 // Load the UI 1247 View chipView = view.findViewById(R.id.chip); 1248 int chipResId = mColorChipResIds[(int)itemView.mAccountId % mColorChipResIds.length]; 1249 chipView.setBackgroundResource(chipResId); 1250 1251 TextView fromView = (TextView) view.findViewById(R.id.from); 1252 String text = cursor.getString(COLUMN_DISPLAY_NAME); 1253 fromView.setText(text); 1254 1255 TextView subjectView = (TextView) view.findViewById(R.id.subject); 1256 text = cursor.getString(COLUMN_SUBJECT); 1257 subjectView.setText(text); 1258 1259 boolean hasAttachments = cursor.getInt(COLUMN_ATTACHMENTS) != 0; 1260 subjectView.setCompoundDrawablesWithIntrinsicBounds(null, null, 1261 hasAttachments ? mAttachmentIcon : null, null); 1262 1263 // TODO ui spec suggests "time", "day", "date" - implement "day" 1264 TextView dateView = (TextView) view.findViewById(R.id.date); 1265 long timestamp = cursor.getLong(COLUMN_DATE); 1266 Date date = new Date(timestamp); 1267 if (Utility.isDateToday(date)) { 1268 text = mTimeFormat.format(date); 1269 } else { 1270 text = mDateFormat.format(date); 1271 } 1272 dateView.setText(text); 1273 1274 if (itemView.mRead) { 1275 subjectView.setTypeface(Typeface.DEFAULT); 1276 fromView.setTypeface(Typeface.DEFAULT); 1277 view.setBackgroundDrawable(context.getResources().getDrawable( 1278 R.color.message_list_item_background_read)); 1279 } else { 1280 subjectView.setTypeface(Typeface.DEFAULT_BOLD); 1281 fromView.setTypeface(Typeface.DEFAULT_BOLD); 1282 view.setBackgroundDrawable(context.getResources().getDrawable( 1283 R.color.message_list_item_background_unread)); 1284 } 1285 1286 ImageView selectedView = (ImageView) view.findViewById(R.id.selected); 1287 selectedView.setImageDrawable(itemView.mSelected ? mSelectedIconOn : mSelectedIconOff); 1288 1289 ImageView favoriteView = (ImageView) view.findViewById(R.id.favorite); 1290 favoriteView.setImageDrawable(itemView.mFavorite ? mFavoriteIconOn : mFavoriteIconOff); 1291 } 1292 1293 @Override 1294 public View newView(Context context, Cursor cursor, ViewGroup parent) { 1295 return mInflater.inflate(R.layout.message_list_item, parent, false); 1296 } 1297 1298 /** 1299 * This is used as a callback from the list items, to set the selected state 1300 * 1301 * @param itemView the item being changed 1302 * @param newSelected the new value of the selected flag (checkbox state) 1303 */ 1304 public void updateSelected(MessageListItem itemView, boolean newSelected) { 1305 ImageView selectedView = (ImageView) itemView.findViewById(R.id.selected); 1306 selectedView.setImageDrawable(newSelected ? mSelectedIconOn : mSelectedIconOff); 1307 1308 // Set checkbox state in list, and show/hide panel if necessary 1309 Long id = Long.valueOf(itemView.mMessageId); 1310 if (newSelected) { 1311 mChecked.add(id); 1312 } else { 1313 mChecked.remove(id); 1314 } 1315 1316 MessageList.this.showMultiPanel(mChecked.size() > 0); 1317 } 1318 1319 /** 1320 * This is used as a callback from the list items, to set the favorite state 1321 * 1322 * @param itemView the item being changed 1323 * @param newFavorite the new value of the favorite flag (star state) 1324 */ 1325 public void updateFavorite(MessageListItem itemView, boolean newFavorite) { 1326 ImageView favoriteView = (ImageView) itemView.findViewById(R.id.favorite); 1327 favoriteView.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff); 1328 onSetMessageFavorite(itemView.mMessageId, newFavorite); 1329 } 1330 } 1331} 1332