MessageList.java revision ea69fc40d75f5fcf38872f836cdea9fb3aa9c991
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.R; 21import com.android.email.Utility; 22import com.android.email.activity.setup.AccountSettings; 23import com.android.email.mail.MessagingException; 24import com.android.email.provider.EmailContent; 25import com.android.email.provider.EmailContent.Account; 26import com.android.email.provider.EmailContent.Mailbox; 27import com.android.email.provider.EmailContent.MailboxColumns; 28import com.android.email.provider.EmailContent.Message; 29import com.android.email.provider.EmailContent.MessageColumns; 30 31import android.app.ListActivity; 32import android.content.ContentUris; 33import android.content.Context; 34import android.content.Intent; 35import android.content.res.Resources; 36import android.database.Cursor; 37import android.graphics.drawable.Drawable; 38import android.net.Uri; 39import android.os.AsyncTask; 40import android.os.Bundle; 41import android.os.Handler; 42import android.view.ContextMenu; 43import android.view.LayoutInflater; 44import android.view.Menu; 45import android.view.MenuItem; 46import android.view.View; 47import android.view.ViewGroup; 48import android.view.Window; 49import android.view.ContextMenu.ContextMenuInfo; 50import android.view.View.OnClickListener; 51import android.view.animation.AnimationUtils; 52import android.widget.AdapterView; 53import android.widget.CursorAdapter; 54import android.widget.ImageView; 55import android.widget.ListView; 56import android.widget.TextView; 57import android.widget.Toast; 58import android.widget.AdapterView.OnItemClickListener; 59 60import java.util.Date; 61import java.util.HashSet; 62import java.util.Set; 63 64public class MessageList extends ListActivity implements OnItemClickListener, OnClickListener { 65 66 // Magic mailbox ID's 67 // NOTE: This is a quick solution for merged mailboxes. I would rather implement this 68 // with a more generic way of packaging and sharing queries between activities 69 public static final long QUERY_ALL_INBOXES = -2; 70 public static final long QUERY_ALL_UNREAD = -3; 71 public static final long QUERY_ALL_FAVORITES = -4; 72 public static final long QUERY_ALL_DRAFTS = -5; 73 public static final long QUERY_ALL_OUTBOX = -6; 74 75 // Intent extras (internal to this activity) 76 private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID"; 77 private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE"; 78 private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID"; 79 private static final String EXTRA_ACCOUNT_NAME = "com.android.email.activity.ACCOUNT_NAME"; 80 private static final String EXTRA_MAILBOX_NAME = "com.android.email.activity.MAILBOX_NAME"; 81 82 // UI support 83 private ListView mListView; 84 private View mMultiSelectPanel; 85 private View mReadUnreadButton; 86 private View mFavoriteButton; 87 private View mDeleteButton; 88 private MessageListAdapter mListAdapter; 89 private MessageListHandler mHandler = new MessageListHandler(); 90 private ControllerResults mControllerCallback = new ControllerResults(); 91 92 private static final int[] mColorChipResIds = new int[] { 93 R.drawable.appointment_indicator_leftside_1, 94 R.drawable.appointment_indicator_leftside_2, 95 R.drawable.appointment_indicator_leftside_3, 96 R.drawable.appointment_indicator_leftside_4, 97 R.drawable.appointment_indicator_leftside_5, 98 R.drawable.appointment_indicator_leftside_6, 99 R.drawable.appointment_indicator_leftside_7, 100 R.drawable.appointment_indicator_leftside_8, 101 R.drawable.appointment_indicator_leftside_9, 102 R.drawable.appointment_indicator_leftside_10, 103 R.drawable.appointment_indicator_leftside_11, 104 R.drawable.appointment_indicator_leftside_12, 105 R.drawable.appointment_indicator_leftside_13, 106 R.drawable.appointment_indicator_leftside_14, 107 R.drawable.appointment_indicator_leftside_15, 108 R.drawable.appointment_indicator_leftside_16, 109 R.drawable.appointment_indicator_leftside_17, 110 R.drawable.appointment_indicator_leftside_18, 111 R.drawable.appointment_indicator_leftside_19, 112 R.drawable.appointment_indicator_leftside_20, 113 R.drawable.appointment_indicator_leftside_21, 114 }; 115 116 // DB access 117 private long mMailboxId; 118 private LoadMessagesTask mLoadMessagesTask; 119 private FindMailboxTask mFindMailboxTask; 120 121 /** 122 * Reduced mailbox projection used to hunt for inboxes 123 * TODO: remove this and implement a custom URI 124 */ 125 public final static int MAILBOX_FIND_INBOX_COLUMN_ID = 0; 126 127 public final static String[] MAILBOX_FIND_INBOX_PROJECTION = new String[] { 128 EmailContent.RECORD_ID, MailboxColumns.TYPE, MailboxColumns.FLAG_VISIBLE 129 }; 130 131 /** 132 * Open a specific mailbox. 133 * 134 * TODO This should just shortcut to a more generic version that can accept a list of 135 * accounts/mailboxes (e.g. merged inboxes). 136 * 137 * @param context 138 * @param id mailbox key 139 * @param accountName the account we're viewing (for title formatting - not for lookup) 140 * @param mailboxName the mailbox we're viewing (for title formatting - not for lookup) 141 */ 142 public static void actionHandleAccount(Context context, long id, 143 String accountName, String mailboxName) { 144 Intent intent = new Intent(context, MessageList.class); 145 intent.putExtra(EXTRA_MAILBOX_ID, id); 146 intent.putExtra(EXTRA_ACCOUNT_NAME, accountName); 147 intent.putExtra(EXTRA_MAILBOX_NAME, mailboxName); 148 context.startActivity(intent); 149 } 150 151 /** 152 * Open a specific mailbox by account & type 153 * 154 * @param context The caller's context (for generating an intent) 155 * @param accountId The account to open 156 * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX) 157 */ 158 public static void actionHandleAccount(Context context, long accountId, int mailboxType) { 159 Intent intent = new Intent(context, MessageList.class); 160 intent.putExtra(EXTRA_ACCOUNT_ID, accountId); 161 intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType); 162 context.startActivity(intent); 163 } 164 165 /** 166 * Return an intent to open a specific mailbox by account & type. It will also clear 167 * notifications. 168 * 169 * @param context The caller's context (for generating an intent) 170 * @param accountId The account to open 171 * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX) 172 */ 173 public static Intent actionHandleAccountIntent(Context context, long accountId, 174 int mailboxType) { 175 Intent intent = new Intent(context, MessageList.class); 176 intent.putExtra(EXTRA_ACCOUNT_ID, accountId); 177 intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType); 178 return intent; 179 } 180 181 /** 182 * Used for generating lightweight (Uri-only) intents. 183 * 184 * @param context Calling context for building the intent 185 * @param accountId The account of interest 186 * @param mailboxType The folder name to open (typically Mailbox.TYPE_INBOX) 187 * @return an Intent which can be used to view that account 188 */ 189 public static Intent actionHandleAccountUriIntent(Context context, long accountId, 190 int mailboxType) { 191 Intent i = actionHandleAccountIntent(context, accountId, mailboxType); 192 i.removeExtra(EXTRA_ACCOUNT_ID); 193 Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 194 i.setData(uri); 195 return i; 196 } 197 198 @Override 199 public void onCreate(Bundle icicle) { 200 super.onCreate(icicle); 201 202 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 203 204 setContentView(R.layout.message_list); 205 mListView = getListView(); 206 mMultiSelectPanel = findViewById(R.id.footer_organize); 207 mReadUnreadButton = findViewById(R.id.btn_read_unread); 208 mFavoriteButton = findViewById(R.id.btn_multi_favorite); 209 mDeleteButton = findViewById(R.id.btn_multi_delete); 210 211 mReadUnreadButton.setOnClickListener(this); 212 mFavoriteButton.setOnClickListener(this); 213 mDeleteButton.setOnClickListener(this); 214 215 mListView.setOnItemClickListener(this); 216 mListView.setItemsCanFocus(false); 217 registerForContextMenu(mListView); 218 219 mListAdapter = new MessageListAdapter(this); 220 setListAdapter(mListAdapter); 221 222 // TODO extend this to properly deal with multiple mailboxes, cursor, etc. 223 224 // Select 'by id' or 'by type' or 'by uri' mode and launch appropriate queries 225 226 mMailboxId = getIntent().getLongExtra(EXTRA_MAILBOX_ID, -1); 227 if (mMailboxId != -1) { 228 // Specific mailbox ID was provided - go directly to it 229 mLoadMessagesTask = new LoadMessagesTask(mMailboxId); 230 mLoadMessagesTask.execute(); 231 } else { 232 long accountId = -1; 233 int mailboxType = getIntent().getIntExtra(EXTRA_MAILBOX_TYPE, Mailbox.TYPE_INBOX); 234 Uri uri = getIntent().getData(); 235 if (uri != null 236 && "content".equals(uri.getScheme()) 237 && EmailContent.AUTHORITY.equals(uri.getAuthority())) { 238 // A content URI was provided - try to look up the account 239 String accountIdString = uri.getPathSegments().get(1); 240 if (accountIdString != null) { 241 accountId = Long.parseLong(accountIdString); 242 } 243 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false); 244 mFindMailboxTask.execute(); 245 } else { 246 // Go by account id + type 247 accountId = getIntent().getLongExtra(EXTRA_ACCOUNT_ID, -1); 248 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, true); 249 mFindMailboxTask.execute(); 250 } 251 } 252 253 // TODO set title to "account > mailbox (#unread)" 254 } 255 256 @Override 257 public void onPause() { 258 super.onPause(); 259 Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback); 260 } 261 262 @Override 263 public void onResume() { 264 super.onResume(); 265 Controller.getInstance(getApplication()).addResultCallback(mControllerCallback); 266 267 // TODO: may need to clear notifications here 268 } 269 270 @Override 271 protected void onDestroy() { 272 super.onDestroy(); 273 274 if (mLoadMessagesTask != null && 275 mLoadMessagesTask.getStatus() != LoadMessagesTask.Status.FINISHED) { 276 mLoadMessagesTask.cancel(true); 277 mLoadMessagesTask = null; 278 } 279 if (mFindMailboxTask != null && 280 mFindMailboxTask.getStatus() != FindMailboxTask.Status.FINISHED) { 281 mFindMailboxTask.cancel(true); 282 mFindMailboxTask = null; 283 } 284 } 285 286 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 287 MessageListItem itemView = (MessageListItem) view; 288 onOpenMessage(id, itemView.mMailboxId); 289 } 290 291 public void onClick(View v) { 292 switch (v.getId()) { 293 case R.id.btn_read_unread: 294 onMultiToggleRead(mListAdapter.getSelectedSet()); 295 break; 296 case R.id.btn_multi_favorite: 297 onMultiToggleFavorite(mListAdapter.getSelectedSet()); 298 break; 299 case R.id.btn_multi_delete: 300 onMultiDelete(mListAdapter.getSelectedSet()); 301 break; 302 } 303 } 304 305 @Override 306 public boolean onCreateOptionsMenu(Menu menu) { 307 super.onCreateOptionsMenu(menu); 308 getMenuInflater().inflate(R.menu.message_list_option, menu); 309 return true; 310 } 311 312 @Override 313 public boolean onOptionsItemSelected(MenuItem item) { 314 switch (item.getItemId()) { 315 case R.id.refresh: 316 onRefresh(); 317 return true; 318 case R.id.accounts: 319 onAccounts(); 320 return true; 321 case R.id.compose: 322 onCompose(); 323 return true; 324 case R.id.account_settings: 325 onEditAccount(); 326 return true; 327 default: 328 return super.onOptionsItemSelected(item); 329 } 330 } 331 332 @Override 333 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 334 super.onCreateContextMenu(menu, v, menuInfo); 335 AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; 336 MessageListItem itemView = (MessageListItem) info.targetView; 337 338 // TODO: There is no context menu for the outbox 339 // TODO: There is probably a special context menu for the trash 340 341 getMenuInflater().inflate(R.menu.message_list_context, menu); 342 343 // The default menu contains "mark as read". If the message is read, change 344 // the menu text to "mark as unread." 345 if (itemView.mRead) { 346 menu.findItem(R.id.mark_as_read).setTitle(R.string.mark_as_unread_action); 347 } 348 } 349 350 @Override 351 public boolean onContextItemSelected(MenuItem item) { 352 AdapterView.AdapterContextMenuInfo info = 353 (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 354 MessageListItem itemView = (MessageListItem) info.targetView; 355 356 switch (item.getItemId()) { 357 case R.id.open: 358 onOpenMessage(info.id, itemView.mMailboxId); 359 break; 360 case R.id.delete: 361 onDelete(info.id, itemView.mAccountId); 362 break; 363 case R.id.reply: 364 //onReply(holder); 365 break; 366 case R.id.reply_all: 367 //onReplyAll(holder); 368 break; 369 case R.id.forward: 370 //onForward(holder); 371 break; 372 case R.id.mark_as_read: 373 onSetMessageRead(info.id, !itemView.mRead); 374 break; 375 } 376 return super.onContextItemSelected(item); 377 } 378 379 private void onRefresh() { 380 // TODO: This needs to loop through all open mailboxes (there might be more than one) 381 // TODO: Should not be reading from DB in UI thread 382 if (mMailboxId >= 0) { 383 EmailContent.Mailbox mailbox = 384 EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId); 385 EmailContent.Account account = 386 EmailContent.Account.restoreAccountWithId(this, mailbox.mAccountKey); 387 mHandler.progress(true); 388 Controller.getInstance(getApplication()).updateMailbox( 389 account, mailbox, mControllerCallback); 390 } 391 } 392 393 private void onAccounts() { 394 AccountFolderList.actionShowAccounts(this); 395 finish(); 396 } 397 398 private void onCompose() { 399 // TODO: Select correct account to send from when there are multiple mailboxes 400 // TODO: Should not be reading from DB in UI thread 401 if (mMailboxId >= 0) { 402 EmailContent.Mailbox mailbox = 403 EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId); 404 MessageCompose.actionCompose(this, mailbox.mAccountKey); 405 } 406 } 407 408 private void onEditAccount() { 409 // TODO: Select correct account to edit when there are multiple mailboxes 410 // TODO: Should not be reading from DB in UI thread 411 if (mMailboxId >= 0) { 412 EmailContent.Mailbox mailbox = 413 EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId); 414 AccountSettings.actionSettings(this, mailbox.mAccountKey); 415 } 416 } 417 418 public void onOpenMessage(long messageId, long mailboxId) { 419 // TODO: Should not be reading from DB in UI thread 420 EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId); 421 422 if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) { 423 MessageCompose.actionEditDraft(this, messageId); 424 } else { 425 MessageView.actionView(this, messageId); 426 } 427 } 428 429 private void onDelete(long messageId, long accountId) { 430 Controller.getInstance(getApplication()).deleteMessage(messageId, accountId); 431 Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show(); 432 } 433 434 private void onSetMessageRead(long messageId, boolean newRead) { 435 Controller.getInstance(getApplication()).setMessageRead(messageId, newRead); 436 } 437 438 private void onSetMessageFavorite(long messageId, boolean newFavorite) { 439 Controller.getInstance(getApplication()).setMessageFavorite(messageId, newFavorite); 440 } 441 442 /** 443 * Toggles a set read/unread states. Note, the default behavior is "mark unread", so the 444 * sense of the helper methods is "true=unread". 445 * 446 * @param selectedSet The current list of selected items 447 */ 448 private void onMultiToggleRead(Set<Long> selectedSet) { 449 int numChanged = toggleMultiple(selectedSet, new MultiToggleHelper() { 450 451 public boolean getField(long messageId, Cursor c) { 452 return c.getInt(MessageListAdapter.COLUMN_READ) == 0; 453 } 454 455 public boolean setField(long messageId, Cursor c, boolean newValue) { 456 boolean oldValue = getField(messageId, c); 457 if (oldValue != newValue) { 458 onSetMessageRead(messageId, !newValue); 459 return true; 460 } 461 return false; 462 } 463 }); 464 } 465 466 /** 467 * Toggles a set of favorites (stars) 468 * 469 * @param selectedSet The current list of selected items 470 */ 471 private void onMultiToggleFavorite(Set<Long> selectedSet) { 472 int numChanged = toggleMultiple(selectedSet, new MultiToggleHelper() { 473 474 public boolean getField(long messageId, Cursor c) { 475 return c.getInt(MessageListAdapter.COLUMN_FAVORITE) != 0; 476 } 477 478 public boolean setField(long messageId, Cursor c, boolean newValue) { 479 boolean oldValue = getField(messageId, c); 480 if (oldValue != newValue) { 481 onSetMessageFavorite(messageId, newValue); 482 return true; 483 } 484 return false; 485 } 486 }); 487 } 488 489 private void onMultiDelete(Set<Long> selectedSet) { 490 // Clone the set, because deleting is going to thrash things 491 HashSet<Long> cloneSet = new HashSet<Long>(selectedSet); 492 for (Long id : cloneSet) { 493 Controller.getInstance(getApplication()).deleteMessage(id, -1); 494 } 495 // TODO: count messages and show "n messages deleted" 496 Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show(); 497 selectedSet.clear(); 498 showMultiPanel(false); 499 } 500 501 private interface MultiToggleHelper { 502 /** 503 * Return true if the field of interest is "set". If one or more are false, then our 504 * bulk action will be to "set". If all are set, our bulk action will be to "clear". 505 * @param messageId the message id of the current message 506 * @param c the cursor, positioned to the item of interest 507 * @return true if the field at this row is "set" 508 */ 509 public boolean getField(long messageId, Cursor c); 510 511 /** 512 * Set or clear the field of interest. Return true if a change was made. 513 * @param messageId the message id of the current message 514 * @param c the cursor, positioned to the item of interest 515 * @param newValue the new value to be set at this row 516 * @return true if a change was actually made 517 */ 518 public boolean setField(long messageId, Cursor c, boolean newValue); 519 } 520 521 /** 522 * Toggle multiple fields in a message, using the following logic: If one or more fields 523 * are "clear", then "set" them. If all fields are "set", then "clear" them all. 524 * 525 * @param selectedSet the set of messages that are selected 526 * @param helper functions to implement the specific getter & setter 527 * @return the number of messages that were updated 528 */ 529 private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) { 530 Cursor c = mListAdapter.getCursor(); 531 boolean anyWereFound = false; 532 boolean allWereSet = true; 533 534 c.moveToPosition(-1); 535 while (c.moveToNext()) { 536 long id = c.getInt(MessageListAdapter.COLUMN_ID); 537 if (selectedSet.contains(Long.valueOf(id))) { 538 anyWereFound = true; 539 if (!helper.getField(id, c)) { 540 allWereSet = false; 541 break; 542 } 543 } 544 } 545 546 int numChanged = 0; 547 548 if (anyWereFound) { 549 boolean newValue = !allWereSet; 550 c.moveToPosition(-1); 551 while (c.moveToNext()) { 552 long id = c.getInt(MessageListAdapter.COLUMN_ID); 553 if (selectedSet.contains(Long.valueOf(id))) { 554 if (helper.setField(id, c, newValue)) { 555 ++numChanged; 556 } 557 } 558 } 559 } 560 561 return numChanged; 562 } 563 564 /** 565 * Show or hide the panel of multi-select options 566 */ 567 private void showMultiPanel(boolean show) { 568 if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) { 569 mMultiSelectPanel.setVisibility(View.VISIBLE); 570 mMultiSelectPanel.startAnimation( 571 AnimationUtils.loadAnimation(this, R.anim.footer_appear)); 572 573 } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) { 574 mMultiSelectPanel.setVisibility(View.GONE); 575 mMultiSelectPanel.startAnimation( 576 AnimationUtils.loadAnimation(this, R.anim.footer_disappear)); 577 } 578 } 579 580 /** 581 * Async task for finding a single mailbox by type (possibly even going to the network). 582 * 583 * This is much too complex, as implemented. It uses this AsyncTask to check for a mailbox, 584 * then (if not found) a Controller call to refresh mailboxes from the server, and a handler 585 * to relaunch this task (a 2nd time) to read the results of the network refresh. The core 586 * problem is that we have two different non-UI-thread jobs (reading DB and reading network) 587 * and two different paradigms for dealing with them. Some unification would be needed here 588 * to make this cleaner. 589 * 590 * TODO: If this problem spreads to other operations, find a cleaner way to handle it. 591 */ 592 private class FindMailboxTask extends AsyncTask<Void, Void, Long> { 593 594 private long mAccountId; 595 private int mMailboxType; 596 private boolean mOkToRecurse; 597 598 /** 599 * Special constructor to cache some local info 600 */ 601 public FindMailboxTask(long accountId, int mailboxType, boolean okToRecurse) { 602 mAccountId = accountId; 603 mMailboxType = mailboxType; 604 mOkToRecurse = okToRecurse; 605 } 606 607 @Override 608 protected Long doInBackground(Void... params) { 609 // See if we can find the requested mailbox in the DB. 610 long mailboxId = Mailbox.findMailboxOfType(MessageList.this, mAccountId, mMailboxType); 611 if (mailboxId == -1 && mOkToRecurse) { 612 // Not found - launch network lookup 613 EmailContent.Account account = 614 EmailContent.Account.restoreAccountWithId(MessageList.this, mAccountId); 615 mControllerCallback.mWaitForMailboxType = mMailboxType; 616 mHandler.progress(true); 617 Controller.getInstance(getApplication()).updateMailboxList( 618 account, mControllerCallback); 619 } 620 return mailboxId; 621 } 622 623 @Override 624 protected void onPostExecute(Long mailboxId) { 625 if (mailboxId != -1) { 626 mMailboxId = mailboxId; 627 mLoadMessagesTask = new LoadMessagesTask(mMailboxId); 628 mLoadMessagesTask.execute(); 629 } 630 } 631 } 632 633 /** 634 * Async task for loading a single folder out of the UI thread 635 * 636 * The code here (for merged boxes) is a placeholder/hack and should be replaced. Some 637 * specific notes: 638 * TODO: Move the double query into a specialized URI that returns all inbox messages 639 * and do the dirty work in raw SQL in the provider. 640 * TODO: Generalize the query generation so we can reuse it in MessageView (for next/prev) 641 */ 642 private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> { 643 644 private long mMailboxKey; 645 646 /** 647 * Special constructor to cache some local info 648 */ 649 public LoadMessagesTask(long mailboxKey) { 650 mMailboxKey = mailboxKey; 651 } 652 653 @Override 654 protected Cursor doInBackground(Void... params) { 655 // Setup default selection & args, then add to it as necessary 656 StringBuilder selection = new StringBuilder( 657 Message.FLAG_LOADED + "!=" + Message.NOT_LOADED + " AND "); 658 String[] selArgs = null; 659 660 if (mMailboxKey == QUERY_ALL_INBOXES || mMailboxKey == QUERY_ALL_DRAFTS || 661 mMailboxKey == QUERY_ALL_OUTBOX) { 662 // query for all mailboxes of type INBOX, DRAFTS, or OUTBOX 663 int type; 664 if (mMailboxKey == QUERY_ALL_INBOXES) { 665 type = Mailbox.TYPE_INBOX; 666 } else if (mMailboxKey == QUERY_ALL_DRAFTS) { 667 type = Mailbox.TYPE_DRAFTS; 668 } else { 669 type = Mailbox.TYPE_OUTBOX; 670 } 671 StringBuilder inboxes = new StringBuilder(); 672 Cursor c = MessageList.this.getContentResolver().query( 673 Mailbox.CONTENT_URI, 674 MAILBOX_FIND_INBOX_PROJECTION, 675 MailboxColumns.TYPE + "=? AND " + MailboxColumns.FLAG_VISIBLE + "=1", 676 new String[] { Integer.toString(type) }, null); 677 // build a long WHERE list 678 // TODO do this directly in the provider 679 while (c.moveToNext()) { 680 if (inboxes.length() != 0) { 681 inboxes.append(" OR "); 682 } 683 inboxes.append(MessageColumns.MAILBOX_KEY + "="); 684 inboxes.append(c.getLong(MAILBOX_FIND_INBOX_COLUMN_ID)); 685 } 686 c.close(); 687 // This is a hack - if there were no matching mailboxes, the empty selection string 688 // would match *all* messages. Instead, force a "non-matching" selection, which 689 // generates an empty Message cursor. 690 // TODO: handle this properly when we move the compound lookup into the provider 691 if (inboxes.length() == 0) { 692 inboxes.append(Message.RECORD_ID + "=-1"); 693 } 694 // make that the selection 695 selection.append(inboxes); 696 } else if (mMailboxKey == QUERY_ALL_UNREAD) { 697 selection.append(Message.FLAG_READ + "=0"); 698 } else if (mMailboxKey == QUERY_ALL_FAVORITES) { 699 selection.append(Message.FLAG_FAVORITE + "=1"); 700 } else { 701 selection.append(MessageColumns.MAILBOX_KEY + "=?"); 702 selArgs = new String[] { String.valueOf(mMailboxKey) }; 703 } 704 return MessageList.this.managedQuery( 705 EmailContent.Message.CONTENT_URI, 706 MessageList.this.mListAdapter.PROJECTION, 707 selection.toString(), selArgs, 708 EmailContent.MessageColumns.TIMESTAMP + " DESC"); 709 } 710 711 @Override 712 protected void onPostExecute(Cursor cursor) { 713 MessageList.this.mListAdapter.changeCursor(cursor); 714 715 // TODO: remove this hack and only update at the right time 716 if (cursor != null && cursor.getCount() == 0) { 717 onRefresh(); 718 } 719 } 720 } 721 722 /** 723 * Handler for UI-thread operations (when called from callbacks or any other threads) 724 */ 725 class MessageListHandler extends Handler { 726 private static final int MSG_PROGRESS = 1; 727 private static final int MSG_LOOKUP_MAILBOX_TYPE = 2; 728 729 @Override 730 public void handleMessage(android.os.Message msg) { 731 switch (msg.what) { 732 case MSG_PROGRESS: 733 setProgressBarIndeterminateVisibility(msg.arg1 != 0); 734 break; 735 case MSG_LOOKUP_MAILBOX_TYPE: 736 // kill running async task, if any 737 if (mFindMailboxTask != null && 738 mFindMailboxTask.getStatus() != FindMailboxTask.Status.FINISHED) { 739 mFindMailboxTask.cancel(true); 740 mFindMailboxTask = null; 741 } 742 // start new one. do not recurse back to controller. 743 long accountId = ((Long)msg.obj).longValue(); 744 int mailboxType = msg.arg1; 745 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false); 746 mFindMailboxTask.execute(); 747 break; 748 default: 749 super.handleMessage(msg); 750 } 751 } 752 753 /** 754 * Call from any thread to start/stop progress indicator(s) 755 * @param progress true to start, false to stop 756 */ 757 public void progress(boolean progress) { 758 android.os.Message msg = android.os.Message.obtain(); 759 msg.what = MSG_PROGRESS; 760 msg.arg1 = progress ? 1 : 0; 761 sendMessage(msg); 762 } 763 764 /** 765 * Called from any thread to look for a mailbox of a specific type. This is designed 766 * to be called from the Controller's MailboxList callback; It instructs the async task 767 * not to recurse, in case the mailbox is not found after this. 768 * 769 * See FindMailboxTask for more notes on this handler. 770 */ 771 public void lookupMailboxType(long accountId, int mailboxType) { 772 android.os.Message msg = android.os.Message.obtain(); 773 msg.what = MSG_LOOKUP_MAILBOX_TYPE; 774 msg.arg1 = mailboxType; 775 msg.obj = Long.valueOf(accountId); 776 sendMessage(msg); 777 } 778 } 779 780 /** 781 * Callback for async Controller results. This is all a placeholder until we figure out the 782 * final way to do this. 783 */ 784 private class ControllerResults implements Controller.Result { 785 786 // These are preset for use by updateMailboxListCallback 787 int mWaitForMailboxType = -1; 788 789 public void updateMailboxListCallback(MessagingException result, long accountKey) { 790 if (mWaitForMailboxType != -1) { 791 mHandler.progress(false); 792 if (result == null) { 793 mHandler.lookupMailboxType(accountKey, mWaitForMailboxType); 794 } 795 } 796 } 797 798 public void updateMailboxCallback(MessagingException result, long accountKey, 799 long mailboxKey, int totalMessagesInMailbox, int numNewMessages) { 800 mHandler.progress(false); 801 } 802 803 public void loadAttachmentCallback(MessagingException result, long messageId, 804 long attachmentId, int progress) { 805 } 806 } 807 808 /** 809 * This class implements the adapter for displaying messages based on cursors. 810 */ 811 /* package */ class MessageListAdapter extends CursorAdapter { 812 813 public static final int COLUMN_ID = 0; 814 public static final int COLUMN_MAILBOX_KEY = 1; 815 public static final int COLUMN_ACCOUNT_KEY = 2; 816 public static final int COLUMN_DISPLAY_NAME = 3; 817 public static final int COLUMN_SUBJECT = 4; 818 public static final int COLUMN_DATE = 5; 819 public static final int COLUMN_READ = 6; 820 public static final int COLUMN_FAVORITE = 7; 821 public static final int COLUMN_ATTACHMENTS = 8; 822 823 public final String[] PROJECTION = new String[] { 824 EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, 825 MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP, 826 MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT, 827 }; 828 829 Context mContext; 830 private LayoutInflater mInflater; 831 private Drawable mAttachmentIcon; 832 private Drawable mFavoriteIconOn; 833 private Drawable mFavoriteIconOff; 834 private Drawable mSelectedIconOn; 835 private Drawable mSelectedIconOff; 836 837 private java.text.DateFormat mDateFormat; 838 private java.text.DateFormat mDayFormat; 839 private java.text.DateFormat mTimeFormat; 840 841 private HashSet<Long> mChecked = new HashSet<Long>(); 842 843 public MessageListAdapter(Context context) { 844 super(context, null); 845 mContext = context; 846 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 847 848 Resources resources = context.getResources(); 849 mAttachmentIcon = resources.getDrawable(R.drawable.ic_mms_attachment_small); 850 mFavoriteIconOn = resources.getDrawable(android.R.drawable.star_on); 851 mFavoriteIconOff = resources.getDrawable(android.R.drawable.star_off); 852 mSelectedIconOn = resources.getDrawable(R.drawable.btn_check_buttonless_on); 853 mSelectedIconOff = resources.getDrawable(R.drawable.btn_check_buttonless_off); 854 855 mDateFormat = android.text.format.DateFormat.getDateFormat(context); // short date 856 mDayFormat = android.text.format.DateFormat.getDateFormat(context); // TODO: day 857 mTimeFormat = android.text.format.DateFormat.getTimeFormat(context); // 12/24 time 858 } 859 860 public Set<Long> getSelectedSet() { 861 return mChecked; 862 } 863 864 @Override 865 public void bindView(View view, Context context, Cursor cursor) { 866 // Reset the view (in case it was recycled) and prepare for binding 867 MessageListItem itemView = (MessageListItem) view; 868 itemView.bindViewInit(this, true); 869 870 // Load the public fields in the view (for later use) 871 itemView.mMessageId = cursor.getLong(COLUMN_ID); 872 itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY); 873 itemView.mAccountId = cursor.getLong(COLUMN_ACCOUNT_KEY); 874 itemView.mRead = cursor.getInt(COLUMN_READ) != 0; 875 itemView.mFavorite = cursor.getInt(COLUMN_FAVORITE) != 0; 876 itemView.mSelected = mChecked.contains(Long.valueOf(itemView.mMessageId)); 877 878 // Load the UI 879 View chipView = view.findViewById(R.id.chip); 880 int chipResId = mColorChipResIds[(int)itemView.mAccountId % mColorChipResIds.length]; 881 chipView.setBackgroundResource(chipResId); 882 // TODO always display chip. Use other indications (e.g. boldface) for read/unread 883 chipView.getBackground().setAlpha(itemView.mRead ? 100 : 255); 884 885 TextView fromView = (TextView) view.findViewById(R.id.from); 886 String text = cursor.getString(COLUMN_DISPLAY_NAME); 887 if (text != null) fromView.setText(text); 888 889 boolean hasAttachments = cursor.getInt(COLUMN_ATTACHMENTS) != 0; 890 fromView.setCompoundDrawablesWithIntrinsicBounds(null, null, 891 hasAttachments ? mAttachmentIcon : null, null); 892 893 TextView subjectView = (TextView) view.findViewById(R.id.subject); 894 text = cursor.getString(COLUMN_SUBJECT); 895 if (text != null) subjectView.setText(text); 896 897 // TODO ui spec suggests "time", "day", "date" - implement "day" 898 TextView dateView = (TextView) view.findViewById(R.id.date); 899 long timestamp = cursor.getLong(COLUMN_DATE); 900 Date date = new Date(timestamp); 901 if (Utility.isDateToday(date)) { 902 text = mTimeFormat.format(date); 903 } else { 904 text = mDateFormat.format(date); 905 } 906 dateView.setText(text); 907 908 ImageView selectedView = (ImageView) view.findViewById(R.id.selected); 909 selectedView.setImageDrawable(itemView.mSelected ? mSelectedIconOn : mSelectedIconOff); 910 911 ImageView favoriteView = (ImageView) view.findViewById(R.id.favorite); 912 favoriteView.setImageDrawable(itemView.mFavorite ? mFavoriteIconOn : mFavoriteIconOff); 913 } 914 915 @Override 916 public View newView(Context context, Cursor cursor, ViewGroup parent) { 917 return mInflater.inflate(R.layout.message_list_item, parent, false); 918 } 919 920 /** 921 * This is used as a callback from the list items, to set the selected state 922 * 923 * @param itemView the item being changed 924 * @param newSelected the new value of the selected flag (checkbox state) 925 */ 926 public void updateSelected(MessageListItem itemView, boolean newSelected) { 927 ImageView selectedView = (ImageView) itemView.findViewById(R.id.selected); 928 selectedView.setImageDrawable(newSelected ? mSelectedIconOn : mSelectedIconOff); 929 930 // Set checkbox state in list, and show/hide panel if necessary 931 Long id = Long.valueOf(itemView.mMessageId); 932 if (newSelected) { 933 mChecked.add(id); 934 } else { 935 mChecked.remove(id); 936 } 937 938 MessageList.this.showMultiPanel(mChecked.size() > 0); 939 } 940 941 /** 942 * This is used as a callback from the list items, to set the favorite state 943 * 944 * @param itemView the item being changed 945 * @param newFavorite the new value of the favorite flag (star state) 946 */ 947 public void updateFavorite(MessageListItem itemView, boolean newFavorite) { 948 ImageView favoriteView = (ImageView) itemView.findViewById(R.id.favorite); 949 favoriteView.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff); 950 onSetMessageFavorite(itemView.mMessageId, newFavorite); 951 } 952 } 953} 954