MessageList.java revision a5af05f1a94ba9d0a91bd6507492cf6946f88a4d
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 // TODO: Should not be reading from DB in UI thread 341 EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(this, 342 itemView.mMailboxId); 343 344 switch (mailbox.mType) { 345 case EmailContent.Mailbox.TYPE_DRAFTS: 346 getMenuInflater().inflate(R.menu.message_list_context, menu); 347 break; 348 case EmailContent.Mailbox.TYPE_OUTBOX: 349 break; 350 default: 351 getMenuInflater().inflate(R.menu.message_list_context, menu); 352 getMenuInflater().inflate(R.menu.message_list_context_extra, menu); 353 // The default menu contains "mark as read". If the message is read, change 354 // the menu text to "mark as unread." 355 if (itemView.mRead) { 356 menu.findItem(R.id.mark_as_read).setTitle(R.string.mark_as_unread_action); 357 } 358 break; 359 } 360 } 361 362 @Override 363 public boolean onContextItemSelected(MenuItem item) { 364 AdapterView.AdapterContextMenuInfo info = 365 (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 366 MessageListItem itemView = (MessageListItem) info.targetView; 367 368 switch (item.getItemId()) { 369 case R.id.open: 370 onOpenMessage(info.id, itemView.mMailboxId); 371 break; 372 case R.id.delete: 373 onDelete(info.id, itemView.mAccountId); 374 break; 375 case R.id.reply: 376 //onReply(holder); 377 break; 378 case R.id.reply_all: 379 //onReplyAll(holder); 380 break; 381 case R.id.forward: 382 //onForward(holder); 383 break; 384 case R.id.mark_as_read: 385 onSetMessageRead(info.id, !itemView.mRead); 386 break; 387 } 388 return super.onContextItemSelected(item); 389 } 390 391 private void onRefresh() { 392 // TODO: This needs to loop through all open mailboxes (there might be more than one) 393 // TODO: Should not be reading from DB in UI thread 394 if (mMailboxId >= 0) { 395 EmailContent.Mailbox mailbox = 396 EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId); 397 Controller.getInstance(getApplication()).updateMailbox( 398 mailbox.mAccountKey, mailbox, mControllerCallback); 399 } 400 } 401 402 private void onAccounts() { 403 AccountFolderList.actionShowAccounts(this); 404 finish(); 405 } 406 407 private long lookupAccountIdFromMailboxId(long mailboxId) { 408 // TODO: Select correct account to send from when there are multiple mailboxes 409 // TODO: Should not be reading from DB in UI thread 410 if (mailboxId < 0) { 411 return -1; // no info, default account 412 } 413 EmailContent.Mailbox mailbox = 414 EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId); 415 return mailbox.mAccountKey; 416 } 417 418 private void onCompose() { 419 MessageCompose.actionCompose(this, lookupAccountIdFromMailboxId(mMailboxId)); 420 } 421 422 private void onEditAccount() { 423 AccountSettings.actionSettings(this, lookupAccountIdFromMailboxId(mMailboxId)); 424 } 425 426 public void onOpenMessage(long messageId, long mailboxId) { 427 // TODO: Should not be reading from DB in UI thread 428 EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId); 429 430 if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) { 431 MessageCompose.actionEditDraft(this, messageId); 432 } else { 433 MessageView.actionView(this, messageId); 434 } 435 } 436 437 private void onDelete(long messageId, long accountId) { 438 Controller.getInstance(getApplication()).deleteMessage(messageId, accountId); 439 Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show(); 440 } 441 442 private void onSetMessageRead(long messageId, boolean newRead) { 443 Controller.getInstance(getApplication()).setMessageRead(messageId, newRead); 444 } 445 446 private void onSetMessageFavorite(long messageId, boolean newFavorite) { 447 Controller.getInstance(getApplication()).setMessageFavorite(messageId, newFavorite); 448 } 449 450 /** 451 * Toggles a set read/unread states. Note, the default behavior is "mark unread", so the 452 * sense of the helper methods is "true=unread". 453 * 454 * @param selectedSet The current list of selected items 455 */ 456 private void onMultiToggleRead(Set<Long> selectedSet) { 457 int numChanged = toggleMultiple(selectedSet, new MultiToggleHelper() { 458 459 public boolean getField(long messageId, Cursor c) { 460 return c.getInt(MessageListAdapter.COLUMN_READ) == 0; 461 } 462 463 public boolean setField(long messageId, Cursor c, boolean newValue) { 464 boolean oldValue = getField(messageId, c); 465 if (oldValue != newValue) { 466 onSetMessageRead(messageId, !newValue); 467 return true; 468 } 469 return false; 470 } 471 }); 472 } 473 474 /** 475 * Toggles a set of favorites (stars) 476 * 477 * @param selectedSet The current list of selected items 478 */ 479 private void onMultiToggleFavorite(Set<Long> selectedSet) { 480 int numChanged = toggleMultiple(selectedSet, new MultiToggleHelper() { 481 482 public boolean getField(long messageId, Cursor c) { 483 return c.getInt(MessageListAdapter.COLUMN_FAVORITE) != 0; 484 } 485 486 public boolean setField(long messageId, Cursor c, boolean newValue) { 487 boolean oldValue = getField(messageId, c); 488 if (oldValue != newValue) { 489 onSetMessageFavorite(messageId, newValue); 490 return true; 491 } 492 return false; 493 } 494 }); 495 } 496 497 private void onMultiDelete(Set<Long> selectedSet) { 498 // Clone the set, because deleting is going to thrash things 499 HashSet<Long> cloneSet = new HashSet<Long>(selectedSet); 500 for (Long id : cloneSet) { 501 Controller.getInstance(getApplication()).deleteMessage(id, -1); 502 } 503 // TODO: count messages and show "n messages deleted" 504 Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show(); 505 selectedSet.clear(); 506 showMultiPanel(false); 507 } 508 509 private interface MultiToggleHelper { 510 /** 511 * Return true if the field of interest is "set". If one or more are false, then our 512 * bulk action will be to "set". If all are set, our bulk action will be to "clear". 513 * @param messageId the message id of the current message 514 * @param c the cursor, positioned to the item of interest 515 * @return true if the field at this row is "set" 516 */ 517 public boolean getField(long messageId, Cursor c); 518 519 /** 520 * Set or clear the field of interest. Return true if a change was made. 521 * @param messageId the message id of the current message 522 * @param c the cursor, positioned to the item of interest 523 * @param newValue the new value to be set at this row 524 * @return true if a change was actually made 525 */ 526 public boolean setField(long messageId, Cursor c, boolean newValue); 527 } 528 529 /** 530 * Toggle multiple fields in a message, using the following logic: If one or more fields 531 * are "clear", then "set" them. If all fields are "set", then "clear" them all. 532 * 533 * @param selectedSet the set of messages that are selected 534 * @param helper functions to implement the specific getter & setter 535 * @return the number of messages that were updated 536 */ 537 private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) { 538 Cursor c = mListAdapter.getCursor(); 539 boolean anyWereFound = false; 540 boolean allWereSet = true; 541 542 c.moveToPosition(-1); 543 while (c.moveToNext()) { 544 long id = c.getInt(MessageListAdapter.COLUMN_ID); 545 if (selectedSet.contains(Long.valueOf(id))) { 546 anyWereFound = true; 547 if (!helper.getField(id, c)) { 548 allWereSet = false; 549 break; 550 } 551 } 552 } 553 554 int numChanged = 0; 555 556 if (anyWereFound) { 557 boolean newValue = !allWereSet; 558 c.moveToPosition(-1); 559 while (c.moveToNext()) { 560 long id = c.getInt(MessageListAdapter.COLUMN_ID); 561 if (selectedSet.contains(Long.valueOf(id))) { 562 if (helper.setField(id, c, newValue)) { 563 ++numChanged; 564 } 565 } 566 } 567 } 568 569 return numChanged; 570 } 571 572 /** 573 * Show or hide the panel of multi-select options 574 */ 575 private void showMultiPanel(boolean show) { 576 if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) { 577 mMultiSelectPanel.setVisibility(View.VISIBLE); 578 mMultiSelectPanel.startAnimation( 579 AnimationUtils.loadAnimation(this, R.anim.footer_appear)); 580 581 } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) { 582 mMultiSelectPanel.setVisibility(View.GONE); 583 mMultiSelectPanel.startAnimation( 584 AnimationUtils.loadAnimation(this, R.anim.footer_disappear)); 585 } 586 } 587 588 /** 589 * Async task for finding a single mailbox by type (possibly even going to the network). 590 * 591 * This is much too complex, as implemented. It uses this AsyncTask to check for a mailbox, 592 * then (if not found) a Controller call to refresh mailboxes from the server, and a handler 593 * to relaunch this task (a 2nd time) to read the results of the network refresh. The core 594 * problem is that we have two different non-UI-thread jobs (reading DB and reading network) 595 * and two different paradigms for dealing with them. Some unification would be needed here 596 * to make this cleaner. 597 * 598 * TODO: If this problem spreads to other operations, find a cleaner way to handle it. 599 */ 600 private class FindMailboxTask extends AsyncTask<Void, Void, Long> { 601 602 private long mAccountId; 603 private int mMailboxType; 604 private boolean mOkToRecurse; 605 606 /** 607 * Special constructor to cache some local info 608 */ 609 public FindMailboxTask(long accountId, int mailboxType, boolean okToRecurse) { 610 mAccountId = accountId; 611 mMailboxType = mailboxType; 612 mOkToRecurse = okToRecurse; 613 } 614 615 @Override 616 protected Long doInBackground(Void... params) { 617 // See if we can find the requested mailbox in the DB. 618 long mailboxId = Mailbox.findMailboxOfType(MessageList.this, mAccountId, mMailboxType); 619 if (mailboxId == -1 && mOkToRecurse) { 620 // Not found - launch network lookup 621 mControllerCallback.mWaitForMailboxType = mMailboxType; 622 Controller.getInstance(getApplication()).updateMailboxList( 623 mAccountId, mControllerCallback); 624 } 625 return mailboxId; 626 } 627 628 @Override 629 protected void onPostExecute(Long mailboxId) { 630 if (mailboxId != -1) { 631 mMailboxId = mailboxId; 632 mLoadMessagesTask = new LoadMessagesTask(mMailboxId); 633 mLoadMessagesTask.execute(); 634 } 635 } 636 } 637 638 /** 639 * Async task for loading a single folder out of the UI thread 640 * 641 * The code here (for merged boxes) is a placeholder/hack and should be replaced. Some 642 * specific notes: 643 * TODO: Move the double query into a specialized URI that returns all inbox messages 644 * and do the dirty work in raw SQL in the provider. 645 * TODO: Generalize the query generation so we can reuse it in MessageView (for next/prev) 646 */ 647 private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> { 648 649 private long mMailboxKey; 650 651 /** 652 * Special constructor to cache some local info 653 */ 654 public LoadMessagesTask(long mailboxKey) { 655 mMailboxKey = mailboxKey; 656 } 657 658 @Override 659 protected Cursor doInBackground(Void... params) { 660 // Setup default selection & args, then add to it as necessary 661 StringBuilder selection = new StringBuilder( 662 Message.FLAG_LOADED + "!=" + Message.NOT_LOADED + " AND "); 663 String[] selArgs = null; 664 665 if (mMailboxKey == QUERY_ALL_INBOXES || mMailboxKey == QUERY_ALL_DRAFTS || 666 mMailboxKey == QUERY_ALL_OUTBOX) { 667 // query for all mailboxes of type INBOX, DRAFTS, or OUTBOX 668 int type; 669 if (mMailboxKey == QUERY_ALL_INBOXES) { 670 type = Mailbox.TYPE_INBOX; 671 } else if (mMailboxKey == QUERY_ALL_DRAFTS) { 672 type = Mailbox.TYPE_DRAFTS; 673 } else { 674 type = Mailbox.TYPE_OUTBOX; 675 } 676 StringBuilder inboxes = new StringBuilder(); 677 Cursor c = MessageList.this.getContentResolver().query( 678 Mailbox.CONTENT_URI, 679 MAILBOX_FIND_INBOX_PROJECTION, 680 MailboxColumns.TYPE + "=? AND " + MailboxColumns.FLAG_VISIBLE + "=1", 681 new String[] { Integer.toString(type) }, null); 682 // build a long WHERE list 683 // TODO do this directly in the provider 684 while (c.moveToNext()) { 685 if (inboxes.length() != 0) { 686 inboxes.append(" OR "); 687 } 688 inboxes.append(MessageColumns.MAILBOX_KEY + "="); 689 inboxes.append(c.getLong(MAILBOX_FIND_INBOX_COLUMN_ID)); 690 } 691 c.close(); 692 // This is a hack - if there were no matching mailboxes, the empty selection string 693 // would match *all* messages. Instead, force a "non-matching" selection, which 694 // generates an empty Message cursor. 695 // TODO: handle this properly when we move the compound lookup into the provider 696 if (inboxes.length() == 0) { 697 inboxes.append(Message.RECORD_ID + "=-1"); 698 } 699 // make that the selection 700 selection.append(inboxes); 701 } else if (mMailboxKey == QUERY_ALL_UNREAD) { 702 selection.append(Message.FLAG_READ + "=0"); 703 } else if (mMailboxKey == QUERY_ALL_FAVORITES) { 704 selection.append(Message.FLAG_FAVORITE + "=1"); 705 } else { 706 selection.append(MessageColumns.MAILBOX_KEY + "=?"); 707 selArgs = new String[] { String.valueOf(mMailboxKey) }; 708 } 709 return MessageList.this.managedQuery( 710 EmailContent.Message.CONTENT_URI, 711 MessageList.this.mListAdapter.PROJECTION, 712 selection.toString(), selArgs, 713 EmailContent.MessageColumns.TIMESTAMP + " DESC"); 714 } 715 716 @Override 717 protected void onPostExecute(Cursor cursor) { 718 MessageList.this.mListAdapter.changeCursor(cursor); 719 720 // TODO: remove this hack and only update at the right time 721 if (cursor != null && cursor.getCount() == 0) { 722 onRefresh(); 723 } 724 } 725 } 726 727 /** 728 * Handler for UI-thread operations (when called from callbacks or any other threads) 729 */ 730 class MessageListHandler extends Handler { 731 private static final int MSG_PROGRESS = 1; 732 private static final int MSG_LOOKUP_MAILBOX_TYPE = 2; 733 734 @Override 735 public void handleMessage(android.os.Message msg) { 736 switch (msg.what) { 737 case MSG_PROGRESS: 738 setProgressBarIndeterminateVisibility(msg.arg1 != 0); 739 break; 740 case MSG_LOOKUP_MAILBOX_TYPE: 741 // kill running async task, if any 742 if (mFindMailboxTask != null && 743 mFindMailboxTask.getStatus() != FindMailboxTask.Status.FINISHED) { 744 mFindMailboxTask.cancel(true); 745 mFindMailboxTask = null; 746 } 747 // start new one. do not recurse back to controller. 748 long accountId = ((Long)msg.obj).longValue(); 749 int mailboxType = msg.arg1; 750 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false); 751 mFindMailboxTask.execute(); 752 break; 753 default: 754 super.handleMessage(msg); 755 } 756 } 757 758 /** 759 * Call from any thread to start/stop progress indicator(s) 760 * @param progress true to start, false to stop 761 */ 762 public void progress(boolean progress) { 763 android.os.Message msg = android.os.Message.obtain(); 764 msg.what = MSG_PROGRESS; 765 msg.arg1 = progress ? 1 : 0; 766 sendMessage(msg); 767 } 768 769 /** 770 * Called from any thread to look for a mailbox of a specific type. This is designed 771 * to be called from the Controller's MailboxList callback; It instructs the async task 772 * not to recurse, in case the mailbox is not found after this. 773 * 774 * See FindMailboxTask for more notes on this handler. 775 */ 776 public void lookupMailboxType(long accountId, int mailboxType) { 777 android.os.Message msg = android.os.Message.obtain(); 778 msg.what = MSG_LOOKUP_MAILBOX_TYPE; 779 msg.arg1 = mailboxType; 780 msg.obj = Long.valueOf(accountId); 781 sendMessage(msg); 782 } 783 } 784 785 /** 786 * Callback for async Controller results. 787 */ 788 private class ControllerResults implements Controller.Result { 789 790 // These are preset for use by updateMailboxListCallback 791 int mWaitForMailboxType = -1; 792 793 // TODO report errors into UI 794 // TODO check accountKey and only react to relevant notifications 795 public void updateMailboxListCallback(MessagingException result, long accountKey, 796 int progress) { 797 if (progress == 0) { 798 mHandler.progress(true); 799 } 800 else if (result != null || progress == 100) { 801 mHandler.progress(false); 802 if (mWaitForMailboxType != -1) { 803 if (result == null) { 804 mHandler.lookupMailboxType(accountKey, mWaitForMailboxType); 805 } 806 } 807 } 808 } 809 810 // TODO report errors into UI 811 // TODO check accountKey and only react to relevant notifications 812 public void updateMailboxCallback(MessagingException result, long accountKey, 813 long mailboxKey, int progress, int totalMessagesInMailbox, int numNewMessages) { 814 if (progress == 0) { 815 mHandler.progress(true); 816 } 817 else if (result != null || progress == 100) { 818 mHandler.progress(false); 819 } 820 } 821 822 public void loadAttachmentCallback(MessagingException result, long messageId, 823 long attachmentId, int progress) { 824 } 825 } 826 827 /** 828 * This class implements the adapter for displaying messages based on cursors. 829 */ 830 /* package */ class MessageListAdapter extends CursorAdapter { 831 832 public static final int COLUMN_ID = 0; 833 public static final int COLUMN_MAILBOX_KEY = 1; 834 public static final int COLUMN_ACCOUNT_KEY = 2; 835 public static final int COLUMN_DISPLAY_NAME = 3; 836 public static final int COLUMN_SUBJECT = 4; 837 public static final int COLUMN_DATE = 5; 838 public static final int COLUMN_READ = 6; 839 public static final int COLUMN_FAVORITE = 7; 840 public static final int COLUMN_ATTACHMENTS = 8; 841 842 public final String[] PROJECTION = new String[] { 843 EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, 844 MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP, 845 MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT, 846 }; 847 848 Context mContext; 849 private LayoutInflater mInflater; 850 private Drawable mAttachmentIcon; 851 private Drawable mFavoriteIconOn; 852 private Drawable mFavoriteIconOff; 853 private Drawable mSelectedIconOn; 854 private Drawable mSelectedIconOff; 855 856 private java.text.DateFormat mDateFormat; 857 private java.text.DateFormat mDayFormat; 858 private java.text.DateFormat mTimeFormat; 859 860 private HashSet<Long> mChecked = new HashSet<Long>(); 861 862 public MessageListAdapter(Context context) { 863 super(context, null); 864 mContext = context; 865 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 866 867 Resources resources = context.getResources(); 868 mAttachmentIcon = resources.getDrawable(R.drawable.ic_mms_attachment_small); 869 mFavoriteIconOn = resources.getDrawable(android.R.drawable.star_on); 870 mFavoriteIconOff = resources.getDrawable(android.R.drawable.star_off); 871 mSelectedIconOn = resources.getDrawable(R.drawable.btn_check_buttonless_on); 872 mSelectedIconOff = resources.getDrawable(R.drawable.btn_check_buttonless_off); 873 874 mDateFormat = android.text.format.DateFormat.getDateFormat(context); // short date 875 mDayFormat = android.text.format.DateFormat.getDateFormat(context); // TODO: day 876 mTimeFormat = android.text.format.DateFormat.getTimeFormat(context); // 12/24 time 877 } 878 879 public Set<Long> getSelectedSet() { 880 return mChecked; 881 } 882 883 @Override 884 public void bindView(View view, Context context, Cursor cursor) { 885 // Reset the view (in case it was recycled) and prepare for binding 886 MessageListItem itemView = (MessageListItem) view; 887 itemView.bindViewInit(this, true); 888 889 // Load the public fields in the view (for later use) 890 itemView.mMessageId = cursor.getLong(COLUMN_ID); 891 itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY); 892 itemView.mAccountId = cursor.getLong(COLUMN_ACCOUNT_KEY); 893 itemView.mRead = cursor.getInt(COLUMN_READ) != 0; 894 itemView.mFavorite = cursor.getInt(COLUMN_FAVORITE) != 0; 895 itemView.mSelected = mChecked.contains(Long.valueOf(itemView.mMessageId)); 896 897 // Load the UI 898 View chipView = view.findViewById(R.id.chip); 899 int chipResId = mColorChipResIds[(int)itemView.mAccountId % mColorChipResIds.length]; 900 chipView.setBackgroundResource(chipResId); 901 // TODO always display chip. Use other indications (e.g. boldface) for read/unread 902 chipView.getBackground().setAlpha(itemView.mRead ? 100 : 255); 903 904 TextView fromView = (TextView) view.findViewById(R.id.from); 905 String text = cursor.getString(COLUMN_DISPLAY_NAME); 906 if (text != null) fromView.setText(text); 907 908 boolean hasAttachments = cursor.getInt(COLUMN_ATTACHMENTS) != 0; 909 fromView.setCompoundDrawablesWithIntrinsicBounds(null, null, 910 hasAttachments ? mAttachmentIcon : null, null); 911 912 TextView subjectView = (TextView) view.findViewById(R.id.subject); 913 text = cursor.getString(COLUMN_SUBJECT); 914 if (text != null) subjectView.setText(text); 915 916 // TODO ui spec suggests "time", "day", "date" - implement "day" 917 TextView dateView = (TextView) view.findViewById(R.id.date); 918 long timestamp = cursor.getLong(COLUMN_DATE); 919 Date date = new Date(timestamp); 920 if (Utility.isDateToday(date)) { 921 text = mTimeFormat.format(date); 922 } else { 923 text = mDateFormat.format(date); 924 } 925 dateView.setText(text); 926 927 ImageView selectedView = (ImageView) view.findViewById(R.id.selected); 928 selectedView.setImageDrawable(itemView.mSelected ? mSelectedIconOn : mSelectedIconOff); 929 930 ImageView favoriteView = (ImageView) view.findViewById(R.id.favorite); 931 favoriteView.setImageDrawable(itemView.mFavorite ? mFavoriteIconOn : mFavoriteIconOff); 932 } 933 934 @Override 935 public View newView(Context context, Cursor cursor, ViewGroup parent) { 936 return mInflater.inflate(R.layout.message_list_item, parent, false); 937 } 938 939 /** 940 * This is used as a callback from the list items, to set the selected state 941 * 942 * @param itemView the item being changed 943 * @param newSelected the new value of the selected flag (checkbox state) 944 */ 945 public void updateSelected(MessageListItem itemView, boolean newSelected) { 946 ImageView selectedView = (ImageView) itemView.findViewById(R.id.selected); 947 selectedView.setImageDrawable(newSelected ? mSelectedIconOn : mSelectedIconOff); 948 949 // Set checkbox state in list, and show/hide panel if necessary 950 Long id = Long.valueOf(itemView.mMessageId); 951 if (newSelected) { 952 mChecked.add(id); 953 } else { 954 mChecked.remove(id); 955 } 956 957 MessageList.this.showMultiPanel(mChecked.size() > 0); 958 } 959 960 /** 961 * This is used as a callback from the list items, to set the favorite state 962 * 963 * @param itemView the item being changed 964 * @param newFavorite the new value of the favorite flag (star state) 965 */ 966 public void updateFavorite(MessageListItem itemView, boolean newFavorite) { 967 ImageView favoriteView = (ImageView) itemView.findViewById(R.id.favorite); 968 favoriteView.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff); 969 onSetMessageFavorite(itemView.mMessageId, newFavorite); 970 } 971 } 972} 973