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