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 17 package com.android.email.activity; 18 19 import com.android.email.Controller; 20 import com.android.email.Email; 21 import com.android.email.R; 22 import com.android.email.Utility; 23 import com.android.email.activity.setup.AccountSettings; 24 import com.android.email.mail.AuthenticationFailedException; 25 import com.android.email.mail.CertificateValidationException; 26 import com.android.email.mail.MessagingException; 27 import com.android.email.provider.EmailContent; 28 import com.android.email.provider.EmailContent.Account; 29 import com.android.email.provider.EmailContent.AccountColumns; 30 import com.android.email.provider.EmailContent.Mailbox; 31 import com.android.email.provider.EmailContent.MailboxColumns; 32 import com.android.email.provider.EmailContent.Message; 33 import com.android.email.provider.EmailContent.MessageColumns; 34 35 import android.app.ListActivity; 36 import android.content.ContentUris; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.database.Cursor; 40 import android.graphics.Typeface; 41 import android.net.Uri; 42 import android.os.AsyncTask; 43 import android.os.Bundle; 44 import android.os.Handler; 45 import android.view.ContextMenu; 46 import android.view.LayoutInflater; 47 import android.view.Menu; 48 import android.view.MenuItem; 49 import android.view.View; 50 import android.view.View.OnClickListener; 51 import android.view.ViewGroup; 52 import android.view.ContextMenu.ContextMenuInfo; 53 import android.view.animation.AnimationUtils; 54 import android.widget.AdapterView; 55 import android.widget.Button; 56 import android.widget.CursorAdapter; 57 import android.widget.ImageView; 58 import android.widget.ListView; 59 import android.widget.ProgressBar; 60 import android.widget.TextView; 61 import android.widget.AdapterView.OnItemClickListener; 62 63 public class MailboxList extends ListActivity implements OnItemClickListener, OnClickListener { 64 65 // Intent extras (internal to this activity) 66 private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID"; 67 68 private static final String MAILBOX_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?" 69 + " AND " + MailboxColumns.TYPE + "<" + Mailbox.TYPE_NOT_EMAIL 70 + " AND " + MailboxColumns.FLAG_VISIBLE + "=1"; 71 private static final String MESSAGE_MAILBOX_ID_SELECTION = 72 MessageColumns.MAILBOX_KEY + "=?"; 73 74 // UI support 75 private ListView mListView; 76 private ProgressBar mProgressIcon; 77 private TextView mErrorBanner; 78 79 private MailboxListAdapter mListAdapter; 80 private MailboxListHandler mHandler; 81 private ControllerResults mControllerCallback; 82 83 // DB access 84 private long mAccountId; 85 private LoadMailboxesTask mLoadMailboxesTask; 86 private AsyncTask<Void, Void, Object[]> mLoadAccountNameTask; 87 private MessageCountTask mMessageCountTask; 88 89 private long mDraftMailboxKey = -1; 90 private long mTrashMailboxKey = -1; 91 private int mUnreadCountDraft = 0; 92 private int mUnreadCountTrash = 0; 93 94 /** 95 * Open a specific account. 96 * 97 * @param context 98 * @param accountId the account to view 99 */ 100 public static void actionHandleAccount(Context context, long accountId) { 101 Intent intent = new Intent(context, MailboxList.class); 102 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 103 intent.putExtra(EXTRA_ACCOUNT_ID, accountId); 104 context.startActivity(intent); 105 } 106 107 @Override 108 public void onCreate(Bundle icicle) { 109 super.onCreate(icicle); 110 setContentView(R.layout.mailbox_list); 111 112 mHandler = new MailboxListHandler(); 113 mControllerCallback = new ControllerResults(); 114 mListView = getListView(); 115 mProgressIcon = (ProgressBar) findViewById(R.id.title_progress_icon); 116 mErrorBanner = (TextView) findViewById(R.id.connection_error_text); 117 118 mListView.setOnItemClickListener(this); 119 mListView.setItemsCanFocus(false); 120 registerForContextMenu(mListView); 121 122 mListAdapter = new MailboxListAdapter(this); 123 setListAdapter(mListAdapter); 124 125 ((Button) findViewById(R.id.account_title_button)).setOnClickListener(this); 126 127 mAccountId = getIntent().getLongExtra(EXTRA_ACCOUNT_ID, -1); 128 if (mAccountId != -1) { 129 mLoadMailboxesTask = new LoadMailboxesTask(mAccountId); 130 mLoadMailboxesTask.execute(); 131 } else { 132 finish(); 133 } 134 135 ((TextView)findViewById(R.id.title_left_text)).setText(R.string.mailbox_list_title); 136 137 // Go to the database for the account name 138 mLoadAccountNameTask = new AsyncTask<Void, Void, Object[]>() { 139 @Override 140 protected Object[] doInBackground(Void... params) { 141 String accountName = null; 142 Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId); 143 Cursor c = MailboxList.this.getContentResolver().query( 144 uri, new String[] { AccountColumns.DISPLAY_NAME }, null, null, null); 145 try { 146 if (c.moveToFirst()) { 147 accountName = c.getString(0); 148 } 149 } finally { 150 c.close(); 151 } 152 int nAccounts = EmailContent.count(MailboxList.this, Account.CONTENT_URI, null, null); 153 return new Object[] {accountName, nAccounts}; 154 } 155 156 @Override 157 protected void onPostExecute(Object[] result) { 158 if (result == null) { 159 return; 160 } 161 final String accountName = (String) result[0]; 162 // accountName is null if account name can't be retrieved or query exception 163 if (accountName == null) { 164 // something is wrong with this account 165 finish(); 166 } 167 168 final int nAccounts = (Integer) result[1]; 169 setTitleAccountName(accountName, nAccounts > 1); 170 } 171 172 }.execute(); 173 } 174 175 @Override 176 public void onPause() { 177 super.onPause(); 178 Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback); 179 } 180 181 @Override 182 public void onResume() { 183 super.onResume(); 184 Controller.getInstance(getApplication()).addResultCallback(mControllerCallback); 185 186 // Exit immediately if the accounts list has changed (e.g. externally deleted) 187 if (Email.getNotifyUiAccountsChanged()) { 188 Welcome.actionStart(this); 189 finish(); 190 return; 191 } 192 193 updateMessageCount(); 194 195 // TODO: may need to clear notifications here 196 } 197 198 @Override 199 protected void onDestroy() { 200 super.onDestroy(); 201 202 Utility.cancelTaskInterrupt(mLoadMailboxesTask); 203 mLoadMailboxesTask = null; 204 Utility.cancelTaskInterrupt(mLoadAccountNameTask); 205 mLoadAccountNameTask = null; 206 Utility.cancelTaskInterrupt(mMessageCountTask); 207 mMessageCountTask = null; 208 209 mListAdapter.changeCursor(null); 210 } 211 212 public void onClick(View v) { 213 switch (v.getId()) { 214 case R.id.account_title_button: 215 onAccounts(); 216 break; 217 } 218 } 219 220 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 221 onOpenMailbox(id); 222 } 223 224 @Override 225 public boolean onCreateOptionsMenu(Menu menu) { 226 super.onCreateOptionsMenu(menu); 227 getMenuInflater().inflate(R.menu.mailbox_list_option, menu); 228 return true; 229 } 230 231 @Override 232 public boolean onOptionsItemSelected(MenuItem item) { 233 switch (item.getItemId()) { 234 case R.id.refresh: 235 onRefresh(-1); 236 return true; 237 case R.id.accounts: 238 onAccounts(); 239 return true; 240 case R.id.compose: 241 onCompose(); 242 return true; 243 case R.id.account_settings: 244 onEditAccount(); 245 return true; 246 default: 247 return super.onOptionsItemSelected(item); 248 } 249 } 250 251 @Override 252 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo info) { 253 super.onCreateContextMenu(menu, v, info); 254 AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) info; 255 Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position); 256 String folderName = Utility.FolderProperties.getInstance(MailboxList.this) 257 .getDisplayName(Integer.valueOf(c.getString(mListAdapter.COLUMN_TYPE))); 258 if (folderName == null) { 259 folderName = c.getString(mListAdapter.COLUMN_DISPLAY_NAME); 260 } 261 262 menu.setHeaderTitle(folderName); 263 getMenuInflater().inflate(R.menu.mailbox_list_context, menu); 264 } 265 266 @Override 267 public boolean onContextItemSelected(MenuItem item) { 268 AdapterView.AdapterContextMenuInfo info = 269 (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 270 271 switch (item.getItemId()) { 272 case R.id.refresh: 273 onRefresh(info.id); 274 break; 275 case R.id.open: 276 onOpenMailbox(info.id); 277 break; 278 } 279 return super.onContextItemSelected(item); 280 } 281 282 /** 283 * Refresh the mailbox list, or a single mailbox 284 * @param mailboxId -1 for all 285 */ 286 private void onRefresh(long mailboxId) { 287 Controller controller = Controller.getInstance(getApplication()); 288 mHandler.progress(true); 289 if (mailboxId >= 0) { 290 controller.updateMailbox(mAccountId, mailboxId, mControllerCallback); 291 } else { 292 controller.updateMailboxList(mAccountId, mControllerCallback); 293 } 294 } 295 296 private void onAccounts() { 297 AccountFolderList.actionShowAccounts(this); 298 finish(); 299 } 300 301 private void onEditAccount() { 302 AccountSettings.actionSettings(this, mAccountId); 303 } 304 305 private void onOpenMailbox(long mailboxId) { 306 MessageList.actionHandleMailbox(this, mailboxId); 307 } 308 309 private void onCompose() { 310 MessageCompose.actionCompose(this, mAccountId); 311 } 312 313 private void setTitleAccountName(String accountName, boolean showAccountsButton) { 314 TextView accountsButton = (TextView) findViewById(R.id.account_title_button); 315 TextView textPlain = (TextView) findViewById(R.id.title_right_text); 316 if (showAccountsButton) { 317 accountsButton.setVisibility(View.VISIBLE); 318 textPlain.setVisibility(View.GONE); 319 accountsButton.setText(accountName); 320 } else { 321 accountsButton.setVisibility(View.GONE); 322 textPlain.setVisibility(View.VISIBLE); 323 textPlain.setText(accountName); 324 } 325 } 326 327 /** 328 * Async task for loading the mailboxes for a given account 329 */ 330 private class LoadMailboxesTask extends AsyncTask<Void, Void, Cursor> { 331 332 private long mAccountKey; 333 334 /** 335 * Special constructor to cache some local info 336 */ 337 public LoadMailboxesTask(long accountId) { 338 mAccountKey = accountId; 339 } 340 341 @Override 342 protected Cursor doInBackground(Void... params) { 343 Cursor c = MailboxList.this.managedQuery( 344 EmailContent.Mailbox.CONTENT_URI, 345 MailboxList.this.mListAdapter.PROJECTION, 346 MAILBOX_SELECTION, 347 new String[] { String.valueOf(mAccountKey) }, 348 MailboxColumns.TYPE + "," + MailboxColumns.DISPLAY_NAME); 349 mDraftMailboxKey = -1; 350 mTrashMailboxKey = -1; 351 c.moveToPosition(-1); 352 while (c.moveToNext()) { 353 long mailboxId = c.getInt(mListAdapter.COLUMN_ID); 354 switch (c.getInt(mListAdapter.COLUMN_TYPE)) { 355 case Mailbox.TYPE_DRAFTS: 356 mDraftMailboxKey = mailboxId; 357 break; 358 case Mailbox.TYPE_TRASH: 359 mTrashMailboxKey = mailboxId; 360 break; 361 } 362 } 363 if (isCancelled()) { 364 c.close(); 365 c = null; 366 } 367 return c; 368 } 369 370 @Override 371 protected void onPostExecute(Cursor cursor) { 372 if (cursor == null || cursor.isClosed()) { 373 return; 374 } 375 MailboxList.this.mListAdapter.changeCursor(cursor); 376 updateMessageCount(); 377 } 378 } 379 380 private class MessageCountTask extends AsyncTask<Void, Void, int[]> { 381 382 @Override 383 protected int[] doInBackground(Void... params) { 384 int[] counts = new int[2]; 385 if (mDraftMailboxKey != -1) { 386 counts[0] = EmailContent.count(MailboxList.this, Message.CONTENT_URI, 387 MESSAGE_MAILBOX_ID_SELECTION, 388 new String[] { String.valueOf(mDraftMailboxKey)}); 389 } else { 390 counts[0] = -1; 391 } 392 if (mTrashMailboxKey != -1) { 393 counts[1] = EmailContent.count(MailboxList.this, Message.CONTENT_URI, 394 MESSAGE_MAILBOX_ID_SELECTION, 395 new String[] { String.valueOf(mTrashMailboxKey)}); 396 } else { 397 counts[1] = -1; 398 } 399 return counts; 400 } 401 402 @Override 403 protected void onPostExecute(int[] counts) { 404 boolean countChanged = false; 405 if (counts == null) { 406 return; 407 } 408 if (counts[0] != -1) { 409 if (mUnreadCountDraft != counts[0]) { 410 mUnreadCountDraft = counts[0]; 411 countChanged = true; 412 } 413 } else { 414 mUnreadCountDraft = 0; 415 } 416 if (counts[1] != -1) { 417 if (mUnreadCountTrash != counts[1]) { 418 mUnreadCountTrash = counts[1]; 419 countChanged = true; 420 } 421 } else { 422 mUnreadCountTrash = 0; 423 } 424 if (countChanged) { 425 mListAdapter.notifyDataSetChanged(); 426 } 427 } 428 } 429 430 private void updateMessageCount() { 431 if (mAccountId == -1 || mListAdapter.getCursor() == null) { 432 return; 433 } 434 if (mMessageCountTask != null 435 && mMessageCountTask.getStatus() != MessageCountTask.Status.FINISHED) { 436 mMessageCountTask.cancel(true); 437 } 438 mMessageCountTask = (MessageCountTask) new MessageCountTask().execute(); 439 } 440 441 /** 442 * Handler for UI-thread operations (when called from callbacks or any other threads) 443 */ 444 class MailboxListHandler extends Handler { 445 private static final int MSG_PROGRESS = 1; 446 private static final int MSG_ERROR_BANNER = 2; 447 448 @Override 449 public void handleMessage(android.os.Message msg) { 450 switch (msg.what) { 451 case MSG_PROGRESS: 452 boolean showProgress = (msg.arg1 != 0); 453 if (showProgress) { 454 mProgressIcon.setVisibility(View.VISIBLE); 455 } else { 456 mProgressIcon.setVisibility(View.GONE); 457 } 458 break; 459 case MSG_ERROR_BANNER: 460 String message = (String) msg.obj; 461 boolean isVisible = mErrorBanner.getVisibility() == View.VISIBLE; 462 if (message != null) { 463 mErrorBanner.setText(message); 464 if (!isVisible) { 465 mErrorBanner.setVisibility(View.VISIBLE); 466 mErrorBanner.startAnimation( 467 AnimationUtils.loadAnimation( 468 MailboxList.this, R.anim.header_appear)); 469 } 470 } else { 471 if (isVisible) { 472 mErrorBanner.setVisibility(View.GONE); 473 mErrorBanner.startAnimation( 474 AnimationUtils.loadAnimation( 475 MailboxList.this, R.anim.header_disappear)); 476 } 477 } 478 break; 479 default: 480 super.handleMessage(msg); 481 } 482 } 483 484 /** 485 * Call from any thread to start/stop progress indicator(s) 486 * @param progress true to start, false to stop 487 */ 488 public void progress(boolean progress) { 489 android.os.Message msg = android.os.Message.obtain(); 490 msg.what = MSG_PROGRESS; 491 msg.arg1 = progress ? 1 : 0; 492 sendMessage(msg); 493 } 494 495 /** 496 * Called from any thread to show or hide the connection error banner. 497 * @param message error text or null to hide the box 498 */ 499 public void showErrorBanner(String message) { 500 android.os.Message msg = android.os.Message.obtain(); 501 msg.what = MSG_ERROR_BANNER; 502 msg.obj = message; 503 sendMessage(msg); 504 } 505 } 506 507 /** 508 * Callback for async Controller results. 509 */ 510 private class ControllerResults implements Controller.Result { 511 512 // TODO report errors into UI 513 public void updateMailboxListCallback(MessagingException result, long accountKey, 514 int progress) { 515 if (accountKey == mAccountId) { 516 updateBanner(result, progress); 517 updateProgress(result, progress); 518 } 519 } 520 521 // TODO report errors into UI 522 public void updateMailboxCallback(MessagingException result, long accountKey, 523 long mailboxKey, int progress, int numNewMessages) { 524 if (result != null || progress == 100) { 525 Email.updateMailboxRefreshTime(mailboxKey); 526 } 527 if (accountKey == mAccountId) { 528 updateBanner(result, progress); 529 updateProgress(result, progress); 530 } 531 } 532 533 public void loadMessageForViewCallback(MessagingException result, long messageId, 534 int progress) { 535 } 536 537 public void loadAttachmentCallback(MessagingException result, long messageId, 538 long attachmentId, int progress) { 539 } 540 541 public void serviceCheckMailCallback(MessagingException result, long accountId, 542 long mailboxId, int progress, long tag) { 543 } 544 545 public void sendMailCallback(MessagingException result, long accountId, long messageId, 546 int progress) { 547 if (accountId == mAccountId) { 548 updateBanner(result, progress); 549 updateProgress(result, progress); 550 } 551 } 552 553 private void updateProgress(MessagingException result, int progress) { 554 if (result != null || progress == 100) { 555 mHandler.progress(false); 556 } else if (progress == 0) { 557 mHandler.progress(true); 558 } 559 } 560 561 /** 562 * Show or hide the connection error banner, and convert the various MessagingException 563 * variants into localizable text. There is hysteresis in the show/hide logic: Once shown, 564 * the banner will remain visible until some progress is made on the connection. The 565 * goal is to keep it from flickering during retries in a bad connection state. 566 * 567 * @param result 568 * @param progress 569 */ 570 private void updateBanner(MessagingException result, int progress) { 571 if (result != null) { 572 int id = R.string.status_network_error; 573 if (result instanceof AuthenticationFailedException) { 574 id = R.string.account_setup_failed_dlg_auth_message; 575 } else if (result instanceof CertificateValidationException) { 576 id = R.string.account_setup_failed_dlg_certificate_message; 577 } else { 578 switch (result.getExceptionType()) { 579 case MessagingException.IOERROR: 580 id = R.string.account_setup_failed_ioerror; 581 break; 582 case MessagingException.TLS_REQUIRED: 583 id = R.string.account_setup_failed_tls_required; 584 break; 585 case MessagingException.AUTH_REQUIRED: 586 id = R.string.account_setup_failed_auth_required; 587 break; 588 case MessagingException.GENERAL_SECURITY: 589 id = R.string.account_setup_failed_security; 590 break; 591 } 592 } 593 mHandler.showErrorBanner(getString(id)); 594 } else if (progress > 0) { 595 mHandler.showErrorBanner(null); 596 } 597 } 598 } 599 600 /** 601 * The adapter for displaying mailboxes. 602 */ 603 /* package */ class MailboxListAdapter extends CursorAdapter { 604 605 public final String[] PROJECTION = new String[] { MailboxColumns.ID, 606 MailboxColumns.DISPLAY_NAME, MailboxColumns.UNREAD_COUNT, MailboxColumns.TYPE }; 607 public final int COLUMN_ID = 0; 608 public final int COLUMN_DISPLAY_NAME = 1; 609 public final int COLUMN_UNREAD_COUNT = 2; 610 public final int COLUMN_TYPE = 3; 611 612 Context mContext; 613 private LayoutInflater mInflater; 614 615 public MailboxListAdapter(Context context) { 616 super(context, null); 617 mContext = context; 618 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 619 } 620 621 @Override 622 public void bindView(View view, Context context, Cursor cursor) { 623 int type = cursor.getInt(COLUMN_TYPE); 624 String text = Utility.FolderProperties.getInstance(context) 625 .getDisplayName(type); 626 if (text == null) { 627 text = cursor.getString(COLUMN_DISPLAY_NAME); 628 } 629 TextView nameView = (TextView) view.findViewById(R.id.mailbox_name); 630 if (text != null) { 631 nameView.setText(text); 632 } 633 634 // TODO get/track live folder status 635 text = null; 636 TextView statusView = (TextView) view.findViewById(R.id.mailbox_status); 637 if (text != null) { 638 statusView.setText(text); 639 statusView.setVisibility(View.VISIBLE); 640 } else { 641 statusView.setVisibility(View.GONE); 642 } 643 View chipView = view.findViewById(R.id.chip); 644 chipView.setBackgroundResource(Email.getAccountColorResourceId(mAccountId)); 645 // TODO do we use a different count for special mailboxes (total count vs. unread) 646 int count = -1; 647 switch (type) { 648 case Mailbox.TYPE_DRAFTS: 649 count = mUnreadCountDraft; 650 text = String.valueOf(count); 651 break; 652 case Mailbox.TYPE_TRASH: 653 count = mUnreadCountTrash; 654 text = String.valueOf(count); 655 break; 656 default: 657 text = cursor.getString(COLUMN_UNREAD_COUNT); 658 if (text != null) { 659 count = Integer.valueOf(text); 660 } 661 break; 662 } 663 TextView unreadCountView = (TextView) view.findViewById(R.id.new_message_count); 664 TextView allCountView = (TextView) view.findViewById(R.id.all_message_count); 665 // If the unread count is zero, not to show countView. 666 if (count > 0) { 667 nameView.setTypeface(Typeface.DEFAULT_BOLD); 668 switch (type) { 669 case Mailbox.TYPE_DRAFTS: 670 case Mailbox.TYPE_OUTBOX: 671 case Mailbox.TYPE_SENT: 672 case Mailbox.TYPE_TRASH: 673 unreadCountView.setVisibility(View.GONE); 674 allCountView.setVisibility(View.VISIBLE); 675 allCountView.setText(text); 676 break; 677 default: 678 allCountView.setVisibility(View.GONE); 679 unreadCountView.setVisibility(View.VISIBLE); 680 unreadCountView.setText(text); 681 break; 682 } 683 } else { 684 nameView.setTypeface(Typeface.DEFAULT); 685 allCountView.setVisibility(View.GONE); 686 unreadCountView.setVisibility(View.GONE); 687 } 688 689 ImageView folderIcon = (ImageView) view.findViewById(R.id.folder_icon); 690 folderIcon.setImageDrawable(Utility.FolderProperties.getInstance(context) 691 .getIconIds(type)); 692 } 693 694 @Override 695 public View newView(Context context, Cursor cursor, ViewGroup parent) { 696 return mInflater.inflate(R.layout.mailbox_list_item, parent, false); 697 } 698 } 699 } 700