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.MatrixCursor.RowBuilder; 48import android.database.MergeCursor; 49import android.net.Uri; 50import android.os.AsyncTask; 51import android.os.Bundle; 52import android.os.Handler; 53import android.view.ContextMenu; 54import android.view.ContextMenu.ContextMenuInfo; 55import android.view.KeyEvent; 56import android.view.LayoutInflater; 57import android.view.Menu; 58import android.view.MenuItem; 59import android.view.View; 60import android.view.ViewGroup; 61import android.view.Window; 62import android.widget.AdapterView; 63import android.widget.AdapterView.OnItemClickListener; 64import android.widget.CursorAdapter; 65import android.widget.ImageView; 66import android.widget.ListAdapter; 67import android.widget.ListView; 68import android.widget.ProgressBar; 69import android.widget.TextView; 70import android.widget.Toast; 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 final int[] SECRET_KEY_CODES = { 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 Cursor c1 = null; 345 Cursor c2 = null; 346 Long defaultAccount = null; 347 if (!isCancelled()) { 348 // Create the summaries cursor 349 c1 = getSummaryChildCursor(); 350 } 351 352 if (!isCancelled()) { 353 // TODO use a custom projection and don't have to sample all of these columns 354 c2 = getContentResolver().query( 355 EmailContent.Account.CONTENT_URI, 356 EmailContent.Account.CONTENT_PROJECTION, null, null, null); 357 } 358 359 if (!isCancelled()) { 360 defaultAccount = Account.getDefaultAccountId(AccountFolderList.this); 361 } 362 363 if (isCancelled()) { 364 if (c1 != null) c1.close(); 365 if (c2 != null) c2.close(); 366 return null; 367 } 368 return new Object[] { c1, c2 , defaultAccount}; 369 } 370 371 @Override 372 protected void onPostExecute(Object[] params) { 373 if (isCancelled() || params == null) { 374 if (params != null) { 375 Cursor c1 = (Cursor)params[0]; 376 if (c1 != null) { 377 c1.close(); 378 } 379 Cursor c2 = (Cursor)params[1]; 380 if (c2 != null) { 381 c2.close(); 382 } 383 } 384 return; 385 } 386 // Before writing a new list adapter into the listview, we need to 387 // shut down the old one (if any). 388 ListAdapter oldAdapter = mListView.getAdapter(); 389 if (oldAdapter != null && oldAdapter instanceof CursorAdapter) { 390 ((CursorAdapter)oldAdapter).changeCursor(null); 391 } 392 // Now create a new list adapter and install it 393 mListAdapter = AccountsAdapter.getInstance((Cursor)params[0], (Cursor)params[1], 394 AccountFolderList.this, (Long)params[2]); 395 mListView.setAdapter(mListAdapter); 396 } 397 } 398 399 private class DeleteAccountTask extends AsyncTask<Void, Void, Void> { 400 private final long mAccountId; 401 private final String mAccountUri; 402 403 public DeleteAccountTask(long accountId, String accountUri) { 404 mAccountId = accountId; 405 mAccountUri = accountUri; 406 } 407 408 @Override 409 protected Void doInBackground(Void... params) { 410 try { 411 // Delete Remote store at first. 412 Store.getInstance(mAccountUri, getApplication(), null).delete(); 413 // Remove the Store instance from cache. 414 Store.removeInstance(mAccountUri); 415 Uri uri = ContentUris.withAppendedId( 416 EmailContent.Account.CONTENT_URI, mAccountId); 417 AccountFolderList.this.getContentResolver().delete(uri, null, null); 418 // Update the backup (side copy) of the accounts 419 AccountBackupRestore.backupAccounts(AccountFolderList.this); 420 // Release or relax device administration, if relevant 421 SecurityPolicy.getInstance(AccountFolderList.this).reducePolicies(); 422 } catch (Exception e) { 423 // Ignore 424 } 425 Email.setServicesEnabled(AccountFolderList.this); 426 return null; 427 } 428 429 @Override 430 protected void onPostExecute(Void v) { 431 if (!isCancelled()) { 432 updateAccounts(); 433 } 434 } 435 } 436 437 private void updateAccounts() { 438 Utility.cancelTaskInterrupt(mLoadAccountsTask); 439 mLoadAccountsTask = (LoadAccountsTask) new LoadAccountsTask().execute(); 440 } 441 442 private void onAddNewAccount() { 443 AccountSetupBasics.actionNewAccount(this); 444 } 445 446 private void onEditAccount(long accountId) { 447 AccountSettings.actionSettings(this, accountId); 448 } 449 450 /** 451 * Refresh one or all accounts 452 * @param accountId A specific id to refresh folders only, or -1 to refresh everything 453 */ 454 private void onRefresh(long accountId) { 455 if (accountId == -1) { 456 // TODO implement a suitable "Refresh all accounts" / "check mail" comment in Controller 457 // TODO this is temp 458 Toast.makeText(this, getString(R.string.account_folder_list_refresh_toast), 459 Toast.LENGTH_LONG).show(); 460 } else { 461 mHandler.progress(true); 462 Controller.getInstance(getApplication()).updateMailboxList( 463 accountId, mControllerCallback); 464 } 465 } 466 467 private void onCompose(long accountId) { 468 if (accountId == -1) { 469 accountId = Account.getDefaultAccountId(this); 470 } 471 if (accountId != -1) { 472 MessageCompose.actionCompose(this, accountId); 473 } else { 474 onAddNewAccount(); 475 } 476 } 477 478 private void onDeleteAccount(long accountId) { 479 mSelectedContextAccount = Account.restoreAccountWithId(this, accountId); 480 showDialog(DIALOG_REMOVE_ACCOUNT); 481 } 482 483 @Override 484 public Dialog onCreateDialog(int id) { 485 switch (id) { 486 case DIALOG_REMOVE_ACCOUNT: 487 return createRemoveAccountDialog(); 488 } 489 return super.onCreateDialog(id); 490 } 491 492 private Dialog createRemoveAccountDialog() { 493 return new AlertDialog.Builder(this) 494 .setIcon(android.R.drawable.ic_dialog_alert) 495 .setTitle(R.string.account_delete_dlg_title) 496 .setMessage(getString(R.string.account_delete_dlg_instructions_fmt, 497 mSelectedContextAccount.getDisplayName())) 498 .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() { 499 public void onClick(DialogInterface dialog, int whichButton) { 500 dismissDialog(DIALOG_REMOVE_ACCOUNT); 501 // Clear notifications, which may become stale here 502 NotificationManager notificationManager = (NotificationManager) 503 getSystemService(Context.NOTIFICATION_SERVICE); 504 notificationManager.cancel(MailService.NOTIFICATION_ID_NEW_MESSAGES); 505 int numAccounts = EmailContent.count(AccountFolderList.this, 506 Account.CONTENT_URI, null, null); 507 mListAdapter.addOnDeletingAccount(mSelectedContextAccount.mId); 508 509 mDeleteAccountTask = (DeleteAccountTask) new DeleteAccountTask( 510 mSelectedContextAccount.mId, 511 mSelectedContextAccount.getStoreUri(AccountFolderList.this)).execute(); 512 if (numAccounts == 1) { 513 AccountSetupBasics.actionNewAccount(AccountFolderList.this); 514 finish(); 515 } 516 } 517 }) 518 .setNegativeButton(R.string.cancel_action, new DialogInterface.OnClickListener() { 519 public void onClick(DialogInterface dialog, int whichButton) { 520 dismissDialog(DIALOG_REMOVE_ACCOUNT); 521 } 522 }) 523 .create(); 524 } 525 526 /** 527 * Update a cached dialog with current values (e.g. account name) 528 */ 529 @Override 530 public void onPrepareDialog(int id, Dialog dialog) { 531 switch (id) { 532 case DIALOG_REMOVE_ACCOUNT: 533 AlertDialog alert = (AlertDialog) dialog; 534 alert.setMessage(getString(R.string.account_delete_dlg_instructions_fmt, 535 mSelectedContextAccount.getDisplayName())); 536 } 537 } 538 539 @Override 540 public boolean onContextItemSelected(MenuItem item) { 541 AdapterView.AdapterContextMenuInfo menuInfo = 542 (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 543 544 if (mListAdapter.isMailbox(menuInfo.position)) { 545 Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position); 546 long id = c.getLong(MAILBOX_COLUMN_ID); 547 switch (item.getItemId()) { 548 case R.id.open_folder: 549 MessageList.actionHandleMailbox(this, id); 550 break; 551 case R.id.check_mail: 552 onRefresh(-1); 553 break; 554 } 555 return false; 556 } else if (mListAdapter.isAccount(menuInfo.position)) { 557 Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position); 558 long accountId = c.getLong(Account.CONTENT_ID_COLUMN); 559 switch (item.getItemId()) { 560 case R.id.open_folder: 561 MailboxList.actionHandleAccount(this, accountId); 562 break; 563 case R.id.compose: 564 onCompose(accountId); 565 break; 566 case R.id.refresh_account: 567 onRefresh(accountId); 568 break; 569 case R.id.edit_account: 570 onEditAccount(accountId); 571 break; 572 case R.id.delete_account: 573 onDeleteAccount(accountId); 574 break; 575 } 576 return true; 577 } 578 return false; 579 } 580 581 @Override 582 public boolean onOptionsItemSelected(MenuItem item) { 583 switch (item.getItemId()) { 584 case R.id.add_new_account: 585 onAddNewAccount(); 586 break; 587 case R.id.check_mail: 588 onRefresh(-1); 589 break; 590 case R.id.compose: 591 onCompose(-1); 592 break; 593 default: 594 return super.onOptionsItemSelected(item); 595 } 596 return true; 597 } 598 599 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { 600 return true; 601 } 602 603 @Override 604 public boolean onCreateOptionsMenu(Menu menu) { 605 super.onCreateOptionsMenu(menu); 606 getMenuInflater().inflate(R.menu.account_folder_list_option, menu); 607 return true; 608 } 609 610 @Override 611 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo info) { 612 super.onCreateContextMenu(menu, v, info); 613 AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) info; 614 if (mListAdapter.isMailbox(menuInfo.position)) { 615 Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position); 616 String displayName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN); 617 menu.setHeaderTitle(displayName); 618 getMenuInflater().inflate(R.menu.account_folder_list_smart_folder_context, menu); 619 } else if (mListAdapter.isAccount(menuInfo.position)) { 620 Cursor c = (Cursor) mListView.getItemAtPosition(menuInfo.position); 621 String accountName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN); 622 menu.setHeaderTitle(accountName); 623 getMenuInflater().inflate(R.menu.account_folder_list_context, menu); 624 } 625 } 626 627 @Override 628 public boolean onKeyDown(int keyCode, KeyEvent event) { 629 if (event.getKeyCode() == SECRET_KEY_CODES[mSecretKeyCodeIndex]) { 630 mSecretKeyCodeIndex++; 631 if (mSecretKeyCodeIndex == SECRET_KEY_CODES.length) { 632 mSecretKeyCodeIndex = 0; 633 startActivity(new Intent(this, Debug.class)); 634 } 635 } else { 636 mSecretKeyCodeIndex = 0; 637 } 638 return super.onKeyDown(keyCode, event); 639 } 640 641 /** 642 * Handler for UI-thread operations (when called from callbacks or any other threads) 643 */ 644 private class MessageListHandler extends Handler { 645 private static final int MSG_PROGRESS = 1; 646 647 @Override 648 public void handleMessage(android.os.Message msg) { 649 switch (msg.what) { 650 case MSG_PROGRESS: 651 boolean showProgress = (msg.arg1 != 0); 652 if (showProgress) { 653 mProgressIcon.setVisibility(View.VISIBLE); 654 } else { 655 mProgressIcon.setVisibility(View.GONE); 656 } 657 break; 658 default: 659 super.handleMessage(msg); 660 } 661 } 662 663 /** 664 * Call from any thread to start/stop progress indicator(s) 665 * @param progress true to start, false to stop 666 */ 667 public void progress(boolean progress) { 668 android.os.Message msg = android.os.Message.obtain(); 669 msg.what = MSG_PROGRESS; 670 msg.arg1 = progress ? 1 : 0; 671 sendMessage(msg); 672 } 673 } 674 675 /** 676 * Callback for async Controller results. 677 */ 678 private class ControllerResults implements Controller.Result { 679 public void updateMailboxListCallback(MessagingException result, long accountKey, 680 int progress) { 681 updateProgress(result, progress); 682 } 683 684 public void updateMailboxCallback(MessagingException result, long accountKey, 685 long mailboxKey, int progress, int numNewMessages) { 686 if (result != null || progress == 100) { 687 Email.updateMailboxRefreshTime(mailboxKey); 688 } 689 if (progress == 100) { 690 updateAccounts(); 691 } 692 updateProgress(result, progress); 693 } 694 695 public void loadMessageForViewCallback(MessagingException result, long messageId, 696 int progress) { 697 } 698 699 public void loadAttachmentCallback(MessagingException result, long messageId, 700 long attachmentId, int progress) { 701 } 702 703 public void serviceCheckMailCallback(MessagingException result, long accountId, 704 long mailboxId, int progress, long tag) { 705 updateProgress(result, progress); 706 } 707 708 public void sendMailCallback(MessagingException result, long accountId, long messageId, 709 int progress) { 710 if (progress == 100) { 711 updateAccounts(); 712 } 713 } 714 715 private void updateProgress(MessagingException result, int progress) { 716 if (result != null || progress == 100) { 717 mHandler.progress(false); 718 } else if (progress == 0) { 719 mHandler.progress(true); 720 } 721 } 722 } 723 724 /* package */ static class AccountsAdapter extends CursorAdapter { 725 726 private final Context mContext; 727 private final LayoutInflater mInflater; 728 private final int mMailboxesCount; 729 private final int mSeparatorPosition; 730 private final long mDefaultAccountId; 731 private final ArrayList<Long> mOnDeletingAccounts = new ArrayList<Long>(); 732 733 public static AccountsAdapter getInstance(Cursor mailboxesCursor, Cursor accountsCursor, 734 Context context, long defaultAccountId) { 735 Cursor[] cursors = new Cursor[] { mailboxesCursor, accountsCursor }; 736 Cursor mc = new MergeCursor(cursors); 737 return new AccountsAdapter(mc, context, mailboxesCursor.getCount(), defaultAccountId); 738 } 739 740 public AccountsAdapter(Cursor c, Context context, int mailboxesCount, 741 long defaultAccountId) { 742 super(context, c, true); 743 mContext = context; 744 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 745 mMailboxesCount = mailboxesCount; 746 mSeparatorPosition = mailboxesCount; 747 mDefaultAccountId = defaultAccountId; 748 } 749 750 public boolean isMailbox(int position) { 751 return position < mMailboxesCount; 752 } 753 754 public boolean isAccount(int position) { 755 return position >= mMailboxesCount; 756 } 757 758 public void addOnDeletingAccount(long accountId) { 759 mOnDeletingAccounts.add(accountId); 760 } 761 762 public boolean isOnDeletingAccountView(long accountId) { 763 return mOnDeletingAccounts.contains(accountId); 764 } 765 766 /** 767 * This is used as a callback from the list items, for clicks in the folder "button" 768 * 769 * @param itemView the item in which the click occurred 770 */ 771 public void onClickFolder(AccountFolderListItem itemView) { 772 MailboxList.actionHandleAccount(mContext, itemView.mAccountId); 773 } 774 775 @Override 776 public void bindView(View view, Context context, Cursor cursor) { 777 if (cursor.getPosition() < mMailboxesCount) { 778 bindMailboxItem(view, context, cursor, false); 779 } else { 780 bindAccountItem(view, context, cursor, false); 781 } 782 } 783 784 private void bindMailboxItem(View view, Context context, Cursor cursor, boolean isLastChild) 785 { 786 // Reset the view (in case it was recycled) and prepare for binding 787 AccountFolderListItem itemView = (AccountFolderListItem) view; 788 itemView.bindViewInit(this, false); 789 790 // Invisible (not "gone") to maintain spacing 791 view.findViewById(R.id.chip).setVisibility(View.INVISIBLE); 792 793 String text = cursor.getString(MAILBOX_DISPLAY_NAME); 794 if (text != null) { 795 TextView nameView = (TextView) view.findViewById(R.id.name); 796 nameView.setText(text); 797 } 798 799 // TODO get/track live folder status 800 text = null; 801 TextView statusView = (TextView) view.findViewById(R.id.status); 802 if (text != null) { 803 statusView.setText(text); 804 statusView.setVisibility(View.VISIBLE); 805 } else { 806 statusView.setVisibility(View.GONE); 807 } 808 809 int count = -1; 810 text = cursor.getString(MAILBOX_UNREAD_COUNT); 811 if (text != null) { 812 count = Integer.valueOf(text); 813 } 814 TextView unreadCountView = (TextView) view.findViewById(R.id.new_message_count); 815 TextView allCountView = (TextView) view.findViewById(R.id.all_message_count); 816 int id = cursor.getInt(MAILBOX_COLUMN_ID); 817 // If the unread count is zero, not to show countView. 818 if (count > 0) { 819 if (id == Mailbox.QUERY_ALL_FAVORITES 820 || id == Mailbox.QUERY_ALL_DRAFTS 821 || id == Mailbox.QUERY_ALL_OUTBOX) { 822 unreadCountView.setVisibility(View.GONE); 823 allCountView.setVisibility(View.VISIBLE); 824 allCountView.setText(text); 825 } else { 826 allCountView.setVisibility(View.GONE); 827 unreadCountView.setVisibility(View.VISIBLE); 828 unreadCountView.setText(text); 829 } 830 } else { 831 allCountView.setVisibility(View.GONE); 832 unreadCountView.setVisibility(View.GONE); 833 } 834 835 view.findViewById(R.id.folder_button).setVisibility(View.GONE); 836 view.findViewById(R.id.folder_separator).setVisibility(View.GONE); 837 view.findViewById(R.id.default_sender).setVisibility(View.GONE); 838 view.findViewById(R.id.folder_icon).setVisibility(View.VISIBLE); 839 ((ImageView)view.findViewById(R.id.folder_icon)).setImageDrawable( 840 Utility.FolderProperties.getInstance(context).getSummaryMailboxIconIds(id)); 841 } 842 843 private void bindAccountItem(View view, Context context, Cursor cursor, boolean isExpanded) 844 { 845 // Reset the view (in case it was recycled) and prepare for binding 846 AccountFolderListItem itemView = (AccountFolderListItem) view; 847 itemView.bindViewInit(this, true); 848 itemView.mAccountId = cursor.getLong(Account.CONTENT_ID_COLUMN); 849 850 long accountId = cursor.getLong(Account.CONTENT_ID_COLUMN); 851 View chipView = view.findViewById(R.id.chip); 852 chipView.setBackgroundResource(Email.getAccountColorResourceId(accountId)); 853 chipView.setVisibility(View.VISIBLE); 854 855 String text = cursor.getString(Account.CONTENT_DISPLAY_NAME_COLUMN); 856 if (text != null) { 857 TextView descriptionView = (TextView) view.findViewById(R.id.name); 858 descriptionView.setText(text); 859 } 860 861 text = cursor.getString(Account.CONTENT_EMAIL_ADDRESS_COLUMN); 862 if (text != null) { 863 TextView emailView = (TextView) view.findViewById(R.id.status); 864 emailView.setText(text); 865 emailView.setVisibility(View.VISIBLE); 866 } 867 868 int unreadMessageCount = 0; 869 Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI, 870 MAILBOX_UNREAD_COUNT_PROJECTION, 871 MAILBOX_INBOX_SELECTION, 872 new String[] { String.valueOf(accountId) }, null); 873 874 try { 875 if (c.moveToFirst()) { 876 String count = c.getString(MAILBOX_UNREAD_COUNT_COLUMN_UNREAD_COUNT); 877 if (count != null) { 878 unreadMessageCount = Integer.valueOf(count); 879 } 880 } 881 } finally { 882 c.close(); 883 } 884 885 view.findViewById(R.id.all_message_count).setVisibility(View.GONE); 886 TextView unreadCountView = (TextView) view.findViewById(R.id.new_message_count); 887 if (unreadMessageCount > 0) { 888 unreadCountView.setText(String.valueOf(unreadMessageCount)); 889 unreadCountView.setVisibility(View.VISIBLE); 890 } else { 891 unreadCountView.setVisibility(View.GONE); 892 } 893 894 view.findViewById(R.id.folder_icon).setVisibility(View.GONE); 895 view.findViewById(R.id.folder_button).setVisibility(View.VISIBLE); 896 view.findViewById(R.id.folder_separator).setVisibility(View.VISIBLE); 897 if (accountId == mDefaultAccountId) { 898 view.findViewById(R.id.default_sender).setVisibility(View.VISIBLE); 899 } else { 900 view.findViewById(R.id.default_sender).setVisibility(View.GONE); 901 } 902 } 903 904 @Override 905 public View newView(Context context, Cursor cursor, ViewGroup parent) { 906 return mInflater.inflate(R.layout.account_folder_list_item, parent, false); 907 } 908 909 /* 910 * The following series of overrides insert the "Accounts" separator 911 */ 912 913 /** 914 * Prevents the separator view from recycling into the other views 915 */ 916 @Override 917 public int getItemViewType(int position) { 918 if (position == mSeparatorPosition) { 919 return IGNORE_ITEM_VIEW_TYPE; 920 } 921 return super.getItemViewType(position); 922 } 923 924 /** 925 * Injects the separator view when required, and fudges the cursor for other views 926 */ 927 @Override 928 public View getView(int position, View convertView, ViewGroup parent) { 929 // The base class's getView() checks for mDataValid at the beginning, but we don't have 930 // to do that, because if the cursor is invalid getCount() returns 0, in which case this 931 // method wouldn't get called. 932 933 // Handle the separator here - create & bind 934 if (position == mSeparatorPosition) { 935 TextView view; 936 view = (TextView) mInflater.inflate(R.layout.list_separator, parent, false); 937 view.setText(R.string.account_folder_list_separator_accounts); 938 return view; 939 } 940 return super.getView(getRealPosition(position), convertView, parent); 941 } 942 943 /** 944 * Forces navigation to skip over the separator 945 */ 946 @Override 947 public boolean areAllItemsEnabled() { 948 return false; 949 } 950 951 /** 952 * Forces navigation to skip over the separator 953 */ 954 @Override 955 public boolean isEnabled(int position) { 956 if (position == mSeparatorPosition) { 957 return false; 958 } else if (isAccount(position)) { 959 Long id = ((MergeCursor)getItem(position)).getLong(Account.CONTENT_ID_COLUMN); 960 return !isOnDeletingAccountView(id); 961 } else { 962 return true; 963 } 964 } 965 966 /** 967 * Adjusts list count to include separator 968 */ 969 @Override 970 public int getCount() { 971 int count = super.getCount(); 972 if (count > 0 && (mSeparatorPosition != ListView.INVALID_POSITION)) { 973 // Increment for separator, if we have anything to show. 974 count += 1; 975 } 976 return count; 977 } 978 979 /** 980 * Converts list position to cursor position 981 */ 982 private int getRealPosition(int pos) { 983 if (mSeparatorPosition == ListView.INVALID_POSITION) { 984 // No separator, identity map 985 return pos; 986 } else if (pos <= mSeparatorPosition) { 987 // Before or at the separator, identity map 988 return pos; 989 } else { 990 // After the separator, remove 1 from the pos to get the real underlying pos 991 return pos - 1; 992 } 993 } 994 995 /** 996 * Returns the item using external position numbering (no separator) 997 */ 998 @Override 999 public Object getItem(int pos) { 1000 return super.getItem(getRealPosition(pos)); 1001 } 1002 1003 /** 1004 * Returns the item id using external position numbering (no separator) 1005 */ 1006 @Override 1007 public long getItemId(int pos) { 1008 return super.getItemId(getRealPosition(pos)); 1009 } 1010 } 1011} 1012 1013 1014