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