Home | History | Annotate | Download | only in activity
      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.AccountSecurity;
     24 import com.android.email.activity.setup.AccountSettings;
     25 import com.android.email.mail.AuthenticationFailedException;
     26 import com.android.email.mail.CertificateValidationException;
     27 import com.android.email.mail.MessagingException;
     28 import com.android.email.provider.EmailContent;
     29 import com.android.email.provider.EmailContent.Account;
     30 import com.android.email.provider.EmailContent.AccountColumns;
     31 import com.android.email.provider.EmailContent.Mailbox;
     32 import com.android.email.provider.EmailContent.MailboxColumns;
     33 import com.android.email.provider.EmailContent.Message;
     34 import com.android.email.provider.EmailContent.MessageColumns;
     35 import com.android.email.service.MailService;
     36 
     37 import android.app.ListActivity;
     38 import android.app.NotificationManager;
     39 import android.content.ContentResolver;
     40 import android.content.ContentUris;
     41 import android.content.Context;
     42 import android.content.Intent;
     43 import android.content.res.ColorStateList;
     44 import android.content.res.Resources;
     45 import android.content.res.TypedArray;
     46 import android.content.res.Resources.Theme;
     47 import android.database.Cursor;
     48 import android.graphics.Typeface;
     49 import android.graphics.drawable.Drawable;
     50 import android.net.Uri;
     51 import android.os.AsyncTask;
     52 import android.os.Bundle;
     53 import android.os.Handler;
     54 import android.os.SystemClock;
     55 import android.util.Log;
     56 import android.view.ContextMenu;
     57 import android.view.LayoutInflater;
     58 import android.view.Menu;
     59 import android.view.MenuItem;
     60 import android.view.View;
     61 import android.view.ViewGroup;
     62 import android.view.ContextMenu.ContextMenuInfo;
     63 import android.view.View.OnClickListener;
     64 import android.view.animation.Animation;
     65 import android.view.animation.AnimationUtils;
     66 import android.view.animation.Animation.AnimationListener;
     67 import android.widget.AdapterView;
     68 import android.widget.Button;
     69 import android.widget.CursorAdapter;
     70 import android.widget.ImageView;
     71 import android.widget.ListView;
     72 import android.widget.ProgressBar;
     73 import android.widget.TextView;
     74 import android.widget.Toast;
     75 import android.widget.AdapterView.OnItemClickListener;
     76 
     77 import java.util.Date;
     78 import java.util.HashSet;
     79 import java.util.Set;
     80 import java.util.Timer;
     81 import java.util.TimerTask;
     82 
     83 public class MessageList extends ListActivity implements OnItemClickListener, OnClickListener,
     84         AnimationListener {
     85     // Intent extras (internal to this activity)
     86     private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID";
     87     private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE";
     88     private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID";
     89     private static final String STATE_SELECTED_ITEM_TOP =
     90         "com.android.email.activity.MessageList.selectedItemTop";
     91     private static final String STATE_SELECTED_POSITION =
     92         "com.android.email.activity.MessageList.selectedPosition";
     93     private static final String STATE_CHECKED_ITEMS =
     94         "com.android.email.activity.MessageList.checkedItems";
     95 
     96     private static final int REQUEST_SECURITY = 0;
     97 
     98     // UI support
     99     private ListView mListView;
    100     private View mMultiSelectPanel;
    101     private Button mReadUnreadButton;
    102     private Button mFavoriteButton;
    103     private Button mDeleteButton;
    104     private View mListFooterView;
    105     private TextView mListFooterText;
    106     private View mListFooterProgress;
    107     private TextView mErrorBanner;
    108 
    109     private static final int LIST_FOOTER_MODE_NONE = 0;
    110     private static final int LIST_FOOTER_MODE_REFRESH = 1;
    111     private static final int LIST_FOOTER_MODE_MORE = 2;
    112     private static final int LIST_FOOTER_MODE_SEND = 3;
    113     private int mListFooterMode;
    114 
    115     private MessageListAdapter mListAdapter;
    116     private MessageListHandler mHandler;
    117     private final Controller mController = Controller.getInstance(getApplication());
    118     private ControllerResults mControllerCallback;
    119 
    120     private TextView mLeftTitle;
    121     private ProgressBar mProgressIcon;
    122 
    123     // DB access
    124     private ContentResolver mResolver;
    125     private long mMailboxId;
    126     private LoadMessagesTask mLoadMessagesTask;
    127     private FindMailboxTask mFindMailboxTask;
    128     private SetTitleTask mSetTitleTask;
    129     private SetFooterTask mSetFooterTask;
    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_NAME_COLUMN_ID = 0;
    136     private static final int MAILBOX_NAME_COLUMN_ACCOUNT_KEY = 1;
    137     private static final int MAILBOX_NAME_COLUMN_TYPE = 2;
    138     private static final String[] MAILBOX_NAME_PROJECTION = new String[] {
    139             MailboxColumns.DISPLAY_NAME, MailboxColumns.ACCOUNT_KEY,
    140             MailboxColumns.TYPE};
    141 
    142     private static final int ACCOUNT_DISPLAY_NAME_COLUMN_ID = 0;
    143     private static final String[] ACCOUNT_NAME_PROJECTION = new String[] {
    144             AccountColumns.DISPLAY_NAME };
    145 
    146     private static final int ACCOUNT_INFO_COLUMN_FLAGS = 0;
    147     private static final String[] ACCOUNT_INFO_PROJECTION = new String[] {
    148             AccountColumns.FLAGS };
    149 
    150     private static final String ID_SELECTION = EmailContent.RECORD_ID + "=?";
    151 
    152     private Boolean mPushModeMailbox = null;
    153     private int mSavedItemTop = 0;
    154     private int mSavedItemPosition = -1;
    155     private int mFirstSelectedItemTop = 0;
    156     private int mFirstSelectedItemPosition = -1;
    157     private int mFirstSelectedItemHeight = -1;
    158     private boolean mCanAutoRefresh = false;
    159 
    160     /* package */ static final String[] MESSAGE_PROJECTION = new String[] {
    161         EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
    162         MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP,
    163         MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT,
    164         MessageColumns.FLAGS,
    165     };
    166 
    167     /**
    168      * Open a specific mailbox.
    169      *
    170      * TODO This should just shortcut to a more generic version that can accept a list of
    171      * accounts/mailboxes (e.g. merged inboxes).
    172      *
    173      * @param context
    174      * @param id mailbox key
    175      */
    176     public static void actionHandleMailbox(Context context, long id) {
    177         context.startActivity(createIntent(context, -1, id, -1));
    178     }
    179 
    180     /**
    181      * Open a specific mailbox by account & type
    182      *
    183      * @param context The caller's context (for generating an intent)
    184      * @param accountId The account to open
    185      * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX)
    186      */
    187     public static void actionHandleAccount(Context context, long accountId, int mailboxType) {
    188         context.startActivity(createIntent(context, accountId, -1, mailboxType));
    189     }
    190 
    191     /**
    192      * Open the inbox of the account with a UUID.  It's used to handle old style
    193      * (Android <= 1.6) desktop shortcut intents.
    194      */
    195     public static void actionOpenAccountInboxUuid(Context context, String accountUuid) {
    196         Intent i = createIntent(context, -1, -1, Mailbox.TYPE_INBOX);
    197         i.setData(Account.getShortcutSafeUriFromUuid(accountUuid));
    198         context.startActivity(i);
    199     }
    200 
    201     /**
    202      * Return an intent to open a specific mailbox by account & type.
    203      *
    204      * @param context The caller's context (for generating an intent)
    205      * @param accountId The account to open, or -1
    206      * @param mailboxId the ID of the mailbox to open, or -1
    207      * @param mailboxType the type of mailbox to open (e.g. @see Mailbox.TYPE_INBOX) or -1
    208      */
    209     public static Intent createIntent(Context context, long accountId, long mailboxId,
    210             int mailboxType) {
    211         Intent intent = new Intent(context, MessageList.class);
    212         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    213         if (accountId != -1) intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
    214         if (mailboxId != -1) intent.putExtra(EXTRA_MAILBOX_ID, mailboxId);
    215         if (mailboxType != -1) intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType);
    216         return intent;
    217     }
    218 
    219     /**
    220      * Create and return an intent for a desktop shortcut for an account.
    221      *
    222      * @param context Calling context for building the intent
    223      * @param account The account of interest
    224      * @param mailboxType The folder name to open (typically Mailbox.TYPE_INBOX)
    225      * @return an Intent which can be used to view that account
    226      */
    227     public static Intent createAccountIntentForShortcut(Context context, Account account,
    228             int mailboxType) {
    229         Intent i = createIntent(context, -1, -1, mailboxType);
    230         i.setData(account.getShortcutSafeUri());
    231         return i;
    232     }
    233 
    234     @Override
    235     public void onCreate(Bundle icicle) {
    236         super.onCreate(icicle);
    237         setContentView(R.layout.message_list);
    238 
    239         mHandler = new MessageListHandler();
    240         mControllerCallback = new ControllerResults();
    241         mCanAutoRefresh = true;
    242         mListView = getListView();
    243         mMultiSelectPanel = findViewById(R.id.footer_organize);
    244         mReadUnreadButton = (Button) findViewById(R.id.btn_read_unread);
    245         mFavoriteButton = (Button) findViewById(R.id.btn_multi_favorite);
    246         mDeleteButton = (Button) findViewById(R.id.btn_multi_delete);
    247         mLeftTitle = (TextView) findViewById(R.id.title_left_text);
    248         mProgressIcon = (ProgressBar) findViewById(R.id.title_progress_icon);
    249         mErrorBanner = (TextView) findViewById(R.id.connection_error_text);
    250 
    251         mReadUnreadButton.setOnClickListener(this);
    252         mFavoriteButton.setOnClickListener(this);
    253         mDeleteButton.setOnClickListener(this);
    254         ((Button) findViewById(R.id.account_title_button)).setOnClickListener(this);
    255 
    256         mListView.setOnItemClickListener(this);
    257         mListView.setItemsCanFocus(false);
    258         registerForContextMenu(mListView);
    259 
    260         mListAdapter = new MessageListAdapter(this);
    261         setListAdapter(mListAdapter);
    262 
    263         mResolver = getContentResolver();
    264 
    265         // TODO extend this to properly deal with multiple mailboxes, cursor, etc.
    266 
    267         // Show the appropriate account/mailbox specified by an {@link Intent}.
    268         selectAccountAndMailbox(getIntent());
    269     }
    270 
    271     /**
    272      * Show the appropriate account/mailbox specified by an {@link Intent}.
    273      */
    274     private void selectAccountAndMailbox(Intent intent) {
    275         mMailboxId = intent.getLongExtra(EXTRA_MAILBOX_ID, -1);
    276         if (mMailboxId != -1) {
    277             // Specific mailbox ID was provided - go directly to it
    278             mSetTitleTask = new SetTitleTask(mMailboxId);
    279             mSetTitleTask.execute();
    280             mLoadMessagesTask = new LoadMessagesTask(mMailboxId, -1);
    281             mLoadMessagesTask.execute();
    282             addFooterView(mMailboxId, -1, -1);
    283         } else {
    284             int mailboxType = intent.getIntExtra(EXTRA_MAILBOX_TYPE, Mailbox.TYPE_INBOX);
    285             Uri uri = intent.getData();
    286             // TODO Possible ANR.  getAccountIdFromShortcutSafeUri accesses DB.
    287             long accountId = (uri == null) ? -1
    288                     : Account.getAccountIdFromShortcutSafeUri(this, uri);
    289 
    290             if (accountId != -1) {
    291                 // A content URI was provided - try to look up the account
    292                 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false);
    293                 mFindMailboxTask.execute();
    294             } else {
    295                 // Go by account id + type
    296                 accountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1);
    297                 mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, true);
    298                 mFindMailboxTask.execute();
    299             }
    300             addFooterView(-1, accountId, mailboxType);
    301         }
    302         // TODO set title to "account > mailbox (#unread)"
    303     }
    304 
    305     @Override
    306     public void onPause() {
    307         super.onPause();
    308         mController.removeResultCallback(mControllerCallback);
    309     }
    310 
    311     @Override
    312     public void onResume() {
    313         super.onResume();
    314         mController.addResultCallback(mControllerCallback);
    315 
    316         // clear notifications here
    317         NotificationManager notificationManager = (NotificationManager)
    318                 getSystemService(Context.NOTIFICATION_SERVICE);
    319         notificationManager.cancel(MailService.NOTIFICATION_ID_NEW_MESSAGES);
    320 
    321         // Exit immediately if the accounts list has changed (e.g. externally deleted)
    322         if (Email.getNotifyUiAccountsChanged()) {
    323             Welcome.actionStart(this);
    324             finish();
    325             return;
    326         }
    327 
    328         restoreListPosition();
    329         autoRefreshStaleMailbox();
    330     }
    331 
    332     @Override
    333     protected void onDestroy() {
    334         super.onDestroy();
    335 
    336         Utility.cancelTaskInterrupt(mLoadMessagesTask);
    337         mLoadMessagesTask = null;
    338         Utility.cancelTaskInterrupt(mFindMailboxTask);
    339         mFindMailboxTask = null;
    340         Utility.cancelTaskInterrupt(mSetTitleTask);
    341         mSetTitleTask = null;
    342         Utility.cancelTaskInterrupt(mSetFooterTask);
    343         mSetFooterTask = null;
    344 
    345         mListAdapter.changeCursor(null);
    346     }
    347 
    348     @Override
    349     protected void onSaveInstanceState(Bundle outState) {
    350         super.onSaveInstanceState(outState);
    351         saveListPosition();
    352         outState.putInt(STATE_SELECTED_POSITION, mSavedItemPosition);
    353         outState.putInt(STATE_SELECTED_ITEM_TOP, mSavedItemTop);
    354         Set<Long> checkedset = mListAdapter.getSelectedSet();
    355         long[] checkedarray = new long[checkedset.size()];
    356         int i = 0;
    357         for (Long l : checkedset) {
    358             checkedarray[i] = l;
    359             i++;
    360         }
    361         outState.putLongArray(STATE_CHECKED_ITEMS, checkedarray);
    362     }
    363 
    364     @Override
    365     protected void onRestoreInstanceState(Bundle savedInstanceState) {
    366         super.onRestoreInstanceState(savedInstanceState);
    367         mSavedItemTop = savedInstanceState.getInt(STATE_SELECTED_ITEM_TOP, 0);
    368         mSavedItemPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION, -1);
    369         Set<Long> checkedset = mListAdapter.getSelectedSet();
    370         for (long l: savedInstanceState.getLongArray(STATE_CHECKED_ITEMS)) {
    371             checkedset.add(l);
    372         }
    373     }
    374 
    375     private void saveListPosition() {
    376         mSavedItemPosition = getListView().getSelectedItemPosition();
    377         if (mSavedItemPosition >= 0 && getListView().isSelected()) {
    378             mSavedItemTop = getListView().getSelectedView().getTop();
    379         } else {
    380             mSavedItemPosition = getListView().getFirstVisiblePosition();
    381             if (mSavedItemPosition >= 0) {
    382                 mSavedItemTop = 0;
    383                 View topChild = getListView().getChildAt(0);
    384                 if (topChild != null) {
    385                     mSavedItemTop = topChild.getTop();
    386                 }
    387             }
    388         }
    389     }
    390 
    391     private void restoreListPosition() {
    392         if (mSavedItemPosition >= 0 && mSavedItemPosition < getListView().getCount()) {
    393             getListView().setSelectionFromTop(mSavedItemPosition, mSavedItemTop);
    394             mSavedItemPosition = -1;
    395             mSavedItemTop = 0;
    396         }
    397     }
    398 
    399     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    400         if (view != mListFooterView) {
    401             MessageListItem itemView = (MessageListItem) view;
    402             onOpenMessage(id, itemView.mMailboxId);
    403         } else {
    404             doFooterClick();
    405         }
    406     }
    407 
    408     public void onClick(View v) {
    409         switch (v.getId()) {
    410             case R.id.btn_read_unread:
    411                 onMultiToggleRead(mListAdapter.getSelectedSet());
    412                 break;
    413             case R.id.btn_multi_favorite:
    414                 onMultiToggleFavorite(mListAdapter.getSelectedSet());
    415                 break;
    416             case R.id.btn_multi_delete:
    417                 onMultiDelete(mListAdapter.getSelectedSet());
    418                 break;
    419             case R.id.account_title_button:
    420                 onAccounts();
    421                 break;
    422         }
    423     }
    424 
    425     public void onAnimationEnd(Animation animation) {
    426         updateListPosition();
    427     }
    428 
    429     public void onAnimationRepeat(Animation animation) {
    430     }
    431 
    432     public void onAnimationStart(Animation animation) {
    433     }
    434 
    435     @Override
    436     public boolean onCreateOptionsMenu(Menu menu) {
    437         super.onCreateOptionsMenu(menu);
    438         if (mMailboxId < 0) {
    439             getMenuInflater().inflate(R.menu.message_list_option_smart_folder, menu);
    440         } else {
    441             getMenuInflater().inflate(R.menu.message_list_option, menu);
    442         }
    443         return true;
    444     }
    445 
    446     @Override
    447     public boolean onPrepareOptionsMenu(Menu menu) {
    448         boolean showDeselect = mListAdapter.getSelectedSet().size() > 0;
    449         menu.setGroupVisible(R.id.deselect_all_group, showDeselect);
    450         return true;
    451     }
    452 
    453     @Override
    454     public boolean onOptionsItemSelected(MenuItem item) {
    455         switch (item.getItemId()) {
    456             case R.id.refresh:
    457                 onRefresh();
    458                 return true;
    459             case R.id.folders:
    460                 onFolders();
    461                 return true;
    462             case R.id.accounts:
    463                 onAccounts();
    464                 return true;
    465             case R.id.compose:
    466                 onCompose();
    467                 return true;
    468             case R.id.account_settings:
    469                 onEditAccount();
    470                 return true;
    471             case R.id.deselect_all:
    472                 onDeselectAll();
    473                 return true;
    474             default:
    475                 return super.onOptionsItemSelected(item);
    476         }
    477     }
    478 
    479     @Override
    480     public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
    481         super.onCreateContextMenu(menu, v, menuInfo);
    482 
    483         AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
    484         // There is no context menu for the list footer
    485         if (info.targetView == mListFooterView) {
    486             return;
    487         }
    488         MessageListItem itemView = (MessageListItem) info.targetView;
    489 
    490         Cursor c = (Cursor) mListView.getItemAtPosition(info.position);
    491         String messageName = c.getString(MessageListAdapter.COLUMN_SUBJECT);
    492 
    493         menu.setHeaderTitle(messageName);
    494 
    495         // TODO: There is probably a special context menu for the trash
    496         Mailbox mailbox = Mailbox.restoreMailboxWithId(this, itemView.mMailboxId);
    497         if (mailbox == null) {
    498             return;
    499         }
    500 
    501         switch (mailbox.mType) {
    502             case EmailContent.Mailbox.TYPE_DRAFTS:
    503                 getMenuInflater().inflate(R.menu.message_list_context_drafts, menu);
    504                 break;
    505             case EmailContent.Mailbox.TYPE_OUTBOX:
    506                 getMenuInflater().inflate(R.menu.message_list_context_outbox, menu);
    507                 break;
    508             case EmailContent.Mailbox.TYPE_TRASH:
    509                 getMenuInflater().inflate(R.menu.message_list_context_trash, menu);
    510                 break;
    511             default:
    512                 getMenuInflater().inflate(R.menu.message_list_context, menu);
    513                 // The default menu contains "mark as read".  If the message is read, change
    514                 // the menu text to "mark as unread."
    515                 if (itemView.mRead) {
    516                     menu.findItem(R.id.mark_as_read).setTitle(R.string.mark_as_unread_action);
    517                 }
    518                 break;
    519         }
    520     }
    521 
    522     @Override
    523     public boolean onContextItemSelected(MenuItem item) {
    524         AdapterView.AdapterContextMenuInfo info =
    525             (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
    526         MessageListItem itemView = (MessageListItem) info.targetView;
    527 
    528         switch (item.getItemId()) {
    529             case R.id.open:
    530                 onOpenMessage(info.id, itemView.mMailboxId);
    531                 break;
    532             case R.id.delete:
    533                 onDelete(info.id, itemView.mAccountId);
    534                 break;
    535             case R.id.reply:
    536                 onReply(itemView.mMessageId);
    537                 break;
    538             case R.id.reply_all:
    539                 onReplyAll(itemView.mMessageId);
    540                 break;
    541             case R.id.forward:
    542                 onForward(itemView.mMessageId);
    543                 break;
    544             case R.id.mark_as_read:
    545                 onSetMessageRead(info.id, !itemView.mRead);
    546                 break;
    547         }
    548         return super.onContextItemSelected(item);
    549     }
    550 
    551     private void onRefresh() {
    552         // TODO: Should not be reading from DB in UI thread - need a cleaner way to get accountId
    553         if (mMailboxId >= 0) {
    554             Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mMailboxId);
    555             if (mailbox != null) {
    556                 mController.updateMailbox(mailbox.mAccountKey, mMailboxId, mControllerCallback);
    557             }
    558         }
    559     }
    560 
    561     private void onFolders() {
    562         if (mMailboxId >= 0) {
    563             // TODO smaller projection
    564             Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mMailboxId);
    565             if (mailbox != null) {
    566                 MailboxList.actionHandleAccount(this, mailbox.mAccountKey);
    567                 finish();
    568             }
    569         }
    570     }
    571 
    572     private void onAccounts() {
    573         AccountFolderList.actionShowAccounts(this);
    574         finish();
    575     }
    576 
    577     private long lookupAccountIdFromMailboxId(long mailboxId) {
    578         // TODO: Select correct account to send from when there are multiple mailboxes
    579         // TODO: Should not be reading from DB in UI thread
    580         if (mailboxId < 0) {
    581             return -1; // no info, default account
    582         }
    583         EmailContent.Mailbox mailbox =
    584             EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId);
    585         if (mailbox == null) {
    586             return -2;
    587         }
    588         return mailbox.mAccountKey;
    589     }
    590 
    591     private void onCompose() {
    592         long accountKey = lookupAccountIdFromMailboxId(mMailboxId);
    593         if (accountKey > -2) {
    594             MessageCompose.actionCompose(this, accountKey);
    595         } else {
    596             finish();
    597         }
    598     }
    599 
    600     private void onEditAccount() {
    601         long accountKey = lookupAccountIdFromMailboxId(mMailboxId);
    602         if (accountKey > -2) {
    603             AccountSettings.actionSettings(this, accountKey);
    604         } else {
    605             finish();
    606         }
    607     }
    608 
    609     private void onDeselectAll() {
    610         mListAdapter.getSelectedSet().clear();
    611         mListView.invalidateViews();
    612         showMultiPanel(false);
    613     }
    614 
    615     private void onOpenMessage(long messageId, long mailboxId) {
    616         // TODO: Should not be reading from DB in UI thread
    617         EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId);
    618         if (mailbox == null) {
    619             return;
    620         }
    621 
    622         if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) {
    623             MessageCompose.actionEditDraft(this, messageId);
    624         } else {
    625             final boolean disableReply = (mailbox.mType == EmailContent.Mailbox.TYPE_TRASH);
    626             // WARNING: here we pass mMailboxId, which can be the negative id of a compound
    627             // mailbox, instead of the mailboxId of the particular message that is opened
    628             MessageView.actionView(this, messageId, mMailboxId, disableReply);
    629         }
    630     }
    631 
    632     private void onReply(long messageId) {
    633         MessageCompose.actionReply(this, messageId, false);
    634     }
    635 
    636     private void onReplyAll(long messageId) {
    637         MessageCompose.actionReply(this, messageId, true);
    638     }
    639 
    640     private void onForward(long messageId) {
    641         MessageCompose.actionForward(this, messageId);
    642     }
    643 
    644     private void onLoadMoreMessages() {
    645         if (mMailboxId >= 0) {
    646             mController.loadMoreMessages(mMailboxId, mControllerCallback);
    647         }
    648     }
    649 
    650     private void onSendPendingMessages() {
    651         if (mMailboxId == Mailbox.QUERY_ALL_OUTBOX) {
    652             // For the combined Outbox, we loop through all accounts and send the messages
    653             Cursor c = mResolver.query(Account.CONTENT_URI, Account.ID_PROJECTION,
    654                     null, null, null);
    655             try {
    656                 while (c.moveToNext()) {
    657                     long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
    658                     mController.sendPendingMessages(accountId, mControllerCallback);
    659                 }
    660             } finally {
    661                 c.close();
    662             }
    663         } else {
    664             long accountKey = lookupAccountIdFromMailboxId(mMailboxId);
    665             if (accountKey > -2) {
    666                 mController.sendPendingMessages(accountKey, mControllerCallback);
    667             } else {
    668                 finish();
    669             }
    670         }
    671     }
    672 
    673     private void onDelete(long messageId, long accountId) {
    674         mController.deleteMessage(messageId, accountId);
    675         Toast.makeText(this, getResources().getQuantityString(
    676                 R.plurals.message_deleted_toast, 1), Toast.LENGTH_SHORT).show();
    677     }
    678 
    679     private void onSetMessageRead(long messageId, boolean newRead) {
    680         mController.setMessageRead(messageId, newRead);
    681     }
    682 
    683     private void onSetMessageFavorite(long messageId, boolean newFavorite) {
    684         mController.setMessageFavorite(messageId, newFavorite);
    685     }
    686 
    687     /**
    688      * Toggles a set read/unread states.  Note, the default behavior is "mark unread", so the
    689      * sense of the helper methods is "true=unread".
    690      *
    691      * @param selectedSet The current list of selected items
    692      */
    693     private void onMultiToggleRead(Set<Long> selectedSet) {
    694         toggleMultiple(selectedSet, new MultiToggleHelper() {
    695 
    696             public boolean getField(long messageId, Cursor c) {
    697                 return c.getInt(MessageListAdapter.COLUMN_READ) == 0;
    698             }
    699 
    700             public boolean setField(long messageId, Cursor c, boolean newValue) {
    701                 boolean oldValue = getField(messageId, c);
    702                 if (oldValue != newValue) {
    703                     onSetMessageRead(messageId, !newValue);
    704                     return true;
    705                 }
    706                 return false;
    707             }
    708         });
    709     }
    710 
    711     /**
    712      * Toggles a set of favorites (stars)
    713      *
    714      * @param selectedSet The current list of selected items
    715      */
    716     private void onMultiToggleFavorite(Set<Long> selectedSet) {
    717         toggleMultiple(selectedSet, new MultiToggleHelper() {
    718 
    719             public boolean getField(long messageId, Cursor c) {
    720                 return c.getInt(MessageListAdapter.COLUMN_FAVORITE) != 0;
    721             }
    722 
    723             public boolean setField(long messageId, Cursor c, boolean newValue) {
    724                 boolean oldValue = getField(messageId, c);
    725                 if (oldValue != newValue) {
    726                     onSetMessageFavorite(messageId, newValue);
    727                     return true;
    728                 }
    729                 return false;
    730             }
    731         });
    732     }
    733 
    734     private void onMultiDelete(Set<Long> selectedSet) {
    735         // Clone the set, because deleting is going to thrash things
    736         HashSet<Long> cloneSet = new HashSet<Long>(selectedSet);
    737         for (Long id : cloneSet) {
    738             mController.deleteMessage(id, -1);
    739         }
    740         Toast.makeText(this, getResources().getQuantityString(
    741                 R.plurals.message_deleted_toast, cloneSet.size()), Toast.LENGTH_SHORT).show();
    742         selectedSet.clear();
    743         showMultiPanel(false);
    744     }
    745 
    746     private interface MultiToggleHelper {
    747         /**
    748          * Return true if the field of interest is "set".  If one or more are false, then our
    749          * bulk action will be to "set".  If all are set, our bulk action will be to "clear".
    750          * @param messageId the message id of the current message
    751          * @param c the cursor, positioned to the item of interest
    752          * @return true if the field at this row is "set"
    753          */
    754         public boolean getField(long messageId, Cursor c);
    755 
    756         /**
    757          * Set or clear the field of interest.  Return true if a change was made.
    758          * @param messageId the message id of the current message
    759          * @param c the cursor, positioned to the item of interest
    760          * @param newValue the new value to be set at this row
    761          * @return true if a change was actually made
    762          */
    763         public boolean setField(long messageId, Cursor c, boolean newValue);
    764     }
    765 
    766     /**
    767      * Toggle multiple fields in a message, using the following logic:  If one or more fields
    768      * are "clear", then "set" them.  If all fields are "set", then "clear" them all.
    769      *
    770      * @param selectedSet the set of messages that are selected
    771      * @param helper functions to implement the specific getter & setter
    772      * @return the number of messages that were updated
    773      */
    774     private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) {
    775         Cursor c = mListAdapter.getCursor();
    776         boolean anyWereFound = false;
    777         boolean allWereSet = true;
    778 
    779         c.moveToPosition(-1);
    780         while (c.moveToNext()) {
    781             long id = c.getInt(MessageListAdapter.COLUMN_ID);
    782             if (selectedSet.contains(Long.valueOf(id))) {
    783                 anyWereFound = true;
    784                 if (!helper.getField(id, c)) {
    785                     allWereSet = false;
    786                     break;
    787                 }
    788             }
    789         }
    790 
    791         int numChanged = 0;
    792 
    793         if (anyWereFound) {
    794             boolean newValue = !allWereSet;
    795             c.moveToPosition(-1);
    796             while (c.moveToNext()) {
    797                 long id = c.getInt(MessageListAdapter.COLUMN_ID);
    798                 if (selectedSet.contains(Long.valueOf(id))) {
    799                     if (helper.setField(id, c, newValue)) {
    800                         ++numChanged;
    801                     }
    802                 }
    803             }
    804         }
    805 
    806         return numChanged;
    807     }
    808 
    809     /**
    810      * Test selected messages for showing appropriate labels
    811      * @param selectedSet
    812      * @param column_id
    813      * @param defaultflag
    814      * @return true when the specified flagged message is selected
    815      */
    816     private boolean testMultiple(Set<Long> selectedSet, int column_id, boolean defaultflag) {
    817         Cursor c = mListAdapter.getCursor();
    818         if (c == null || c.isClosed()) {
    819             return false;
    820         }
    821         c.moveToPosition(-1);
    822         while (c.moveToNext()) {
    823             long id = c.getInt(MessageListAdapter.COLUMN_ID);
    824             if (selectedSet.contains(Long.valueOf(id))) {
    825                 if (c.getInt(column_id) == (defaultflag? 1 : 0)) {
    826                     return true;
    827                 }
    828             }
    829         }
    830         return false;
    831     }
    832 
    833     /**
    834      * Implements a timed refresh of "stale" mailboxes.  This should only happen when
    835      * multiple conditions are true, including:
    836      *   Only when the user explicitly opens the mailbox (not onResume, for example)
    837      *   Only for real, non-push mailboxes
    838      *   Only when the mailbox is "stale" (currently set to 5 minutes since last refresh)
    839      */
    840     private void autoRefreshStaleMailbox() {
    841         if (!mCanAutoRefresh
    842                 || (mListAdapter.getCursor() == null) // Check if messages info is loaded
    843                 || (mPushModeMailbox != null && mPushModeMailbox) // Check the push mode
    844                 || (mMailboxId < 0)) { // Check if this mailbox is synthetic/combined
    845             return;
    846         }
    847         mCanAutoRefresh = false;
    848         if (!Email.mailboxRequiresRefresh(mMailboxId)) {
    849             return;
    850         }
    851         onRefresh();
    852     }
    853 
    854     private void updateFooterButtonNames () {
    855         // Show "unread_action" when one or more read messages are selected.
    856         if (testMultiple(mListAdapter.getSelectedSet(), MessageListAdapter.COLUMN_READ, true)) {
    857             mReadUnreadButton.setText(R.string.unread_action);
    858         } else {
    859             mReadUnreadButton.setText(R.string.read_action);
    860         }
    861         // Show "set_star_action" when one or more un-starred messages are selected.
    862         if (testMultiple(mListAdapter.getSelectedSet(),
    863                 MessageListAdapter.COLUMN_FAVORITE, false)) {
    864             mFavoriteButton.setText(R.string.set_star_action);
    865         } else {
    866             mFavoriteButton.setText(R.string.remove_star_action);
    867         }
    868     }
    869 
    870     private void updateListPosition () {
    871         int listViewHeight = getListView().getHeight();
    872         if (mListAdapter.getSelectedSet().size() == 1 && mFirstSelectedItemPosition >= 0
    873                 && mFirstSelectedItemPosition < getListView().getCount()
    874                 && listViewHeight < mFirstSelectedItemTop) {
    875             getListView().setSelectionFromTop(mFirstSelectedItemPosition,
    876                     listViewHeight - mFirstSelectedItemHeight);
    877         }
    878     }
    879 
    880     /**
    881      * Show or hide the panel of multi-select options
    882      */
    883     private void showMultiPanel(boolean show) {
    884         if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) {
    885             mMultiSelectPanel.setVisibility(View.VISIBLE);
    886             Animation animation = AnimationUtils.loadAnimation(this, R.anim.footer_appear);
    887             animation.setAnimationListener(this);
    888             mMultiSelectPanel.startAnimation(animation);
    889         } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) {
    890             mMultiSelectPanel.setVisibility(View.GONE);
    891             mMultiSelectPanel.startAnimation(
    892                         AnimationUtils.loadAnimation(this, R.anim.footer_disappear));
    893         }
    894         if (show) {
    895             updateFooterButtonNames();
    896         }
    897     }
    898 
    899     /**
    900      * Add the fixed footer view if appropriate (not always - not all accounts & mailboxes).
    901      *
    902      * Here are some rules (finish this list):
    903      *
    904      * Any merged, synced box (except send):  refresh
    905      * Any push-mode account:  refresh
    906      * Any non-push-mode account:  load more
    907      * Any outbox (send again):
    908      *
    909      * @param mailboxId the ID of the mailbox
    910      * @param accountId the ID of the account
    911      * @param mailboxType {@code Mailbox.TYPE_} constant, or -1
    912      */
    913     private void addFooterView(long mailboxId, long accountId, int mailboxType) {
    914         // first, look for shortcuts that don't need us to spin up a DB access task
    915         if (mailboxId == Mailbox.QUERY_ALL_INBOXES
    916                 || mailboxId == Mailbox.QUERY_ALL_UNREAD
    917                 || mailboxId == Mailbox.QUERY_ALL_FAVORITES) {
    918             finishFooterView(LIST_FOOTER_MODE_REFRESH);
    919             return;
    920         }
    921         if (mailboxId == Mailbox.QUERY_ALL_DRAFTS || mailboxType == Mailbox.TYPE_DRAFTS) {
    922             finishFooterView(LIST_FOOTER_MODE_NONE);
    923             return;
    924         }
    925         if (mailboxId == Mailbox.QUERY_ALL_OUTBOX || mailboxType == Mailbox.TYPE_OUTBOX) {
    926             finishFooterView(LIST_FOOTER_MODE_SEND);
    927             return;
    928         }
    929 
    930         // We don't know enough to select the footer command type (yet), so we'll
    931         // launch an async task to do the remaining lookups and decide what to do
    932         mSetFooterTask = new SetFooterTask();
    933         mSetFooterTask.execute(mailboxId, accountId);
    934     }
    935 
    936     private final static String[] MAILBOX_ACCOUNT_AND_TYPE_PROJECTION =
    937         new String[] { MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE };
    938 
    939     private class SetFooterTask extends AsyncTask<Long, Void, Integer> {
    940         /**
    941          * There are two operational modes here, requiring different lookup.
    942          * mailboxIs != -1:  A specific mailbox - check its type, then look up its account
    943          * accountId != -1:  A specific account - look up the account
    944          */
    945         @Override
    946         protected Integer doInBackground(Long... params) {
    947             long mailboxId = params[0];
    948             long accountId = params[1];
    949             int mailboxType = -1;
    950             if (mailboxId != -1) {
    951                 try {
    952                     Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
    953                     Cursor c = mResolver.query(uri, MAILBOX_ACCOUNT_AND_TYPE_PROJECTION,
    954                             null, null, null);
    955                     if (c.moveToFirst()) {
    956                         try {
    957                             accountId = c.getLong(0);
    958                             mailboxType = c.getInt(1);
    959                         } finally {
    960                             c.close();
    961                         }
    962                     }
    963                 } catch (IllegalArgumentException iae) {
    964                     // can't do any more here
    965                     return LIST_FOOTER_MODE_NONE;
    966                 }
    967             }
    968             switch (mailboxType) {
    969                 case Mailbox.TYPE_OUTBOX:
    970                     return LIST_FOOTER_MODE_SEND;
    971                 case Mailbox.TYPE_DRAFTS:
    972                     return LIST_FOOTER_MODE_NONE;
    973             }
    974             if (accountId != -1) {
    975                 // This is inefficient but the best fix is not here but in isMessagingController
    976                 Account account = Account.restoreAccountWithId(MessageList.this, accountId);
    977                 if (account != null) {
    978                     mPushModeMailbox = account.mSyncInterval == Account.CHECK_INTERVAL_PUSH;
    979                     if (MessageList.this.mController.isMessagingController(account)) {
    980                         return LIST_FOOTER_MODE_MORE;       // IMAP or POP
    981                     } else {
    982                         return LIST_FOOTER_MODE_NONE;    // EAS
    983                     }
    984                 }
    985             }
    986             return LIST_FOOTER_MODE_NONE;
    987         }
    988 
    989         @Override
    990         protected void onPostExecute(Integer listFooterMode) {
    991             if (listFooterMode == null) {
    992                 return;
    993             }
    994             finishFooterView(listFooterMode);
    995         }
    996     }
    997 
    998     /**
    999      * Add the fixed footer view as specified, and set up the test as well.
   1000      *
   1001      * @param listFooterMode the footer mode we've determined should be used for this list
   1002      */
   1003     private void finishFooterView(int listFooterMode) {
   1004         mListFooterMode = listFooterMode;
   1005         if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
   1006             mListFooterView = ((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE))
   1007                     .inflate(R.layout.message_list_item_footer, mListView, false);
   1008             getListView().addFooterView(mListFooterView);
   1009             setListAdapter(mListAdapter);
   1010 
   1011             mListFooterProgress = mListFooterView.findViewById(R.id.progress);
   1012             mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text);
   1013             setListFooterText(false);
   1014         }
   1015     }
   1016 
   1017     /**
   1018      * Set the list footer text based on mode and "active" status
   1019      */
   1020     private void setListFooterText(boolean active) {
   1021         if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
   1022             int footerTextId = 0;
   1023             switch (mListFooterMode) {
   1024                 case LIST_FOOTER_MODE_REFRESH:
   1025                     footerTextId = active ? R.string.status_loading_more
   1026                                           : R.string.refresh_action;
   1027                     break;
   1028                 case LIST_FOOTER_MODE_MORE:
   1029                     footerTextId = active ? R.string.status_loading_more
   1030                                           : R.string.message_list_load_more_messages_action;
   1031                     break;
   1032                 case LIST_FOOTER_MODE_SEND:
   1033                     footerTextId = active ? R.string.status_sending_messages
   1034                                           : R.string.message_list_send_pending_messages_action;
   1035                     break;
   1036             }
   1037             mListFooterText.setText(footerTextId);
   1038         }
   1039     }
   1040 
   1041     /**
   1042      * Handle a click in the list footer, which changes meaning depending on what we're looking at.
   1043      */
   1044     private void doFooterClick() {
   1045         switch (mListFooterMode) {
   1046             case LIST_FOOTER_MODE_NONE:         // should never happen
   1047                 break;
   1048             case LIST_FOOTER_MODE_REFRESH:
   1049                 onRefresh();
   1050                 break;
   1051             case LIST_FOOTER_MODE_MORE:
   1052                 onLoadMoreMessages();
   1053                 break;
   1054             case LIST_FOOTER_MODE_SEND:
   1055                 onSendPendingMessages();
   1056                 break;
   1057         }
   1058     }
   1059 
   1060     /**
   1061      * Async task for finding a single mailbox by type (possibly even going to the network).
   1062      *
   1063      * This is much too complex, as implemented.  It uses this AsyncTask to check for a mailbox,
   1064      * then (if not found) a Controller call to refresh mailboxes from the server, and a handler
   1065      * to relaunch this task (a 2nd time) to read the results of the network refresh.  The core
   1066      * problem is that we have two different non-UI-thread jobs (reading DB and reading network)
   1067      * and two different paradigms for dealing with them.  Some unification would be needed here
   1068      * to make this cleaner.
   1069      *
   1070      * TODO: If this problem spreads to other operations, find a cleaner way to handle it.
   1071      */
   1072     private class FindMailboxTask extends AsyncTask<Void, Void, Long> {
   1073 
   1074         private final long mAccountId;
   1075         private final int mMailboxType;
   1076         private final boolean mOkToRecurse;
   1077         private boolean showWelcomeActivity;
   1078         private boolean showSecurityActivity;
   1079 
   1080         /**
   1081          * Special constructor to cache some local info
   1082          */
   1083         public FindMailboxTask(long accountId, int mailboxType, boolean okToRecurse) {
   1084             mAccountId = accountId;
   1085             mMailboxType = mailboxType;
   1086             mOkToRecurse = okToRecurse;
   1087             showWelcomeActivity = false;
   1088             showSecurityActivity = false;
   1089         }
   1090 
   1091         @Override
   1092         protected Long doInBackground(Void... params) {
   1093             // Quick check that account is not in security hold
   1094             if (mAccountId != -1 && isSecurityHold(mAccountId)) {
   1095                 showSecurityActivity = true;
   1096                 return Long.valueOf(-1);
   1097             }
   1098             // See if we can find the requested mailbox in the DB.
   1099             long mailboxId = Mailbox.findMailboxOfType(MessageList.this, mAccountId, mMailboxType);
   1100             if (mailboxId == Mailbox.NO_MAILBOX) {
   1101                 // Mailbox not found.  Does the account really exists?
   1102                 final boolean accountExists = Account.isValidId(MessageList.this, mAccountId);
   1103                 if (accountExists && mOkToRecurse) {
   1104                     // launch network lookup
   1105                     mControllerCallback.presetMailboxListCallback(mMailboxType, mAccountId);
   1106                     mController.updateMailboxList(mAccountId, mControllerCallback);
   1107                 } else {
   1108                     // We don't want to do the network lookup, or the account doesn't exist in the
   1109                     // first place.
   1110                     showWelcomeActivity = true;
   1111                 }
   1112             }
   1113             return mailboxId;
   1114         }
   1115 
   1116         @Override
   1117         protected void onPostExecute(Long mailboxId) {
   1118             if (showSecurityActivity) {
   1119                 // launch the security setup activity
   1120                 Intent i = AccountSecurity.actionUpdateSecurityIntent(
   1121                         MessageList.this, mAccountId);
   1122                 MessageList.this.startActivityForResult(i, REQUEST_SECURITY);
   1123                 return;
   1124             }
   1125             if (showWelcomeActivity) {
   1126                 // Let the Welcome activity show the default screen.
   1127                 Welcome.actionStart(MessageList.this);
   1128                 finish();
   1129                 return;
   1130             }
   1131             if (mailboxId == null || mailboxId == Mailbox.NO_MAILBOX) {
   1132                 return;
   1133             }
   1134             mMailboxId = mailboxId;
   1135             mSetTitleTask = new SetTitleTask(mMailboxId);
   1136             mSetTitleTask.execute();
   1137             mLoadMessagesTask = new LoadMessagesTask(mMailboxId, mAccountId);
   1138             mLoadMessagesTask.execute();
   1139         }
   1140     }
   1141 
   1142     /**
   1143      * Check a single account for security hold status.  Do not call from UI thread.
   1144      */
   1145     private boolean isSecurityHold(long accountId) {
   1146         Cursor c = MessageList.this.getContentResolver().query(
   1147                 ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
   1148                 ACCOUNT_INFO_PROJECTION, null, null, null);
   1149         try {
   1150             if (c.moveToFirst()) {
   1151                 int flags = c.getInt(ACCOUNT_INFO_COLUMN_FLAGS);
   1152                 if ((flags & Account.FLAGS_SECURITY_HOLD) != 0) {
   1153                     return true;
   1154                 }
   1155             }
   1156         } finally {
   1157             c.close();
   1158         }
   1159         return false;
   1160     }
   1161 
   1162     /**
   1163      * Handle the eventual result from the security update activity
   1164      *
   1165      * Note, this is extremely coarse, and it simply returns the user to the Accounts list.
   1166      * Anything more requires refactoring of this Activity.
   1167      */
   1168     @Override
   1169     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
   1170         switch (requestCode) {
   1171             case REQUEST_SECURITY:
   1172                 onAccounts();
   1173         }
   1174         super.onActivityResult(requestCode, resultCode, data);
   1175     }
   1176 
   1177     /**
   1178      * Async task for loading a single folder out of the UI thread
   1179      *
   1180      * The code here (for merged boxes) is a placeholder/hack and should be replaced.  Some
   1181      * specific notes:
   1182      * TODO:  Move the double query into a specialized URI that returns all inbox messages
   1183      * and do the dirty work in raw SQL in the provider.
   1184      * TODO:  Generalize the query generation so we can reuse it in MessageView (for next/prev)
   1185      */
   1186     private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> {
   1187 
   1188         private long mMailboxKey;
   1189         private long mAccountKey;
   1190 
   1191         /**
   1192          * Special constructor to cache some local info
   1193          */
   1194         public LoadMessagesTask(long mailboxKey, long accountKey) {
   1195             mMailboxKey = mailboxKey;
   1196             mAccountKey = accountKey;
   1197         }
   1198 
   1199         @Override
   1200         protected Cursor doInBackground(Void... params) {
   1201             String selection =
   1202                 Utility.buildMailboxIdSelection(MessageList.this.mResolver, mMailboxKey);
   1203             Cursor c = MessageList.this.managedQuery(
   1204                     EmailContent.Message.CONTENT_URI, MESSAGE_PROJECTION,
   1205                     selection, null, EmailContent.MessageColumns.TIMESTAMP + " DESC");
   1206             return c;
   1207         }
   1208 
   1209         @Override
   1210         protected void onPostExecute(Cursor cursor) {
   1211             if (cursor == null || cursor.isClosed()) {
   1212                 return;
   1213             }
   1214             MessageList.this.mListAdapter.changeCursor(cursor);
   1215             // changeCursor occurs the jumping of position in ListView, so it's need to restore
   1216             // the position;
   1217             restoreListPosition();
   1218             autoRefreshStaleMailbox();
   1219             // Reset the "new messages" count in the service, since we're seeing them now
   1220             if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) {
   1221                 MailService.resetNewMessageCount(MessageList.this, -1);
   1222             } else if (mMailboxKey >= 0 && mAccountKey != -1) {
   1223                 MailService.resetNewMessageCount(MessageList.this, mAccountKey);
   1224             }
   1225         }
   1226     }
   1227 
   1228     private class SetTitleTask extends AsyncTask<Void, Void, Object[]> {
   1229 
   1230         private long mMailboxKey;
   1231 
   1232         public SetTitleTask(long mailboxKey) {
   1233             mMailboxKey = mailboxKey;
   1234         }
   1235 
   1236         @Override
   1237         protected Object[] doInBackground(Void... params) {
   1238             // Check special Mailboxes
   1239             int resIdSpecialMailbox = 0;
   1240             if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) {
   1241                 resIdSpecialMailbox = R.string.account_folder_list_summary_inbox;
   1242             } else if (mMailboxKey == Mailbox.QUERY_ALL_FAVORITES) {
   1243                 resIdSpecialMailbox = R.string.account_folder_list_summary_starred;
   1244             } else if (mMailboxKey == Mailbox.QUERY_ALL_DRAFTS) {
   1245                 resIdSpecialMailbox = R.string.account_folder_list_summary_drafts;
   1246             } else if (mMailboxKey == Mailbox.QUERY_ALL_OUTBOX) {
   1247                 resIdSpecialMailbox = R.string.account_folder_list_summary_outbox;
   1248             }
   1249             if (resIdSpecialMailbox != 0) {
   1250                 return new Object[] {null, getString(resIdSpecialMailbox), 0};
   1251             }
   1252 
   1253             String accountName = null;
   1254             String mailboxName = null;
   1255             String accountKey = null;
   1256             Cursor c = MessageList.this.mResolver.query(Mailbox.CONTENT_URI,
   1257                     MAILBOX_NAME_PROJECTION, ID_SELECTION,
   1258                     new String[] { Long.toString(mMailboxKey) }, null);
   1259             try {
   1260                 if (c.moveToFirst()) {
   1261                     mailboxName = Utility.FolderProperties.getInstance(MessageList.this)
   1262                             .getDisplayName(c.getInt(MAILBOX_NAME_COLUMN_TYPE));
   1263                     if (mailboxName == null) {
   1264                         mailboxName = c.getString(MAILBOX_NAME_COLUMN_ID);
   1265                     }
   1266                     accountKey = c.getString(MAILBOX_NAME_COLUMN_ACCOUNT_KEY);
   1267                 }
   1268             } finally {
   1269                 c.close();
   1270             }
   1271             if (accountKey != null) {
   1272                 c = MessageList.this.mResolver.query(Account.CONTENT_URI,
   1273                         ACCOUNT_NAME_PROJECTION, ID_SELECTION, new String[] { accountKey },
   1274                         null);
   1275                 try {
   1276                     if (c.moveToFirst()) {
   1277                         accountName = c.getString(ACCOUNT_DISPLAY_NAME_COLUMN_ID);
   1278                     }
   1279                 } finally {
   1280                     c.close();
   1281                 }
   1282             }
   1283             int nAccounts = EmailContent.count(MessageList.this, Account.CONTENT_URI, null, null);
   1284             return new Object[] {accountName, mailboxName, nAccounts};
   1285         }
   1286 
   1287         @Override
   1288         protected void onPostExecute(Object[] result) {
   1289             if (result == null) {
   1290                 return;
   1291             }
   1292 
   1293             final int nAccounts = (Integer) result[2];
   1294             if (result[0] != null) {
   1295                 setTitleAccountName((String) result[0], nAccounts > 1);
   1296             }
   1297 
   1298             if (result[1] != null) {
   1299                 mLeftTitle.setText((String) result[1]);
   1300             }
   1301         }
   1302     }
   1303 
   1304     private void setTitleAccountName(String accountName, boolean showAccountsButton) {
   1305         TextView accountsButton = (TextView) findViewById(R.id.account_title_button);
   1306         TextView textPlain = (TextView) findViewById(R.id.title_right_text);
   1307         if (showAccountsButton) {
   1308             accountsButton.setVisibility(View.VISIBLE);
   1309             textPlain.setVisibility(View.GONE);
   1310             accountsButton.setText(accountName);
   1311         } else {
   1312             accountsButton.setVisibility(View.GONE);
   1313             textPlain.setVisibility(View.VISIBLE);
   1314             textPlain.setText(accountName);
   1315         }
   1316     }
   1317 
   1318     /**
   1319      * Handler for UI-thread operations (when called from callbacks or any other threads)
   1320      */
   1321     class MessageListHandler extends Handler {
   1322         private static final int MSG_PROGRESS = 1;
   1323         private static final int MSG_LOOKUP_MAILBOX_TYPE = 2;
   1324         private static final int MSG_ERROR_BANNER = 3;
   1325         private static final int MSG_REQUERY_LIST = 4;
   1326 
   1327         @Override
   1328         public void handleMessage(android.os.Message msg) {
   1329             switch (msg.what) {
   1330                 case MSG_PROGRESS:
   1331                     boolean visible = (msg.arg1 != 0);
   1332                     if (visible) {
   1333                         mProgressIcon.setVisibility(View.VISIBLE);
   1334                     } else {
   1335                         mProgressIcon.setVisibility(View.GONE);
   1336                     }
   1337                     if (mListFooterProgress != null) {
   1338                         mListFooterProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
   1339                     }
   1340                     setListFooterText(visible);
   1341                     break;
   1342                 case MSG_LOOKUP_MAILBOX_TYPE:
   1343                     // kill running async task, if any
   1344                     if (mFindMailboxTask != null &&
   1345                             mFindMailboxTask.getStatus() != FindMailboxTask.Status.FINISHED) {
   1346                         mFindMailboxTask.cancel(true);
   1347                         mFindMailboxTask = null;
   1348                     }
   1349                     // start new one.  do not recurse back to controller.
   1350                     long accountId = ((Long)msg.obj).longValue();
   1351                     int mailboxType = msg.arg1;
   1352                     mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false);
   1353                     mFindMailboxTask.execute();
   1354                     break;
   1355                 case MSG_ERROR_BANNER:
   1356                     String message = (String) msg.obj;
   1357                     boolean isVisible = mErrorBanner.getVisibility() == View.VISIBLE;
   1358                     if (message != null) {
   1359                         mErrorBanner.setText(message);
   1360                         if (!isVisible) {
   1361                             mErrorBanner.setVisibility(View.VISIBLE);
   1362                             mErrorBanner.startAnimation(
   1363                                     AnimationUtils.loadAnimation(
   1364                                             MessageList.this, R.anim.header_appear));
   1365                         }
   1366                     } else {
   1367                         if (isVisible) {
   1368                             mErrorBanner.setVisibility(View.GONE);
   1369                             mErrorBanner.startAnimation(
   1370                                     AnimationUtils.loadAnimation(
   1371                                             MessageList.this, R.anim.header_disappear));
   1372                         }
   1373                     }
   1374                     break;
   1375                 case MSG_REQUERY_LIST:
   1376                     mListAdapter.doRequery();
   1377                     if (mMultiSelectPanel.getVisibility() == View.VISIBLE) {
   1378                         updateFooterButtonNames();
   1379                     }
   1380                     break;
   1381                 default:
   1382                     super.handleMessage(msg);
   1383             }
   1384         }
   1385 
   1386         /**
   1387          * Call from any thread to start/stop progress indicator(s)
   1388          * @param progress true to start, false to stop
   1389          */
   1390         public void progress(boolean progress) {
   1391             android.os.Message msg = android.os.Message.obtain();
   1392             msg.what = MSG_PROGRESS;
   1393             msg.arg1 = progress ? 1 : 0;
   1394             sendMessage(msg);
   1395         }
   1396 
   1397         /**
   1398          * Called from any thread to look for a mailbox of a specific type.  This is designed
   1399          * to be called from the Controller's MailboxList callback;  It instructs the async task
   1400          * not to recurse, in case the mailbox is not found after this.
   1401          *
   1402          * See FindMailboxTask for more notes on this handler.
   1403          */
   1404         public void lookupMailboxType(long accountId, int mailboxType) {
   1405             android.os.Message msg = android.os.Message.obtain();
   1406             msg.what = MSG_LOOKUP_MAILBOX_TYPE;
   1407             msg.arg1 = mailboxType;
   1408             msg.obj = Long.valueOf(accountId);
   1409             sendMessage(msg);
   1410         }
   1411 
   1412         /**
   1413          * Called from any thread to show or hide the connection error banner.
   1414          * @param message error text or null to hide the box
   1415          */
   1416         public void showErrorBanner(String message) {
   1417             android.os.Message msg = android.os.Message.obtain();
   1418             msg.what = MSG_ERROR_BANNER;
   1419             msg.obj = message;
   1420             sendMessage(msg);
   1421         }
   1422 
   1423         /**
   1424          * Called from any thread to signal that the list adapter should requery and update.
   1425          */
   1426         public void requeryList() {
   1427             sendEmptyMessage(MSG_REQUERY_LIST);
   1428         }
   1429     }
   1430 
   1431     /**
   1432      * Callback for async Controller results.
   1433      */
   1434     private class ControllerResults implements Controller.Result {
   1435 
   1436         // This is used to alter the connection banner operation for sending messages
   1437         MessagingException mSendMessageException;
   1438 
   1439         // These values are set by FindMailboxTask, and used by updateMailboxListCallback
   1440         // Access to these must be synchronized because of various threads dealing with them
   1441         private int mWaitForMailboxType = -1;
   1442         private long mWaitForMailboxAccount = -1;
   1443 
   1444         public synchronized void presetMailboxListCallback(int mailboxType, long accountId) {
   1445             mWaitForMailboxType = mailboxType;
   1446             mWaitForMailboxAccount = accountId;
   1447         }
   1448 
   1449         public synchronized void updateMailboxListCallback(MessagingException result,
   1450                 long accountKey, int progress) {
   1451             // updateMailboxList is never the end goal in MessageList, so we don't show
   1452             // these errors.  There are a couple of corner cases that we miss reporting, but
   1453             // this is better than reporting a number of non-problem intermediate states.
   1454             // updateBanner(result, progress, mMailboxId);
   1455 
   1456             updateProgress(result, progress);
   1457             if (progress == 100 && accountKey == mWaitForMailboxAccount) {
   1458                 mWaitForMailboxAccount = -1;
   1459                 mHandler.lookupMailboxType(accountKey, mWaitForMailboxType);
   1460             }
   1461         }
   1462 
   1463         // TODO check accountKey and only react to relevant notifications
   1464         public void updateMailboxCallback(MessagingException result, long accountKey,
   1465                 long mailboxKey, int progress, int numNewMessages) {
   1466             updateBanner(result, progress, mailboxKey);
   1467             if (result != null || progress == 100) {
   1468                 Email.updateMailboxRefreshTime(mailboxKey);
   1469             }
   1470             updateProgress(result, progress);
   1471         }
   1472 
   1473         public void loadMessageForViewCallback(MessagingException result, long messageId,
   1474                 int progress) {
   1475         }
   1476 
   1477         public void loadAttachmentCallback(MessagingException result, long messageId,
   1478                 long attachmentId, int progress) {
   1479         }
   1480 
   1481         public void serviceCheckMailCallback(MessagingException result, long accountId,
   1482                 long mailboxId, int progress, long tag) {
   1483         }
   1484 
   1485         /**
   1486          * We alter the updateBanner hysteresis here to capture any failures and handle
   1487          * them just once at the end.  This callback is overly overloaded:
   1488          *  result == null, messageId == -1, progress == 0:     start batch send
   1489          *  result == null, messageId == xx, progress == 0:     start sending one message
   1490          *  result == xxxx, messageId == xx, progress == 0;     failed sending one message
   1491          *  result == null, messageId == -1, progres == 100;    finish sending batch
   1492          */
   1493         public void sendMailCallback(MessagingException result, long accountId, long messageId,
   1494                 int progress) {
   1495             if (mListFooterMode == LIST_FOOTER_MODE_SEND) {
   1496                 // reset captured error when we start sending one or more messages
   1497                 if (messageId == -1 && result == null && progress == 0) {
   1498                     mSendMessageException = null;
   1499                 }
   1500                 // capture first exception that comes along
   1501                 if (result != null && mSendMessageException == null) {
   1502                     mSendMessageException = result;
   1503                 }
   1504                 // if we're completing the sequence, change the banner state
   1505                 if (messageId == -1 && progress == 100) {
   1506                     updateBanner(mSendMessageException, progress, mMailboxId);
   1507                 }
   1508                 // always update the spinner, which has less state to worry about
   1509                 updateProgress(result, progress);
   1510             }
   1511         }
   1512 
   1513         private void updateProgress(MessagingException result, int progress) {
   1514             if (result != null || progress == 100) {
   1515                 mHandler.progress(false);
   1516             } else if (progress == 0) {
   1517                 mHandler.progress(true);
   1518             }
   1519         }
   1520 
   1521         /**
   1522          * Show or hide the connection error banner, and convert the various MessagingException
   1523          * variants into localizable text.  There is hysteresis in the show/hide logic:  Once shown,
   1524          * the banner will remain visible until some progress is made on the connection.  The
   1525          * goal is to keep it from flickering during retries in a bad connection state.
   1526          *
   1527          * @param result
   1528          * @param progress
   1529          */
   1530         private void updateBanner(MessagingException result, int progress, long mailboxKey) {
   1531             if (mailboxKey != mMailboxId) {
   1532                 return;
   1533             }
   1534             if (result != null) {
   1535                 int id = R.string.status_network_error;
   1536                 if (result instanceof AuthenticationFailedException) {
   1537                     id = R.string.account_setup_failed_dlg_auth_message;
   1538                 } else if (result instanceof CertificateValidationException) {
   1539                     id = R.string.account_setup_failed_dlg_certificate_message;
   1540                 } else {
   1541                     switch (result.getExceptionType()) {
   1542                         case MessagingException.IOERROR:
   1543                             id = R.string.account_setup_failed_ioerror;
   1544                             break;
   1545                         case MessagingException.TLS_REQUIRED:
   1546                             id = R.string.account_setup_failed_tls_required;
   1547                             break;
   1548                         case MessagingException.AUTH_REQUIRED:
   1549                             id = R.string.account_setup_failed_auth_required;
   1550                             break;
   1551                         case MessagingException.GENERAL_SECURITY:
   1552                             id = R.string.account_setup_failed_security;
   1553                             break;
   1554                         // TODO Generate a unique string for this case, which is the case
   1555                         // where the security policy needs to be updated.
   1556                         case MessagingException.SECURITY_POLICIES_REQUIRED:
   1557                             id = R.string.account_setup_failed_security;
   1558                             break;
   1559                     }
   1560                 }
   1561                 mHandler.showErrorBanner(getString(id));
   1562             } else if (progress > 0) {
   1563                 mHandler.showErrorBanner(null);
   1564             }
   1565         }
   1566     }
   1567 
   1568     /**
   1569      * This class implements the adapter for displaying messages based on cursors.
   1570      */
   1571     /* package */ class MessageListAdapter extends CursorAdapter {
   1572 
   1573         public static final int COLUMN_ID = 0;
   1574         public static final int COLUMN_MAILBOX_KEY = 1;
   1575         public static final int COLUMN_ACCOUNT_KEY = 2;
   1576         public static final int COLUMN_DISPLAY_NAME = 3;
   1577         public static final int COLUMN_SUBJECT = 4;
   1578         public static final int COLUMN_DATE = 5;
   1579         public static final int COLUMN_READ = 6;
   1580         public static final int COLUMN_FAVORITE = 7;
   1581         public static final int COLUMN_ATTACHMENTS = 8;
   1582         public static final int COLUMN_FLAGS = 9;
   1583 
   1584         Context mContext;
   1585         private LayoutInflater mInflater;
   1586         private Drawable mAttachmentIcon;
   1587         private Drawable mInvitationIcon;
   1588         private Drawable mFavoriteIconOn;
   1589         private Drawable mFavoriteIconOff;
   1590         private Drawable mSelectedIconOn;
   1591         private Drawable mSelectedIconOff;
   1592 
   1593         private ColorStateList mTextColorPrimary;
   1594         private ColorStateList mTextColorSecondary;
   1595 
   1596         // Timer to control the refresh rate of the list
   1597         private final RefreshTimer mRefreshTimer = new RefreshTimer();
   1598         // Last time we allowed a refresh of the list
   1599         private long mLastRefreshTime = 0;
   1600         // How long we want to wait for refreshes (a good starting guess)
   1601         // I suspect this could be lowered down to even 1000 or so, but this seems ok for now
   1602         private static final long REFRESH_INTERVAL_MS = 2500;
   1603 
   1604         private java.text.DateFormat mDateFormat;
   1605         private java.text.DateFormat mTimeFormat;
   1606 
   1607         private HashSet<Long> mChecked = new HashSet<Long>();
   1608 
   1609         public MessageListAdapter(Context context) {
   1610             super(context, null, true);
   1611             mContext = context;
   1612             mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   1613 
   1614             Resources resources = context.getResources();
   1615             mAttachmentIcon = resources.getDrawable(R.drawable.ic_mms_attachment_small);
   1616             mInvitationIcon = resources.getDrawable(R.drawable.ic_calendar_event_small);
   1617             mFavoriteIconOn = resources.getDrawable(R.drawable.btn_star_big_buttonless_dark_on);
   1618             mFavoriteIconOff = resources.getDrawable(R.drawable.btn_star_big_buttonless_dark_off);
   1619             mSelectedIconOn = resources.getDrawable(R.drawable.btn_check_buttonless_dark_on);
   1620             mSelectedIconOff = resources.getDrawable(R.drawable.btn_check_buttonless_dark_off);
   1621 
   1622             Theme theme = context.getTheme();
   1623             TypedArray array;
   1624             array = theme.obtainStyledAttributes(new int[] { android.R.attr.textColorPrimary });
   1625             mTextColorPrimary = resources.getColorStateList(array.getResourceId(0, 0));
   1626             array = theme.obtainStyledAttributes(new int[] { android.R.attr.textColorSecondary });
   1627             mTextColorSecondary = resources.getColorStateList(array.getResourceId(0, 0));
   1628 
   1629             mDateFormat = android.text.format.DateFormat.getDateFormat(context);    // short date
   1630             mTimeFormat = android.text.format.DateFormat.getTimeFormat(context);    // 12/24 time
   1631         }
   1632 
   1633         /**
   1634          * We override onContentChange to throttle the refresh, which can happen way too often
   1635          * on syncing a large list (up to many times per second).  This will prevent ANR's during
   1636          * initial sync and potentially at other times as well.
   1637          */
   1638         @Override
   1639         protected synchronized void onContentChanged() {
   1640             final Cursor cursor = getCursor();
   1641             if (cursor != null && !cursor.isClosed()) {
   1642                 long sinceRefresh = SystemClock.elapsedRealtime() - mLastRefreshTime;
   1643                 mRefreshTimer.schedule(REFRESH_INTERVAL_MS - sinceRefresh);
   1644             }
   1645         }
   1646 
   1647         /**
   1648          * Called in UI thread only, from Handler, to complete the requery that we
   1649          * intercepted in onContentChanged().
   1650          */
   1651         public void doRequery() {
   1652             super.onContentChanged();
   1653         }
   1654 
   1655         class RefreshTimer extends Timer {
   1656             private TimerTask timerTask = null;
   1657 
   1658             protected void clear() {
   1659                 timerTask = null;
   1660             }
   1661 
   1662             protected synchronized void schedule(long delay) {
   1663                 if (timerTask != null) return;
   1664                 if (delay < 0) {
   1665                     refreshList();
   1666                 } else {
   1667                     timerTask = new RefreshTimerTask();
   1668                     schedule(timerTask, delay);
   1669                 }
   1670             }
   1671         }
   1672 
   1673         class RefreshTimerTask extends TimerTask {
   1674             @Override
   1675             public void run() {
   1676                 refreshList();
   1677             }
   1678         }
   1679 
   1680         /**
   1681          * Do the work of requerying the list and notifying the UI of changed data
   1682          * Make sure we call notifyDataSetChanged on the UI thread.
   1683          */
   1684         private synchronized void refreshList() {
   1685             if (Email.LOGD) {
   1686                 Log.d("messageList", "refresh: "
   1687                         + (SystemClock.elapsedRealtime() - mLastRefreshTime) + "ms");
   1688             }
   1689             mHandler.requeryList();
   1690             mLastRefreshTime = SystemClock.elapsedRealtime();
   1691             mRefreshTimer.clear();
   1692         }
   1693 
   1694         public Set<Long> getSelectedSet() {
   1695             return mChecked;
   1696         }
   1697 
   1698         @Override
   1699         public void bindView(View view, Context context, Cursor cursor) {
   1700             // Reset the view (in case it was recycled) and prepare for binding
   1701             MessageListItem itemView = (MessageListItem) view;
   1702             itemView.bindViewInit(this, true);
   1703 
   1704             // Load the public fields in the view (for later use)
   1705             itemView.mMessageId = cursor.getLong(COLUMN_ID);
   1706             itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY);
   1707             itemView.mAccountId = cursor.getLong(COLUMN_ACCOUNT_KEY);
   1708             itemView.mRead = cursor.getInt(COLUMN_READ) != 0;
   1709             itemView.mFavorite = cursor.getInt(COLUMN_FAVORITE) != 0;
   1710             itemView.mSelected = mChecked.contains(Long.valueOf(itemView.mMessageId));
   1711 
   1712             // Load the UI
   1713             View chipView = view.findViewById(R.id.chip);
   1714             chipView.setBackgroundResource(Email.getAccountColorResourceId(itemView.mAccountId));
   1715 
   1716             TextView fromView = (TextView) view.findViewById(R.id.from);
   1717             String text = cursor.getString(COLUMN_DISPLAY_NAME);
   1718             fromView.setText(text);
   1719 
   1720             TextView subjectView = (TextView) view.findViewById(R.id.subject);
   1721             text = cursor.getString(COLUMN_SUBJECT);
   1722             subjectView.setText(text);
   1723 
   1724             boolean hasInvitation =
   1725                         (cursor.getInt(COLUMN_FLAGS) & Message.FLAG_INCOMING_MEETING_INVITE) != 0;
   1726             boolean hasAttachments = cursor.getInt(COLUMN_ATTACHMENTS) != 0;
   1727             Drawable icon =
   1728                     hasInvitation ? mInvitationIcon
   1729                     : hasAttachments ? mAttachmentIcon : null;
   1730             subjectView.setCompoundDrawablesWithIntrinsicBounds(null, null, icon, null);
   1731 
   1732             // TODO ui spec suggests "time", "day", "date" - implement "day"
   1733             TextView dateView = (TextView) view.findViewById(R.id.date);
   1734             long timestamp = cursor.getLong(COLUMN_DATE);
   1735             Date date = new Date(timestamp);
   1736             if (Utility.isDateToday(date)) {
   1737                 text = mTimeFormat.format(date);
   1738             } else {
   1739                 text = mDateFormat.format(date);
   1740             }
   1741             dateView.setText(text);
   1742 
   1743             if (itemView.mRead) {
   1744                 subjectView.setTypeface(Typeface.DEFAULT);
   1745                 fromView.setTypeface(Typeface.DEFAULT);
   1746                 fromView.setTextColor(mTextColorSecondary);
   1747                 view.setBackgroundDrawable(context.getResources().getDrawable(
   1748                         R.drawable.message_list_item_background_read));
   1749             } else {
   1750                 subjectView.setTypeface(Typeface.DEFAULT_BOLD);
   1751                 fromView.setTypeface(Typeface.DEFAULT_BOLD);
   1752                 fromView.setTextColor(mTextColorPrimary);
   1753                 view.setBackgroundDrawable(context.getResources().getDrawable(
   1754                         R.drawable.message_list_item_background_unread));
   1755             }
   1756 
   1757             ImageView selectedView = (ImageView) view.findViewById(R.id.selected);
   1758             selectedView.setImageDrawable(itemView.mSelected ? mSelectedIconOn : mSelectedIconOff);
   1759 
   1760             ImageView favoriteView = (ImageView) view.findViewById(R.id.favorite);
   1761             favoriteView.setImageDrawable(itemView.mFavorite ? mFavoriteIconOn : mFavoriteIconOff);
   1762         }
   1763 
   1764         @Override
   1765         public View newView(Context context, Cursor cursor, ViewGroup parent) {
   1766             return mInflater.inflate(R.layout.message_list_item, parent, false);
   1767         }
   1768 
   1769         /**
   1770          * This is used as a callback from the list items, to set the selected state
   1771          *
   1772          * @param itemView the item being changed
   1773          * @param newSelected the new value of the selected flag (checkbox state)
   1774          */
   1775         public void updateSelected(MessageListItem itemView, boolean newSelected) {
   1776             ImageView selectedView = (ImageView) itemView.findViewById(R.id.selected);
   1777             selectedView.setImageDrawable(newSelected ? mSelectedIconOn : mSelectedIconOff);
   1778 
   1779             // Set checkbox state in list, and show/hide panel if necessary
   1780             Long id = Long.valueOf(itemView.mMessageId);
   1781             if (newSelected) {
   1782                 mChecked.add(id);
   1783             } else {
   1784                 mChecked.remove(id);
   1785             }
   1786             if (mChecked.size() == 1 && newSelected) {
   1787                 mFirstSelectedItemPosition = getListView().getPositionForView(itemView);
   1788                 mFirstSelectedItemTop = itemView.getBottom();
   1789                 mFirstSelectedItemHeight = itemView.getHeight();
   1790             } else {
   1791                 mFirstSelectedItemPosition = -1;
   1792             }
   1793 
   1794             MessageList.this.showMultiPanel(mChecked.size() > 0);
   1795         }
   1796 
   1797         /**
   1798          * This is used as a callback from the list items, to set the favorite state
   1799          *
   1800          * @param itemView the item being changed
   1801          * @param newFavorite the new value of the favorite flag (star state)
   1802          */
   1803         public void updateFavorite(MessageListItem itemView, boolean newFavorite) {
   1804             ImageView favoriteView = (ImageView) itemView.findViewById(R.id.favorite);
   1805             favoriteView.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
   1806             onSetMessageFavorite(itemView.mMessageId, newFavorite);
   1807         }
   1808     }
   1809 }
   1810