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