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.AccountBackupRestore; 20import com.android.email.Controller; 21import com.android.email.Email; 22import com.android.email.R; 23import com.android.email.SecurityPolicy; 24import com.android.email.Utility; 25import com.android.email.activity.setup.AccountSettings; 26import com.android.email.activity.setup.AccountSetupBasics; 27import com.android.email.mail.MessagingException; 28import com.android.email.mail.Store; 29import com.android.email.provider.EmailContent; 30import com.android.email.provider.EmailContent.Account; 31import com.android.email.provider.EmailContent.Mailbox; 32import com.android.email.provider.EmailContent.MailboxColumns; 33import com.android.email.provider.EmailContent.Message; 34import com.android.email.provider.EmailContent.MessageColumns; 35import com.android.email.service.MailService; 36 37import android.app.AlertDialog; 38import android.app.Dialog; 39import android.app.ListActivity; 40import android.app.NotificationManager; 41import android.content.ContentUris; 42import android.content.Context; 43import android.content.DialogInterface; 44import android.content.Intent; 45import android.database.Cursor; 46import android.database.MatrixCursor; 47import android.database.MergeCursor; 48import android.database.MatrixCursor.RowBuilder; 49import android.net.Uri; 50import android.os.AsyncTask; 51import android.os.Bundle; 52import android.os.Handler; 53import android.view.ContextMenu; 54import android.view.KeyEvent; 55import android.view.LayoutInflater; 56import android.view.Menu; 57import android.view.MenuItem; 58import android.view.View; 59import android.view.ViewGroup; 60import android.view.Window; 61import android.view.ContextMenu.ContextMenuInfo; 62import android.widget.AdapterView; 63import android.widget.CursorAdapter; 64import android.widget.ImageView; 65import android.widget.ListAdapter; 66import android.widget.ListView; 67import android.widget.ProgressBar; 68import android.widget.TextView; 69import android.widget.Toast; 70import android.widget.AdapterView.OnItemClickListener; 71 72import java.util.ArrayList; 73 74public class AccountFolderList extends ListActivity implements OnItemClickListener { 75 private static final int DIALOG_REMOVE_ACCOUNT = 1; 76 /** 77 * Key codes used to open a debug settings screen. 78 */ 79 private static int[] secretKeyCodes = { 80 KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_B, KeyEvent.KEYCODE_U, 81 KeyEvent.KEYCODE_G 82 }; 83 private int mSecretKeyCodeIndex = 0; 84 85 private static final String ICICLE_SELECTED_ACCOUNT = "com.android.email.selectedAccount"; 86 private EmailContent.Account mSelectedContextAccount; 87 88 private ListView mListView; 89 private ProgressBar mProgressIcon; 90 91 private AccountsAdapter mListAdapter; 92 93 private LoadAccountsTask mLoadAccountsTask; 94 private DeleteAccountTask mDeleteAccountTask; 95 private MessageListHandler mHandler; 96 private ControllerResults mControllerCallback; 97 98 /** 99 * Reduced mailbox projection used by AccountsAdapter 100 */ 101 public final static int MAILBOX_COLUMN_ID = 0; 102 public final static int MAILBOX_DISPLAY_NAME = 1; 103 public final static int MAILBOX_ACCOUNT_KEY = 2; 104 public final static int MAILBOX_TYPE = 3; 105 public final static int MAILBOX_UNREAD_COUNT = 4; 106 public final static int MAILBOX_FLAG_VISIBLE = 5; 107 public final static int MAILBOX_FLAGS = 6; 108 109 public final static String[] MAILBOX_PROJECTION = new String[] { 110 EmailContent.RECORD_ID, MailboxColumns.DISPLAY_NAME, 111 MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE, 112 MailboxColumns.UNREAD_COUNT, 113 MailboxColumns.FLAG_VISIBLE, MailboxColumns.FLAGS 114 }; 115 116 private static final String FAVORITE_COUNT_SELECTION = 117 MessageColumns.FLAG_FAVORITE + "= 1"; 118 119 private static final String MAILBOX_TYPE_SELECTION = 120 MailboxColumns.TYPE + " =?"; 121 122 private static final String MAILBOX_ID_SELECTION = 123 MessageColumns.MAILBOX_KEY + " =?"; 124 125 private static final String[] MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION = new String [] { 126 "sum(" + MailboxColumns.UNREAD_COUNT + ")" 127 }; 128 129 private static final String MAILBOX_INBOX_SELECTION = 130 MailboxColumns.ACCOUNT_KEY + " =?" + " AND " + MailboxColumns.TYPE +" = " 131 + Mailbox.TYPE_INBOX; 132 133 private static final int MAILBOX_UNREAD_COUNT_COLUMN_UNREAD_COUNT = 0; 134 private static final String[] MAILBOX_UNREAD_COUNT_PROJECTION = new String [] { 135 MailboxColumns.UNREAD_COUNT 136 }; 137 138 /** 139 * Start the Accounts list activity. Uses the CLEAR_TOP flag which means that other stacked 140 * activities may be killed in order to get back to Accounts. 141 */ 142 public static void actionShowAccounts(Context context) { 143 Intent i = new Intent(context, AccountFolderList.class); 144 i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 145 context.startActivity(i); 146 } 147 148 @Override 149 public void onCreate(Bundle icicle) { 150 super.onCreate(icicle); 151 152 requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); 153 setContentView(R.layout.account_folder_list); 154 getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, 155 R.layout.list_title); 156 157 mHandler = new MessageListHandler(); 158 mControllerCallback = new ControllerResults(); 159 mProgressIcon = (ProgressBar) findViewById(R.id.title_progress_icon); 160 161 mListView = getListView(); 162 mListView.setItemsCanFocus(false); 163 mListView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_INSET); 164 mListView.setOnItemClickListener(this); 165 mListView.setLongClickable(true); 166 registerForContextMenu(mListView); 167 168 if (icicle != null && icicle.containsKey(ICICLE_SELECTED_ACCOUNT)) { 169 mSelectedContextAccount = (Account) icicle.getParcelable(ICICLE_SELECTED_ACCOUNT); 170 } 171 172 ((TextView) findViewById(R.id.title_left_text)).setText(R.string.app_name); 173 } 174 175 @Override 176 public void onSaveInstanceState(Bundle outState) { 177 super.onSaveInstanceState(outState); 178 if (mSelectedContextAccount != null) { 179 outState.putParcelable(ICICLE_SELECTED_ACCOUNT, mSelectedContextAccount); 180 } 181 } 182 183 @Override 184 public void onPause() { 185 super.onPause(); 186 Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback); 187 } 188 189 @Override 190 public void onResume() { 191 super.onResume(); 192 193 NotificationManager notifMgr = (NotificationManager) 194 getSystemService(Context.NOTIFICATION_SERVICE); 195 notifMgr.cancel(1); 196 197 Controller.getInstance(getApplication()).addResultCallback(mControllerCallback); 198 199 // Exit immediately if the accounts list has changed (e.g. externally deleted) 200 if (Email.getNotifyUiAccountsChanged()) { 201 Welcome.actionStart(this); 202 finish(); 203 return; 204 } 205 206 updateAccounts(); 207 // TODO: What updates do we need to auto-trigger, now that we have mailboxes in view? 208 } 209 210 @Override 211 protected void onDestroy() { 212 super.onDestroy(); 213 Utility.cancelTaskInterrupt(mLoadAccountsTask); 214 mLoadAccountsTask = null; 215 216 // TODO: We shouldn't call cancel() for DeleteAccountTask. If the task hasn't 217 // started, this will mark it as "don't run", but we always want it to finish. 218 // (But don't just remove this cancel() call. DeleteAccountTask.onPostExecute() checks if 219 // it's been canceled to decided whether to update the UI.) 220 Utility.cancelTask(mDeleteAccountTask, false); // Don't interrupt if it's running. 221 mDeleteAccountTask = null; 222 223 if (mListAdapter != null) { 224 mListAdapter.changeCursor(null); 225 } 226 } 227 228 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 229 if (mListAdapter.isMailbox(position)) { 230 MessageList.actionHandleMailbox(this, id); 231 } else if (mListAdapter.isAccount(position)) { 232 MessageList.actionHandleAccount(this, id, Mailbox.TYPE_INBOX); 233 } 234 } 235 236 private static int getUnreadCountByMailboxType(Context context, int type) { 237 int count = 0; 238 Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI, 239 MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION, 240 MAILBOX_TYPE_SELECTION, 241 new String[] { String.valueOf(type) }, null); 242 243 try { 244 if (c.moveToFirst()) { 245 return c.getInt(0); 246 } 247 } finally { 248 c.close(); 249 } 250 return count; 251 } 252 253 private static int getCountByMailboxType(Context context, int type) { 254 int count = 0; 255 Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI, 256 EmailContent.ID_PROJECTION, MAILBOX_TYPE_SELECTION, 257 new String[] { String.valueOf(type) }, null); 258 259 try { 260 c.moveToPosition(-1); 261 while (c.moveToNext()) { 262 count += EmailContent.count(context, Message.CONTENT_URI, 263 MAILBOX_ID_SELECTION, 264 new String[] { 265 String.valueOf(c.getLong(EmailContent.ID_PROJECTION_COLUMN)) }); 266 } 267 } finally { 268 c.close(); 269 } 270 return count; 271 } 272 273 /** 274 * Build the group and child cursors that support the summary views (aka "at a glance"). 275 * 276 * This is a placeholder implementation with significant problems that need to be addressed: 277 * 278 * TODO: We should only show summary mailboxes if they are non-empty. So there needs to be 279 * a more dynamic child-cursor here, probably listening for update notifications on a number 280 * of other internally-held queries such as count-of-inbox, count-of-unread, etc. 281 * 282 * TODO: This simple list is incomplete. For example, we probably want drafts, outbox, and 283 * (maybe) sent (again, these would be displayed only when non-empty). 284 * 285 * TODO: We need a way to count total unread in all inboxes (probably with some provider help) 286 * 287 * TODO: We need a way to count total # messages in all other summary boxes (probably with 288 * some provider help). 289 * 290 * TODO use narrower account projection (see LoadAccountsTask) 291 */ 292 private MatrixCursor getSummaryChildCursor() { 293 MatrixCursor childCursor = new MatrixCursor(MAILBOX_PROJECTION); 294 int count; 295 RowBuilder row; 296 // TYPE_INBOX 297 count = getUnreadCountByMailboxType(this, Mailbox.TYPE_INBOX); 298 row = childCursor.newRow(); 299 row.add(Long.valueOf(Mailbox.QUERY_ALL_INBOXES)); // MAILBOX_COLUMN_ID = 0; 300 row.add(getString(R.string.account_folder_list_summary_inbox)); // MAILBOX_DISPLAY_NAME 301 row.add(null); // MAILBOX_ACCOUNT_KEY = 2; 302 row.add(Integer.valueOf(Mailbox.TYPE_INBOX)); // MAILBOX_TYPE = 3; 303 row.add(Integer.valueOf(count)); // MAILBOX_UNREAD_COUNT = 4; 304 // TYPE_MAIL (FAVORITES) 305 count = EmailContent.count(this, Message.CONTENT_URI, FAVORITE_COUNT_SELECTION, null); 306 if (count > 0) { 307 row = childCursor.newRow(); 308 row.add(Long.valueOf(Mailbox.QUERY_ALL_FAVORITES)); // MAILBOX_COLUMN_ID = 0; 309 // MAILBOX_DISPLAY_NAME 310 row.add(getString(R.string.account_folder_list_summary_starred)); 311 row.add(null); // MAILBOX_ACCOUNT_KEY = 2; 312 row.add(Integer.valueOf(Mailbox.TYPE_MAIL)); // MAILBOX_TYPE = 3; 313 row.add(Integer.valueOf(count)); // MAILBOX_UNREAD_COUNT = 4; 314 } 315 // TYPE_DRAFTS 316 count = getCountByMailboxType(this, Mailbox.TYPE_DRAFTS); 317 if (count > 0) { 318 row = childCursor.newRow(); 319 row.add(Long.valueOf(Mailbox.QUERY_ALL_DRAFTS)); // MAILBOX_COLUMN_ID = 0; 320 row.add(getString(R.string.account_folder_list_summary_drafts));// MAILBOX_DISPLAY_NAME 321 row.add(null); // MAILBOX_ACCOUNT_KEY = 2; 322 row.add(Integer.valueOf(Mailbox.TYPE_DRAFTS)); // MAILBOX_TYPE = 3; 323 row.add(Integer.valueOf(count)); // MAILBOX_UNREAD_COUNT = 4; 324 } 325 // TYPE_OUTBOX 326 count = getCountByMailboxType(this, Mailbox.TYPE_OUTBOX); 327 if (count > 0) { 328 row = childCursor.newRow(); 329 row.add(Long.valueOf(Mailbox.QUERY_ALL_OUTBOX)); // MAILBOX_COLUMN_ID = 0; 330 row.add(getString(R.string.account_folder_list_summary_outbox));// MAILBOX_DISPLAY_NAME 331 row.add(null); // MAILBOX_ACCOUNT_KEY = 2; 332 row.add(Integer.valueOf(Mailbox.TYPE_OUTBOX)); // MAILBOX_TYPE = 3; 333 row.add(Integer.valueOf(count)); // MAILBOX_UNREAD_COUNT = 4; 334 } 335 return childCursor; 336 } 337 338 /** 339 * Async task to handle the accounts query outside of the UI thread 340 */ 341 private class LoadAccountsTask extends AsyncTask<Void, Void, Object[]> { 342 @Override 343 protected Object[] doInBackground(Void... params) { 344 // Create the summaries cursor 345 Cursor c1 = getSummaryChildCursor(); 346 347 // TODO use a custom projection and don't have to sample all of these columns 348 Cursor c2 = getContentResolver().query( 349 EmailContent.Account.CONTENT_URI, 350 EmailContent.Account.CONTENT_PROJECTION, null, null, null); 351 Long defaultAccount = Account.getDefaultAccountId(AccountFolderList.this); 352 return new Object[] { c1, c2 , defaultAccount}; 353 } 354 355 @Override 356 protected void onPostExecute(Object[] params) { 357 if (isCancelled() || params == null || ((Cursor)params[1]).isClosed()) { 358 return; 359 } 360 // Before writing a new list adapter into the listview, we need to 361 // shut down the old one (if any). 362 ListAdapter oldAdapter = mListView.getAdapter(); 363 if (oldAdapter != null && oldAdapter instanceof CursorAdapter) { 364 ((CursorAdapter)oldAdapter).changeCursor(null); 365 } 366 // Now create a new list adapter and install it 367 mListAdapter = AccountsAdapter.getInstance((Cursor)params[0], (Cursor)params[1], 368 AccountFolderList.this, (Long)params[2]); 369 mListView.setAdapter(mListAdapter); 370 } 371 } 372 373 private class DeleteAccountTask extends AsyncTask<Void, Void, Void> { 374 private final long mAccountId; 375 private final String mAccountUri; 376 377 public DeleteAccountTask(long accountId, String accountUri) { 378 mAccountId = accountId; 379 mAccountUri = accountUri; 380 } 381 382 @Override 383 protected Void doInBackground(Void... params) { 384 try { 385 // Delete Remote store at first. 386 Store.getInstance(mAccountUri, getApplication(), null).delete(); 387 // Remove the Store instance from cache. 388 Store.removeInstance(mAccountUri); 389 Uri uri = ContentUris.withAppendedId( 390 EmailContent.Account.CONTENT_URI, mAccountId); 391 AccountFolderList.this.getContentResolver().delete(uri, null, null); 392 // Update the backup (side copy) of the accounts 393 AccountBackupRestore.backupAccounts(AccountFolderList.this); 394 // Release or relax device administration, if relevant 395 SecurityPolicy.getInstance(AccountFolderList.this).reducePolicies(); 396 } catch (Exception e) { 397 // Ignore 398 } 399 Email.setServicesEnabled(AccountFolderList.this); 400 return null; 401 } 402 403 @Override 404 protected void onPostExecute(Void v) { 405 if (!isCancelled()) { 406 updateAccounts(); 407 } 408 } 409 } 410 411 private void updateAccounts() { 412 Utility.cancelTaskInterrupt(mLoadAccountsTask); 413 mLoadAccountsTask = (LoadAccountsTask) new LoadAccountsTask().execute(); 414 } 415 416 private void onAddNewAccount() { 417 AccountSetupBasics.actionNewAccount(this); 418 } 419 420 private void onEditAccount(long accountId) { 421 AccountSettings.actionSettings(this, accountId); 422 } 423 424 /** 425 * Refresh one or all accounts 426 * @param accountId A specific id to refresh folders only, or -1 to refresh everything 427 */ 428 private void onRefresh(long accountId) { 429 if (accountId == -1) { 430 // TODO implement a suitable "Refresh all accounts" / "check mail" comment in Controller 431 // TODO this is temp 432 Toast.makeText(this, getString(R.string.account_folder_list_refresh_toast), 433 Toast.LENGTH_LONG).show(); 434 } else { 435 mHandler.progress(true); 436 Controller.getInstance(getApplication()).updateMailboxList( 437 accountId, mControllerCallback); 438 } 439 } 440 441 private void onCompose(long accountId) { 442 if (accountId == -1) { 443 accountId = Account.getDefaultAccountId(this); 444 } 445 if (accountId != -1) { 446 MessageCompose.actionCompose(this, accountId); 447 } else { 448 onAddNewAccount(); 449 } 450 } 451 452 private void onDeleteAccount(long accountId) { 453 mSelectedContextAccount = Account.restoreAccountWithId(this, accountId); 454 showDialog(DIALOG_REMOVE_ACCOUNT); 455 } 456 457 @Override 458 public Dialog onCreateDialog(int id) { 459 switch (id) { 460 case DIALOG_REMOVE_ACCOUNT: 461 return createRemoveAccountDialog(); 462 } 463 return super.onCreateDialog(id); 464 } 465 466 private Dialog createRemoveAccountDialog() { 467 return new AlertDialog.Builder(this) 468 .setIcon(android.R.drawable.ic_dialog_alert) 469 .setTitle(R.string.account_delete_dlg_title) 470 .setMessage(getString(R.string.account_delete_dlg_instructions_fmt, 471 mSelectedContextAccount.getDisplayName())) 472 .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() { 473 public void onClick(DialogInterface dialog, int whichButton) { 474 dismissDialog(DIALOG_REMOVE_ACCOUNT); 475 // Clear notifications, which may become stale here 476 NotificationManager notificationManager = (NotificationManager) 477 getSystemService(Context.NOTIFICATION_SERVICE); 478 notificationManager.cancel(MailService.NOTIFICATION_ID_NEW_MESSAGES); 479 int numAccounts = EmailContent.count(AccountFolderList.this, 480 Account.CONTENT_URI, null, null); 481 mListAdapter.addOnDeletingAccount(mSelectedContextAccount.mId); 482 483 mDeleteAccountTask = (DeleteAccountTask) new DeleteAccountTask( 484 mSelectedContextAccount.mId, 485 mSelectedContextAccount.getStoreUri(AccountFolderList.this)).execute(); 486 if (numAccounts == 1) { 487 AccountSetupBasics.actionNewAccount(AccountFolderList.this); 488 finish(); 489 } 490 } 491 }) 492 .setNegativeButton(R.string.cancel_action, new DialogInterface.OnClickListener() { 493 public void onClick(DialogInterface dialog, int whichButton) { 494 dismissDialog(DIALOG_REMOVE_ACCOUNT); 495 } 496 }) 497 .create(); 498 } 499 500 /** 501 * Update a cached dialog with current values (e.g. account name) 502 */ 503 @Override 504 public void onPrepareDialog(int id, Dialog dialog) { 505 switch (id) { 506 case DIALOG_REMOVE_ACCOUNT: 507 AlertDialog alert = (AlertDialog) dialog; 508 alert.setMessage(getString(R.string.account_delete_dlg_instructions_fmt, 509 mSelectedContextAccount.getDisplayName())); 510 } 511 } 512 513 @Override 514 public boolean onContextItemSelected(MenuItem item) { 515 AdapterView.AdapterContextMenuInfo menuInfo = 516 (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 517 518 if (mListAdapter.isMailbox(menuInfo.position)) { 519 Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position); 520 long id = c.getLong(MAILBOX_COLUMN_ID); 521 switch (item.getItemId()) { 522 case R.id.open_folder: 523 MessageList.actionHandleMailbox(this, id); 524 break; 525 case R.id.check_mail: 526 onRefresh(-1); 527 break; 528 } 529 return false; 530 } else if (mListAdapter.isAccount(menuInfo.position)) { 531 Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position); 532 long accountId = c.getLong(Account.CONTENT_ID_COLUMN); 533 switch (item.getItemId()) { 534 case R.id.open_folder: 535 MailboxList.actionHandleAccount(this, accountId); 536 break; 537 case R.id.compose: 538 onCompose(accountId); 539 break; 540 case R.id.refresh_account: 541 onRefresh(accountId); 542 break; 543 case R.id.edit_account: 544 onEditAccount(accountId); 545 break; 546 case R.id.delete_account: 547 onDeleteAccount(accountId); 548 break; 549 } 550 return true; 551 } 552 return false; 553 } 554 555 @Override 556 public boolean onOptionsItemSelected(MenuItem item) { 557 switch (item.getItemId()) { 558 case R.id.add_new_account: 559 onAddNewAccount(); 560 break; 561 case R.id.check_mail: 562 onRefresh(-1); 563 break; 564 case R.id.compose: 565 onCompose(-1); 566 break; 567 default: 568 return super.onOptionsItemSelected(item); 569 } 570 return true; 571 } 572 573 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { 574 return true; 575 } 576 577 @Override 578 public boolean onCreateOptionsMenu(Menu menu) { 579 super.onCreateOptionsMenu(menu); 580 getMenuInflater().inflate(R.menu.account_folder_list_option, menu); 581 return true; 582 } 583 584 @Override 585 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo info) { 586 super.onCreateContextMenu(menu, v, info); 587 AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) info; 588 if (mListAdapter.isMailbox(menuInfo.position)) { 589 Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position); 590 String displayName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN); 591 menu.setHeaderTitle(displayName); 592 getMenuInflater().inflate(R.menu.account_folder_list_smart_folder_context, menu); 593 } else if (mListAdapter.isAccount(menuInfo.position)) { 594 Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position); 595 String accountName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN); 596 menu.setHeaderTitle(accountName); 597 getMenuInflater().inflate(R.menu.account_folder_list_context, menu); 598 } 599 } 600 601 @Override 602 public boolean onKeyDown(int keyCode, KeyEvent event) { 603 if (event.getKeyCode() == secretKeyCodes[mSecretKeyCodeIndex]) { 604 mSecretKeyCodeIndex++; 605 if (mSecretKeyCodeIndex == secretKeyCodes.length) { 606 mSecretKeyCodeIndex = 0; 607 startActivity(new Intent(this, Debug.class)); 608 } 609 } else { 610 mSecretKeyCodeIndex = 0; 611 } 612 return super.onKeyDown(keyCode, event); 613 } 614 615 /** 616 * Handler for UI-thread operations (when called from callbacks or any other threads) 617 */ 618 private class MessageListHandler extends Handler { 619 private static final int MSG_PROGRESS = 1; 620 621 @Override 622 public void handleMessage(android.os.Message msg) { 623 switch (msg.what) { 624 case MSG_PROGRESS: 625 boolean showProgress = (msg.arg1 != 0); 626 if (showProgress) { 627 mProgressIcon.setVisibility(View.VISIBLE); 628 } else { 629 mProgressIcon.setVisibility(View.GONE); 630 } 631 break; 632 default: 633 super.handleMessage(msg); 634 } 635 } 636 637 /** 638 * Call from any thread to start/stop progress indicator(s) 639 * @param progress true to start, false to stop 640 */ 641 public void progress(boolean progress) { 642 android.os.Message msg = android.os.Message.obtain(); 643 msg.what = MSG_PROGRESS; 644 msg.arg1 = progress ? 1 : 0; 645 sendMessage(msg); 646 } 647 } 648 649 /** 650 * Callback for async Controller results. 651 */ 652 private class ControllerResults implements Controller.Result { 653 public void updateMailboxListCallback(MessagingException result, long accountKey, 654 int progress) { 655 updateProgress(result, progress); 656 } 657 658 public void updateMailboxCallback(MessagingException result, long accountKey, 659 long mailboxKey, int progress, int numNewMessages) { 660 if (result != null || progress == 100) { 661 Email.updateMailboxRefreshTime(mailboxKey); 662 } 663 if (progress == 100) { 664 updateAccounts(); 665 } 666 updateProgress(result, progress); 667 } 668 669 public void loadMessageForViewCallback(MessagingException result, long messageId, 670 int progress) { 671 } 672 673 public void loadAttachmentCallback(MessagingException result, long messageId, 674 long attachmentId, int progress) { 675 } 676 677 public void serviceCheckMailCallback(MessagingException result, long accountId, 678 long mailboxId, int progress, long tag) { 679 updateProgress(result, progress); 680 } 681 682 public void sendMailCallback(MessagingException result, long accountId, long messageId, 683 int progress) { 684 if (progress == 100) { 685 updateAccounts(); 686 } 687 } 688 689 private void updateProgress(MessagingException result, int progress) { 690 if (result != null || progress == 100) { 691 mHandler.progress(false); 692 } else if (progress == 0) { 693 mHandler.progress(true); 694 } 695 } 696 } 697 698 /* package */ static class AccountsAdapter extends CursorAdapter { 699 700 private final Context mContext; 701 private final LayoutInflater mInflater; 702 private final int mMailboxesCount; 703 private final int mSeparatorPosition; 704 private final long mDefaultAccountId; 705 private final ArrayList<Long> mOnDeletingAccounts = new ArrayList<Long>(); 706 707 public static AccountsAdapter getInstance(Cursor mailboxesCursor, Cursor accountsCursor, 708 Context context, long defaultAccountId) { 709 Cursor[] cursors = new Cursor[] { mailboxesCursor, accountsCursor }; 710 Cursor mc = new MergeCursor(cursors); 711 return new AccountsAdapter(mc, context, mailboxesCursor.getCount(), defaultAccountId); 712 } 713 714 public AccountsAdapter(Cursor c, Context context, int mailboxesCount, 715 long defaultAccountId) { 716 super(context, c, true); 717 mContext = context; 718 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 719 mMailboxesCount = mailboxesCount; 720 mSeparatorPosition = mailboxesCount; 721 mDefaultAccountId = defaultAccountId; 722 } 723 724 public boolean isMailbox(int position) { 725 return position < mMailboxesCount; 726 } 727 728 public boolean isAccount(int position) { 729 return position >= mMailboxesCount; 730 } 731 732 public void addOnDeletingAccount(long accountId) { 733 mOnDeletingAccounts.add(accountId); 734 } 735 736 public boolean isOnDeletingAccountView(long accountId) { 737 return mOnDeletingAccounts.contains(accountId); 738 } 739 740 /** 741 * This is used as a callback from the list items, for clicks in the folder "button" 742 * 743 * @param itemView the item in which the click occurred 744 */ 745 public void onClickFolder(AccountFolderListItem itemView) { 746 MailboxList.actionHandleAccount(mContext, itemView.mAccountId); 747 } 748 749 @Override 750 public void bindView(View view, Context context, Cursor cursor) { 751 if (cursor.getPosition() < mMailboxesCount) { 752 bindMailboxItem(view, context, cursor, false); 753 } else { 754 bindAccountItem(view, context, cursor, false); 755 } 756 } 757 758 private void bindMailboxItem(View view, Context context, Cursor cursor, boolean isLastChild) 759 { 760 // Reset the view (in case it was recycled) and prepare for binding 761 AccountFolderListItem itemView = (AccountFolderListItem) view; 762 itemView.bindViewInit(this, false); 763 764 // Invisible (not "gone") to maintain spacing 765 view.findViewById(R.id.chip).setVisibility(View.INVISIBLE); 766 767 String text = cursor.getString(MAILBOX_DISPLAY_NAME); 768 if (text != null) { 769 TextView nameView = (TextView) view.findViewById(R.id.name); 770 nameView.setText(text); 771 } 772 773 // TODO get/track live folder status 774 text = null; 775 TextView statusView = (TextView) view.findViewById(R.id.status); 776 if (text != null) { 777 statusView.setText(text); 778 statusView.setVisibility(View.VISIBLE); 779 } else { 780 statusView.setVisibility(View.GONE); 781 } 782 783 int count = -1; 784 text = cursor.getString(MAILBOX_UNREAD_COUNT); 785 if (text != null) { 786 count = Integer.valueOf(text); 787 } 788 TextView unreadCountView = (TextView) view.findViewById(R.id.new_message_count); 789 TextView allCountView = (TextView) view.findViewById(R.id.all_message_count); 790 int id = cursor.getInt(MAILBOX_COLUMN_ID); 791 // If the unread count is zero, not to show countView. 792 if (count > 0) { 793 if (id == Mailbox.QUERY_ALL_FAVORITES 794 || id == Mailbox.QUERY_ALL_DRAFTS 795 || id == Mailbox.QUERY_ALL_OUTBOX) { 796 unreadCountView.setVisibility(View.GONE); 797 allCountView.setVisibility(View.VISIBLE); 798 allCountView.setText(text); 799 } else { 800 allCountView.setVisibility(View.GONE); 801 unreadCountView.setVisibility(View.VISIBLE); 802 unreadCountView.setText(text); 803 } 804 } else { 805 allCountView.setVisibility(View.GONE); 806 unreadCountView.setVisibility(View.GONE); 807 } 808 809 view.findViewById(R.id.folder_button).setVisibility(View.GONE); 810 view.findViewById(R.id.folder_separator).setVisibility(View.GONE); 811 view.findViewById(R.id.default_sender).setVisibility(View.GONE); 812 view.findViewById(R.id.folder_icon).setVisibility(View.VISIBLE); 813 ((ImageView)view.findViewById(R.id.folder_icon)).setImageDrawable( 814 Utility.FolderProperties.getInstance(context).getSummaryMailboxIconIds(id)); 815 } 816 817 private void bindAccountItem(View view, Context context, Cursor cursor, boolean isExpanded) 818 { 819 // Reset the view (in case it was recycled) and prepare for binding 820 AccountFolderListItem itemView = (AccountFolderListItem) view; 821 itemView.bindViewInit(this, true); 822 itemView.mAccountId = cursor.getLong(Account.CONTENT_ID_COLUMN); 823 824 long accountId = cursor.getLong(Account.CONTENT_ID_COLUMN); 825 View chipView = view.findViewById(R.id.chip); 826 chipView.setBackgroundResource(Email.getAccountColorResourceId(accountId)); 827 chipView.setVisibility(View.VISIBLE); 828 829 String text = cursor.getString(Account.CONTENT_DISPLAY_NAME_COLUMN); 830 if (text != null) { 831 TextView descriptionView = (TextView) view.findViewById(R.id.name); 832 descriptionView.setText(text); 833 } 834 835 text = cursor.getString(Account.CONTENT_EMAIL_ADDRESS_COLUMN); 836 if (text != null) { 837 TextView emailView = (TextView) view.findViewById(R.id.status); 838 emailView.setText(text); 839 emailView.setVisibility(View.VISIBLE); 840 } 841 842 int unreadMessageCount = 0; 843 Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI, 844 MAILBOX_UNREAD_COUNT_PROJECTION, 845 MAILBOX_INBOX_SELECTION, 846 new String[] { String.valueOf(accountId) }, null); 847 848 try { 849 if (c.moveToFirst()) { 850 String count = c.getString(MAILBOX_UNREAD_COUNT_COLUMN_UNREAD_COUNT); 851 if (count != null) { 852 unreadMessageCount = Integer.valueOf(count); 853 } 854 } 855 } finally { 856 c.close(); 857 } 858 859 view.findViewById(R.id.all_message_count).setVisibility(View.GONE); 860 TextView unreadCountView = (TextView) view.findViewById(R.id.new_message_count); 861 if (unreadMessageCount > 0) { 862 unreadCountView.setText(String.valueOf(unreadMessageCount)); 863 unreadCountView.setVisibility(View.VISIBLE); 864 } else { 865 unreadCountView.setVisibility(View.GONE); 866 } 867 868 view.findViewById(R.id.folder_icon).setVisibility(View.GONE); 869 view.findViewById(R.id.folder_button).setVisibility(View.VISIBLE); 870 view.findViewById(R.id.folder_separator).setVisibility(View.VISIBLE); 871 if (accountId == mDefaultAccountId) { 872 view.findViewById(R.id.default_sender).setVisibility(View.VISIBLE); 873 } else { 874 view.findViewById(R.id.default_sender).setVisibility(View.GONE); 875 } 876 } 877 878 @Override 879 public View newView(Context context, Cursor cursor, ViewGroup parent) { 880 return mInflater.inflate(R.layout.account_folder_list_item, parent, false); 881 } 882 883 /* 884 * The following series of overrides insert the "Accounts" separator 885 */ 886 887 /** 888 * Prevents the separator view from recycling into the other views 889 */ 890 @Override 891 public int getItemViewType(int position) { 892 if (position == mSeparatorPosition) { 893 return IGNORE_ITEM_VIEW_TYPE; 894 } 895 return super.getItemViewType(position); 896 } 897 898 /** 899 * Injects the separator view when required, and fudges the cursor for other views 900 */ 901 @Override 902 public View getView(int position, View convertView, ViewGroup parent) { 903 // The base class's getView() checks for mDataValid at the beginning, but we don't have 904 // to do that, because if the cursor is invalid getCount() returns 0, in which case this 905 // method wouldn't get called. 906 907 // Handle the separator here - create & bind 908 if (position == mSeparatorPosition) { 909 TextView view; 910 view = (TextView) mInflater.inflate(R.layout.list_separator, parent, false); 911 view.setText(R.string.account_folder_list_separator_accounts); 912 return view; 913 } 914 return super.getView(getRealPosition(position), convertView, parent); 915 } 916 917 /** 918 * Forces navigation to skip over the separator 919 */ 920 @Override 921 public boolean areAllItemsEnabled() { 922 return false; 923 } 924 925 /** 926 * Forces navigation to skip over the separator 927 */ 928 @Override 929 public boolean isEnabled(int position) { 930 if (position == mSeparatorPosition) { 931 return false; 932 } else if (isAccount(position)) { 933 Long id = ((MergeCursor)getItem(position)).getLong(Account.CONTENT_ID_COLUMN); 934 return !isOnDeletingAccountView(id); 935 } else { 936 return true; 937 } 938 } 939 940 /** 941 * Adjusts list count to include separator 942 */ 943 @Override 944 public int getCount() { 945 int count = super.getCount(); 946 if (count > 0 && (mSeparatorPosition != ListView.INVALID_POSITION)) { 947 // Increment for separator, if we have anything to show. 948 count += 1; 949 } 950 return count; 951 } 952 953 /** 954 * Converts list position to cursor position 955 */ 956 private int getRealPosition(int pos) { 957 if (mSeparatorPosition == ListView.INVALID_POSITION) { 958 // No separator, identity map 959 return pos; 960 } else if (pos <= mSeparatorPosition) { 961 // Before or at the separator, identity map 962 return pos; 963 } else { 964 // After the separator, remove 1 from the pos to get the real underlying pos 965 return pos - 1; 966 } 967 } 968 969 /** 970 * Returns the item using external position numbering (no separator) 971 */ 972 @Override 973 public Object getItem(int pos) { 974 return super.getItem(getRealPosition(pos)); 975 } 976 977 /** 978 * Returns the item id using external position numbering (no separator) 979 */ 980 @Override 981 public long getItemId(int pos) { 982 return super.getItemId(getRealPosition(pos)); 983 } 984 } 985} 986 987 988