MessageList.java revision 54e0a5f65c777147a1d90cd73d2ebeab2a67e417
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.activity;
18
19import com.android.email.Controller;
20import com.android.email.Email;
21import com.android.email.R;
22import com.android.email.Utility;
23import com.android.email.activity.setup.AccountSettings;
24import com.android.email.mail.AuthenticationFailedException;
25import com.android.email.mail.CertificateValidationException;
26import com.android.email.mail.MessagingException;
27import com.android.email.provider.EmailContent;
28import com.android.email.provider.EmailContent.Account;
29import com.android.email.provider.EmailContent.AccountColumns;
30import com.android.email.provider.EmailContent.Mailbox;
31import com.android.email.provider.EmailContent.MailboxColumns;
32import com.android.email.provider.EmailContent.MessageColumns;
33import com.android.email.service.MailService;
34
35import android.app.ListActivity;
36import android.app.NotificationManager;
37import android.content.ContentResolver;
38import android.content.ContentUris;
39import android.content.Context;
40import android.content.Intent;
41import android.content.res.Resources;
42import android.database.Cursor;
43import android.graphics.Typeface;
44import android.graphics.drawable.Drawable;
45import android.net.Uri;
46import android.os.AsyncTask;
47import android.os.Bundle;
48import android.os.Handler;
49import android.view.ContextMenu;
50import android.view.LayoutInflater;
51import android.view.Menu;
52import android.view.MenuItem;
53import android.view.View;
54import android.view.ViewGroup;
55import android.view.Window;
56import android.view.ContextMenu.ContextMenuInfo;
57import android.view.View.OnClickListener;
58import android.view.animation.AnimationUtils;
59import android.widget.AdapterView;
60import android.widget.Button;
61import android.widget.CursorAdapter;
62import android.widget.ImageView;
63import android.widget.ListView;
64import android.widget.ProgressBar;
65import android.widget.TextView;
66import android.widget.Toast;
67import android.widget.AdapterView.OnItemClickListener;
68
69import java.util.Date;
70import java.util.HashSet;
71import java.util.Set;
72
73public class MessageList extends ListActivity implements OnItemClickListener, OnClickListener {
74    // Intent extras (internal to this activity)
75    private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID";
76    private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE";
77    private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID";
78    private static final String STATE_SELECTED_ITEM_TOP =
79        "com.android.email.activity.MessageList.selectedItemTop";
80    private static final String STATE_SELECTED_POSITION =
81        "com.android.email.activity.MessageList.selectedPosition";
82
83    // UI support
84    private ListView mListView;
85    private View mMultiSelectPanel;
86    private Button mReadUnreadButton;
87    private Button mFavoriteButton;
88    private Button mDeleteButton;
89    private View mListFooterView;
90    private TextView mListFooterText;
91    private View mListFooterProgress;
92    private TextView mErrorBanner;
93
94    private static final int LIST_FOOTER_MODE_NONE = 0;
95    private static final int LIST_FOOTER_MODE_REFRESH = 1;
96    private static final int LIST_FOOTER_MODE_MORE = 2;
97    private static final int LIST_FOOTER_MODE_SEND = 3;
98    private int mListFooterMode;
99
100    private MessageListAdapter mListAdapter;
101    private MessageListHandler mHandler = new MessageListHandler();
102    private Controller mController = Controller.getInstance(getApplication());
103    private ControllerResults mControllerCallback = new ControllerResults();
104    private TextView mLeftTitle;
105    private TextView mRightTitle;
106    private ProgressBar mProgressIcon;
107
108    private static final int[] mColorChipResIds = new int[] {
109        R.drawable.appointment_indicator_leftside_1,
110        R.drawable.appointment_indicator_leftside_2,
111        R.drawable.appointment_indicator_leftside_3,
112        R.drawable.appointment_indicator_leftside_4,
113        R.drawable.appointment_indicator_leftside_5,
114        R.drawable.appointment_indicator_leftside_6,
115        R.drawable.appointment_indicator_leftside_7,
116        R.drawable.appointment_indicator_leftside_8,
117        R.drawable.appointment_indicator_leftside_9,
118        R.drawable.appointment_indicator_leftside_10,
119        R.drawable.appointment_indicator_leftside_11,
120        R.drawable.appointment_indicator_leftside_12,
121        R.drawable.appointment_indicator_leftside_13,
122        R.drawable.appointment_indicator_leftside_14,
123        R.drawable.appointment_indicator_leftside_15,
124        R.drawable.appointment_indicator_leftside_16,
125        R.drawable.appointment_indicator_leftside_17,
126        R.drawable.appointment_indicator_leftside_18,
127        R.drawable.appointment_indicator_leftside_19,
128        R.drawable.appointment_indicator_leftside_20,
129        R.drawable.appointment_indicator_leftside_21,
130    };
131
132    // DB access
133    private ContentResolver mResolver;
134    private long mMailboxId;
135    private LoadMessagesTask mLoadMessagesTask;
136    private FindMailboxTask mFindMailboxTask;
137    private SetTitleTask mSetTitleTask;
138    private SetFooterTask mSetFooterTask;
139
140    public final static String[] MAILBOX_FIND_INBOX_PROJECTION = new String[] {
141        EmailContent.RECORD_ID, MailboxColumns.TYPE, MailboxColumns.FLAG_VISIBLE
142    };
143
144    private static final int MAILBOX_NAME_COLUMN_ID = 0;
145    private static final int MAILBOX_NAME_COLUMN_ACCOUNT_KEY = 1;
146    private static final int MAILBOX_NAME_COLUMN_TYPE = 2;
147    private static final String[] MAILBOX_NAME_PROJECTION = new String[] {
148            MailboxColumns.DISPLAY_NAME, MailboxColumns.ACCOUNT_KEY,
149            MailboxColumns.TYPE};
150
151    private static final int ACCOUNT_DISPLAY_NAME_COLUMN_ID = 0;
152    private static final String[] ACCOUNT_NAME_PROJECTION = new String[] {
153            AccountColumns.DISPLAY_NAME };
154
155    private static final String ID_SELECTION = EmailContent.RECORD_ID + "=?";
156
157    private Boolean mPushModeMailbox = null;
158    private int mSavedItemTop = 0;
159    private int mSavedItemPosition = -1;
160    private boolean mCanAutoRefresh = false;
161
162    /**
163     * Open a specific mailbox.
164     *
165     * TODO This should just shortcut to a more generic version that can accept a list of
166     * accounts/mailboxes (e.g. merged inboxes).
167     *
168     * @param context
169     * @param id mailbox key
170     */
171    public static void actionHandleMailbox(Context context, long id) {
172        Intent intent = new Intent(context, MessageList.class);
173        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
174        intent.putExtra(EXTRA_MAILBOX_ID, id);
175        context.startActivity(intent);
176    }
177
178    /**
179     * Open a specific mailbox by account & type
180     *
181     * @param context The caller's context (for generating an intent)
182     * @param accountId The account to open
183     * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX)
184     */
185    public static void actionHandleAccount(Context context, long accountId, int mailboxType) {
186        Intent intent = new Intent(context, MessageList.class);
187        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
188        intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
189        intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType);
190        context.startActivity(intent);
191    }
192
193    /**
194     * Return an intent to open a specific mailbox by account & type.  It will also clear
195     * notifications.
196     *
197     * @param context The caller's context (for generating an intent)
198     * @param accountId The account to open, or -1
199     * @param mailboxId the ID of the mailbox to open, or -1
200     * @param mailboxType the type of mailbox to open (e.g. @see Mailbox.TYPE_INBOX) or -1
201     */
202    public static Intent actionHandleAccountIntent(Context context, long accountId,
203            long mailboxId, int mailboxType) {
204        Intent intent = new Intent(context, MessageList.class);
205        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
206        intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
207        intent.putExtra(EXTRA_MAILBOX_ID, mailboxId);
208        intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType);
209        return intent;
210    }
211
212    /**
213     * Used for generating lightweight (Uri-only) intents.
214     *
215     * @param context Calling context for building the intent
216     * @param accountId The account of interest
217     * @param mailboxType The folder name to open (typically Mailbox.TYPE_INBOX)
218     * @return an Intent which can be used to view that account
219     */
220    public static Intent actionHandleAccountUriIntent(Context context, long accountId,
221            int mailboxType) {
222        Intent i = actionHandleAccountIntent(context, accountId, -1, mailboxType);
223        i.removeExtra(EXTRA_ACCOUNT_ID);
224        Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
225        i.setData(uri);
226        return i;
227    }
228
229    @Override
230    public void onCreate(Bundle icicle) {
231        super.onCreate(icicle);
232
233        requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);
234        setContentView(R.layout.message_list);
235        getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,
236                R.layout.list_title);
237
238        mCanAutoRefresh = true;
239        mListView = getListView();
240        mMultiSelectPanel = findViewById(R.id.footer_organize);
241        mReadUnreadButton = (Button) findViewById(R.id.btn_read_unread);
242        mFavoriteButton = (Button) findViewById(R.id.btn_multi_favorite);
243        mDeleteButton = (Button) findViewById(R.id.btn_multi_delete);
244        mLeftTitle = (TextView) findViewById(R.id.title_left_text);
245        mRightTitle = (TextView) findViewById(R.id.title_right_text);
246        mProgressIcon = (ProgressBar) findViewById(R.id.title_progress_icon);
247        mErrorBanner = (TextView) findViewById(R.id.connection_error_text);
248
249        mReadUnreadButton.setOnClickListener(this);
250        mFavoriteButton.setOnClickListener(this);
251        mDeleteButton.setOnClickListener(this);
252
253        mListView.setOnItemClickListener(this);
254        mListView.setItemsCanFocus(false);
255        registerForContextMenu(mListView);
256
257        mListAdapter = new MessageListAdapter(this);
258        setListAdapter(mListAdapter);
259
260        mResolver = getContentResolver();
261
262        // TODO extend this to properly deal with multiple mailboxes, cursor, etc.
263
264        // Select 'by id' or 'by type' or 'by uri' mode and launch appropriate queries
265
266        mMailboxId = getIntent().getLongExtra(EXTRA_MAILBOX_ID, -1);
267        if (mMailboxId != -1) {
268            // Specific mailbox ID was provided - go directly to it
269            mSetTitleTask = new SetTitleTask(mMailboxId);
270            mSetTitleTask.execute();
271            mLoadMessagesTask = new LoadMessagesTask(mMailboxId, -1);
272            mLoadMessagesTask.execute();
273            addFooterView(mMailboxId, -1, -1);
274        } else {
275            long accountId = -1;
276            int mailboxType = getIntent().getIntExtra(EXTRA_MAILBOX_TYPE, Mailbox.TYPE_INBOX);
277            Uri uri = getIntent().getData();
278            if (uri != null
279                    && "content".equals(uri.getScheme())
280                    && EmailContent.AUTHORITY.equals(uri.getAuthority())) {
281                // A content URI was provided - try to look up the account
282                String accountIdString = uri.getPathSegments().get(1);
283                if (accountIdString != null) {
284                    accountId = Long.parseLong(accountIdString);
285                }
286                mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false);
287                mFindMailboxTask.execute();
288            } else {
289                // Go by account id + type
290                accountId = getIntent().getLongExtra(EXTRA_ACCOUNT_ID, -1);
291                mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, true);
292                mFindMailboxTask.execute();
293            }
294            addFooterView(-1, accountId, mailboxType);
295        }
296        // TODO set title to "account > mailbox (#unread)"
297    }
298
299    @Override
300    public void onPause() {
301        super.onPause();
302        mController.removeResultCallback(mControllerCallback);
303    }
304
305    @Override
306    public void onResume() {
307        super.onResume();
308        mController.addResultCallback(mControllerCallback);
309
310        // clear notifications here
311        NotificationManager notificationManager = (NotificationManager)
312                getSystemService(Context.NOTIFICATION_SERVICE);
313        notificationManager.cancel(MailService.NEW_MESSAGE_NOTIFICATION_ID);
314        restoreListPosition();
315        autoRefreshStaleMailbox();
316    }
317
318    @Override
319    protected void onDestroy() {
320        super.onDestroy();
321
322        if (mLoadMessagesTask != null &&
323                mLoadMessagesTask.getStatus() != LoadMessagesTask.Status.FINISHED) {
324            mLoadMessagesTask.cancel(true);
325            mLoadMessagesTask = null;
326        }
327        if (mFindMailboxTask != null &&
328                mFindMailboxTask.getStatus() != FindMailboxTask.Status.FINISHED) {
329            mFindMailboxTask.cancel(true);
330            mFindMailboxTask = null;
331        }
332        if (mSetTitleTask != null &&
333                mSetTitleTask.getStatus() != SetTitleTask.Status.FINISHED) {
334            mSetTitleTask.cancel(true);
335            mSetTitleTask = null;
336        }
337        if (mSetFooterTask != null &&
338                mSetFooterTask.getStatus() != SetTitleTask.Status.FINISHED) {
339            mSetFooterTask.cancel(true);
340            mSetFooterTask = null;
341        }
342    }
343
344    @Override
345    protected void onSaveInstanceState(Bundle outState) {
346        super.onSaveInstanceState(outState);
347        saveListPosition();
348        outState.putInt(STATE_SELECTED_POSITION, mSavedItemPosition);
349        outState.putInt(STATE_SELECTED_ITEM_TOP, mSavedItemTop);
350    }
351
352    @Override
353    protected void onRestoreInstanceState(Bundle savedInstanceState) {
354        super.onRestoreInstanceState(savedInstanceState);
355        mSavedItemTop = savedInstanceState.getInt(STATE_SELECTED_ITEM_TOP, 0);
356        mSavedItemPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION, -1);
357    }
358
359    private void saveListPosition() {
360        mSavedItemPosition = getListView().getSelectedItemPosition();
361        if (mSavedItemPosition >= 0) {
362            mSavedItemTop = getListView().getSelectedView().getTop();
363        } else {
364            mSavedItemPosition = getListView().getFirstVisiblePosition();
365            if (mSavedItemPosition >= 0) {
366                mSavedItemTop = 0;
367                View topChild = getListView().getChildAt(0);
368                if (topChild != null) {
369                    mSavedItemTop = topChild.getTop();
370                }
371            }
372        }
373    }
374
375    private void restoreListPosition() {
376        if (mSavedItemPosition >= 0 && mSavedItemPosition < getListView().getCount()) {
377            getListView().setSelectionFromTop(mSavedItemPosition, mSavedItemTop);
378            mSavedItemPosition = -1;
379            mSavedItemTop = 0;
380        }
381    }
382
383    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
384        if (view != mListFooterView) {
385            MessageListItem itemView = (MessageListItem) view;
386            onOpenMessage(id, itemView.mMailboxId);
387        } else {
388            doFooterClick();
389        }
390    }
391
392    public void onClick(View v) {
393        switch (v.getId()) {
394            case R.id.btn_read_unread:
395                onMultiToggleRead(mListAdapter.getSelectedSet());
396                break;
397            case R.id.btn_multi_favorite:
398                onMultiToggleFavorite(mListAdapter.getSelectedSet());
399                break;
400            case R.id.btn_multi_delete:
401                onMultiDelete(mListAdapter.getSelectedSet());
402                break;
403        }
404    }
405
406    @Override
407    public boolean onCreateOptionsMenu(Menu menu) {
408        super.onCreateOptionsMenu(menu);
409        if (mMailboxId < 0) {
410            getMenuInflater().inflate(R.menu.message_list_option_smart_folder, menu);
411        } else {
412            getMenuInflater().inflate(R.menu.message_list_option, menu);
413        }
414        return true;
415    }
416
417    @Override
418    public boolean onPrepareOptionsMenu(Menu menu) {
419        boolean showDeselect = mListAdapter.getSelectedSet().size() > 0;
420        menu.setGroupVisible(R.id.deselect_all_group, showDeselect);
421        return true;
422    }
423
424    @Override
425    public boolean onOptionsItemSelected(MenuItem item) {
426        switch (item.getItemId()) {
427            case R.id.refresh:
428                onRefresh();
429                return true;
430            case R.id.folders:
431                onFolders();
432                return true;
433            case R.id.accounts:
434                onAccounts();
435                return true;
436            case R.id.compose:
437                onCompose();
438                return true;
439            case R.id.account_settings:
440                onEditAccount();
441                return true;
442            case R.id.deselect_all:
443                onDeselectAll();
444                return true;
445            default:
446                return super.onOptionsItemSelected(item);
447        }
448    }
449
450    @Override
451    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
452        super.onCreateContextMenu(menu, v, menuInfo);
453
454        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
455        // There is no context menu for the list footer
456        if (info.targetView == mListFooterView) {
457            return;
458        }
459        MessageListItem itemView = (MessageListItem) info.targetView;
460
461        Cursor c = (Cursor) mListView.getItemAtPosition(info.position);
462        String messageName = c.getString(MessageListAdapter.COLUMN_SUBJECT);
463
464        menu.setHeaderTitle(messageName);
465
466        // TODO: There is probably a special context menu for the trash
467        Mailbox mailbox = Mailbox.restoreMailboxWithId(this, itemView.mMailboxId);
468
469        switch (mailbox.mType) {
470            case EmailContent.Mailbox.TYPE_DRAFTS:
471                getMenuInflater().inflate(R.menu.message_list_context_drafts, menu);
472                break;
473            case EmailContent.Mailbox.TYPE_OUTBOX:
474                getMenuInflater().inflate(R.menu.message_list_context_outbox, menu);
475                break;
476            case EmailContent.Mailbox.TYPE_TRASH:
477                getMenuInflater().inflate(R.menu.message_list_context_trash, menu);
478                break;
479            default:
480                getMenuInflater().inflate(R.menu.message_list_context, menu);
481                // The default menu contains "mark as read".  If the message is read, change
482                // the menu text to "mark as unread."
483                if (itemView.mRead) {
484                    menu.findItem(R.id.mark_as_read).setTitle(R.string.mark_as_unread_action);
485                }
486                break;
487        }
488    }
489
490    @Override
491    public boolean onContextItemSelected(MenuItem item) {
492        AdapterView.AdapterContextMenuInfo info =
493            (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
494        MessageListItem itemView = (MessageListItem) info.targetView;
495
496        switch (item.getItemId()) {
497            case R.id.open:
498                onOpenMessage(info.id, itemView.mMailboxId);
499                break;
500            case R.id.delete:
501                onDelete(info.id, itemView.mAccountId);
502                break;
503            case R.id.reply:
504                onReply(itemView.mMessageId);
505                break;
506            case R.id.reply_all:
507                onReplyAll(itemView.mMessageId);
508                break;
509            case R.id.forward:
510                onForward(itemView.mMessageId);
511                break;
512            case R.id.mark_as_read:
513                onSetMessageRead(info.id, !itemView.mRead);
514                break;
515        }
516        return super.onContextItemSelected(item);
517    }
518
519    private void onRefresh() {
520        // TODO: Should not be reading from DB in UI thread - need a cleaner way to get accountId
521        if (mMailboxId >= 0) {
522            Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mMailboxId);
523            mController.updateMailbox(mailbox.mAccountKey, mMailboxId, mControllerCallback);
524        }
525    }
526
527    private void onFolders() {
528        if (mMailboxId >= 0) {
529            // TODO smaller projection
530            Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mMailboxId);
531            MailboxList.actionHandleAccount(this, mailbox.mAccountKey);
532            finish();
533        }
534    }
535
536    private void onAccounts() {
537        AccountFolderList.actionShowAccounts(this);
538        finish();
539    }
540
541    private long lookupAccountIdFromMailboxId(long mailboxId) {
542        // TODO: Select correct account to send from when there are multiple mailboxes
543        // TODO: Should not be reading from DB in UI thread
544        if (mailboxId < 0) {
545            return -1; // no info, default account
546        }
547        EmailContent.Mailbox mailbox =
548            EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId);
549        return mailbox.mAccountKey;
550    }
551
552    private void onCompose() {
553        MessageCompose.actionCompose(this, lookupAccountIdFromMailboxId(mMailboxId));
554    }
555
556    private void onEditAccount() {
557        AccountSettings.actionSettings(this, lookupAccountIdFromMailboxId(mMailboxId));
558    }
559
560    private void onDeselectAll() {
561        mListAdapter.getSelectedSet().clear();
562        mListView.invalidateViews();
563        showMultiPanel(false);
564    }
565
566    private void onOpenMessage(long messageId, long mailboxId) {
567        // TODO: Should not be reading from DB in UI thread
568        EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(this, mailboxId);
569
570        if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) {
571            MessageCompose.actionEditDraft(this, messageId);
572        } else {
573            // WARNING: here we pass mMailboxId, which can be the negative id of a compound
574            // mailbox, instead of the mailboxId of the particular message that is opened
575            MessageView.actionView(this, messageId, mMailboxId);
576        }
577    }
578
579    private void onReply(long messageId) {
580        MessageCompose.actionReply(this, messageId, false);
581    }
582
583    private void onReplyAll(long messageId) {
584        MessageCompose.actionReply(this, messageId, true);
585    }
586
587    private void onForward(long messageId) {
588        MessageCompose.actionForward(this, messageId);
589    }
590
591    private void onLoadMoreMessages() {
592        if (mMailboxId >= 0) {
593            mController.loadMoreMessages(mMailboxId, mControllerCallback);
594        }
595    }
596
597    private void onSendPendingMessages() {
598        long accountId = lookupAccountIdFromMailboxId(mMailboxId);
599        mController.sendPendingMessages(accountId, mControllerCallback);
600    }
601
602    private void onDelete(long messageId, long accountId) {
603        mController.deleteMessage(messageId, accountId);
604        Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
605    }
606
607    private void onSetMessageRead(long messageId, boolean newRead) {
608        mController.setMessageRead(messageId, newRead);
609    }
610
611    private void onSetMessageFavorite(long messageId, boolean newFavorite) {
612        mController.setMessageFavorite(messageId, newFavorite);
613    }
614
615    /**
616     * Toggles a set read/unread states.  Note, the default behavior is "mark unread", so the
617     * sense of the helper methods is "true=unread".
618     *
619     * @param selectedSet The current list of selected items
620     */
621    private void onMultiToggleRead(Set<Long> selectedSet) {
622        toggleMultiple(selectedSet, new MultiToggleHelper() {
623
624            public boolean getField(long messageId, Cursor c) {
625                return c.getInt(MessageListAdapter.COLUMN_READ) == 0;
626            }
627
628            public boolean setField(long messageId, Cursor c, boolean newValue) {
629                boolean oldValue = getField(messageId, c);
630                if (oldValue != newValue) {
631                    onSetMessageRead(messageId, !newValue);
632                    return true;
633                }
634                return false;
635            }
636        });
637    }
638
639    /**
640     * Toggles a set of favorites (stars)
641     *
642     * @param selectedSet The current list of selected items
643     */
644    private void onMultiToggleFavorite(Set<Long> selectedSet) {
645        toggleMultiple(selectedSet, new MultiToggleHelper() {
646
647            public boolean getField(long messageId, Cursor c) {
648                return c.getInt(MessageListAdapter.COLUMN_FAVORITE) != 0;
649            }
650
651            public boolean setField(long messageId, Cursor c, boolean newValue) {
652                boolean oldValue = getField(messageId, c);
653                if (oldValue != newValue) {
654                    onSetMessageFavorite(messageId, newValue);
655                    return true;
656                }
657                return false;
658            }
659        });
660    }
661
662    private void onMultiDelete(Set<Long> selectedSet) {
663        // Clone the set, because deleting is going to thrash things
664        HashSet<Long> cloneSet = new HashSet<Long>(selectedSet);
665        for (Long id : cloneSet) {
666            mController.deleteMessage(id, -1);
667        }
668        // TODO: count messages and show "n messages deleted"
669        Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
670        selectedSet.clear();
671        showMultiPanel(false);
672    }
673
674    private interface MultiToggleHelper {
675        /**
676         * Return true if the field of interest is "set".  If one or more are false, then our
677         * bulk action will be to "set".  If all are set, our bulk action will be to "clear".
678         * @param messageId the message id of the current message
679         * @param c the cursor, positioned to the item of interest
680         * @return true if the field at this row is "set"
681         */
682        public boolean getField(long messageId, Cursor c);
683
684        /**
685         * Set or clear the field of interest.  Return true if a change was made.
686         * @param messageId the message id of the current message
687         * @param c the cursor, positioned to the item of interest
688         * @param newValue the new value to be set at this row
689         * @return true if a change was actually made
690         */
691        public boolean setField(long messageId, Cursor c, boolean newValue);
692    }
693
694    /**
695     * Toggle multiple fields in a message, using the following logic:  If one or more fields
696     * are "clear", then "set" them.  If all fields are "set", then "clear" them all.
697     *
698     * @param selectedSet the set of messages that are selected
699     * @param helper functions to implement the specific getter & setter
700     * @return the number of messages that were updated
701     */
702    private int toggleMultiple(Set<Long> selectedSet, MultiToggleHelper helper) {
703        Cursor c = mListAdapter.getCursor();
704        boolean anyWereFound = false;
705        boolean allWereSet = true;
706
707        c.moveToPosition(-1);
708        while (c.moveToNext()) {
709            long id = c.getInt(MessageListAdapter.COLUMN_ID);
710            if (selectedSet.contains(Long.valueOf(id))) {
711                anyWereFound = true;
712                if (!helper.getField(id, c)) {
713                    allWereSet = false;
714                    break;
715                }
716            }
717        }
718
719        int numChanged = 0;
720
721        if (anyWereFound) {
722            boolean newValue = !allWereSet;
723            c.moveToPosition(-1);
724            while (c.moveToNext()) {
725                long id = c.getInt(MessageListAdapter.COLUMN_ID);
726                if (selectedSet.contains(Long.valueOf(id))) {
727                    if (helper.setField(id, c, newValue)) {
728                        ++numChanged;
729                    }
730                }
731            }
732        }
733
734        return numChanged;
735    }
736
737    /**
738     * Test selected messages for showing appropriate labels
739     * @param selectedSet
740     * @param column_id
741     * @param defaultflag
742     * @return true when the specified flagged message is selected
743     */
744    private boolean testMultiple(Set<Long> selectedSet, int column_id, boolean defaultflag) {
745        Cursor c = mListAdapter.getCursor();
746        c.moveToPosition(-1);
747        while (c.moveToNext()) {
748            long id = c.getInt(MessageListAdapter.COLUMN_ID);
749            if (selectedSet.contains(Long.valueOf(id))) {
750                if (c.getInt(column_id) == (defaultflag? 1 : 0)) {
751                    return true;
752                }
753            }
754        }
755        return false;
756    }
757
758    /**
759     * Implements a timed refresh of "stale" mailboxes.  This should only happen when
760     * multiple conditions are true, including:
761     *   Only when the user explicitly opens the mailbox (not onResume, for example)
762     *   Only for real, non-push mailboxes
763     *   Only when the mailbox is "stale" (currently set to 5 minutes since last refresh)
764     */
765    private void autoRefreshStaleMailbox() {
766        if (!mCanAutoRefresh
767                || (mListAdapter.getCursor() == null) // Check if messages info is loaded
768                || (mPushModeMailbox != null && mPushModeMailbox) // Check the push mode
769                || (mMailboxId < 0)) { // Check if this mailbox is synthetic/combined
770            return;
771        }
772        mCanAutoRefresh = false;
773        if (!Email.mailboxRequiresRefresh(mMailboxId)) {
774            return;
775        }
776        onRefresh();
777    }
778
779    private void updateFooterButtonNames () {
780        // Show "unread_action" when one or more read messages are selected.
781        if (testMultiple(mListAdapter.getSelectedSet(), MessageListAdapter.COLUMN_READ, true)) {
782            mReadUnreadButton.setText(R.string.unread_action);
783        } else {
784            mReadUnreadButton.setText(R.string.read_action);
785        }
786        // Show "set_star_action" when one or more un-starred messages are selected.
787        if (testMultiple(mListAdapter.getSelectedSet(),
788                MessageListAdapter.COLUMN_FAVORITE, false)) {
789            mFavoriteButton.setText(R.string.set_star_action);
790        } else {
791            mFavoriteButton.setText(R.string.remove_star_action);
792        }
793    }
794
795    /**
796     * Show or hide the panel of multi-select options
797     */
798    private void showMultiPanel(boolean show) {
799        if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) {
800            mMultiSelectPanel.setVisibility(View.VISIBLE);
801            mMultiSelectPanel.startAnimation(
802                    AnimationUtils.loadAnimation(this, R.anim.footer_appear));
803        } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) {
804            mMultiSelectPanel.setVisibility(View.GONE);
805            mMultiSelectPanel.startAnimation(
806                        AnimationUtils.loadAnimation(this, R.anim.footer_disappear));
807        }
808        if (show) {
809            updateFooterButtonNames();
810        }
811    }
812
813    /**
814     * Add the fixed footer view if appropriate (not always - not all accounts & mailboxes).
815     *
816     * Here are some rules (finish this list):
817     *
818     * Any merged, synced box (except send):  refresh
819     * Any push-mode account:  refresh
820     * Any non-push-mode account:  load more
821     * Any outbox (send again):
822     *
823     * @param mailboxId the ID of the mailbox
824     */
825    private void addFooterView(long mailboxId, long accountId, int mailboxType) {
826        // first, look for shortcuts that don't need us to spin up a DB access task
827        if (mailboxId == Mailbox.QUERY_ALL_INBOXES
828                || mailboxId == Mailbox.QUERY_ALL_UNREAD
829                || mailboxId == Mailbox.QUERY_ALL_FAVORITES) {
830            finishFooterView(LIST_FOOTER_MODE_REFRESH);
831            return;
832        }
833        if (mailboxId == Mailbox.QUERY_ALL_DRAFTS || mailboxType == Mailbox.TYPE_DRAFTS) {
834            finishFooterView(LIST_FOOTER_MODE_NONE);
835            return;
836        }
837        if (mailboxId == Mailbox.QUERY_ALL_OUTBOX || mailboxType == Mailbox.TYPE_OUTBOX) {
838            finishFooterView(LIST_FOOTER_MODE_SEND);
839            return;
840        }
841
842        // We don't know enough to select the footer command type (yet), so we'll
843        // launch an async task to do the remaining lookups and decide what to do
844        mSetFooterTask = new SetFooterTask();
845        mSetFooterTask.execute(mailboxId, accountId);
846    }
847
848    private final static String[] MAILBOX_ACCOUNT_AND_TYPE_PROJECTION =
849        new String[] { MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE };
850
851    private class SetFooterTask extends AsyncTask<Long, Void, Integer> {
852        /**
853         * There are two operational modes here, requiring different lookup.
854         * mailboxIs != -1:  A specific mailbox - check its type, then look up its account
855         * accountId != -1:  A specific account - look up the account
856         */
857        @Override
858        protected Integer doInBackground(Long... params) {
859            long mailboxId = params[0];
860            long accountId = params[1];
861            int mailboxType = -1;
862            if (mailboxId != -1) {
863                try {
864                    Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId);
865                    Cursor c = mResolver.query(uri, MAILBOX_ACCOUNT_AND_TYPE_PROJECTION,
866                            null, null, null);
867                    if (c.moveToFirst()) {
868                        try {
869                            accountId = c.getLong(0);
870                            mailboxType = c.getInt(1);
871                        } finally {
872                            c.close();
873                        }
874                    }
875                } catch (IllegalArgumentException iae) {
876                    // can't do any more here
877                    return LIST_FOOTER_MODE_NONE;
878                }
879            }
880            switch (mailboxType) {
881                case Mailbox.TYPE_OUTBOX:
882                    return LIST_FOOTER_MODE_SEND;
883                case Mailbox.TYPE_DRAFTS:
884                    return LIST_FOOTER_MODE_NONE;
885            }
886            if (accountId != -1) {
887                // This is inefficient but the best fix is not here but in isMessagingController
888                Account account = Account.restoreAccountWithId(MessageList.this, accountId);
889                if (account != null) {
890                    mPushModeMailbox = account.mSyncInterval == Account.CHECK_INTERVAL_PUSH;
891                    if (MessageList.this.mController.isMessagingController(account)) {
892                        return LIST_FOOTER_MODE_MORE;       // IMAP or POP
893                    } else {
894                        return LIST_FOOTER_MODE_NONE;    // EAS
895                    }
896                }
897            }
898            return LIST_FOOTER_MODE_NONE;
899        }
900
901        @Override
902        protected void onPostExecute(Integer listFooterMode) {
903            finishFooterView(listFooterMode);
904        }
905    }
906
907    /**
908     * Add the fixed footer view as specified, and set up the test as well.
909     *
910     * @param listFooterMode the footer mode we've determined should be used for this list
911     */
912    private void finishFooterView(int listFooterMode) {
913        mListFooterMode = listFooterMode;
914        if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
915            mListFooterView = ((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE))
916                    .inflate(R.layout.message_list_item_footer, mListView, false);
917            mList.addFooterView(mListFooterView);
918            setListAdapter(mListAdapter);
919
920            mListFooterProgress = mListFooterView.findViewById(R.id.progress);
921            mListFooterText = (TextView) mListFooterView.findViewById(R.id.main_text);
922            setListFooterText(false);
923        }
924    }
925
926    /**
927     * Set the list footer text based on mode and "active" status
928     */
929    private void setListFooterText(boolean active) {
930        if (mListFooterMode != LIST_FOOTER_MODE_NONE) {
931            int footerTextId = 0;
932            switch (mListFooterMode) {
933                case LIST_FOOTER_MODE_REFRESH:
934                    footerTextId = active ? R.string.status_loading_more
935                                          : R.string.refresh_action;
936                    break;
937                case LIST_FOOTER_MODE_MORE:
938                    footerTextId = active ? R.string.status_loading_more
939                                          : R.string.message_list_load_more_messages_action;
940                    break;
941                case LIST_FOOTER_MODE_SEND:
942                    footerTextId = active ? R.string.status_sending_messages
943                                          : R.string.message_list_send_pending_messages_action;
944                    break;
945            }
946            mListFooterText.setText(footerTextId);
947        }
948    }
949
950    /**
951     * Handle a click in the list footer, which changes meaning depending on what we're looking at.
952     */
953    private void doFooterClick() {
954        switch (mListFooterMode) {
955            case LIST_FOOTER_MODE_NONE:         // should never happen
956                break;
957            case LIST_FOOTER_MODE_REFRESH:
958                onRefresh();
959                break;
960            case LIST_FOOTER_MODE_MORE:
961                onLoadMoreMessages();
962                break;
963            case LIST_FOOTER_MODE_SEND:
964                onSendPendingMessages();
965                break;
966        }
967    }
968
969    /**
970     * Async task for finding a single mailbox by type (possibly even going to the network).
971     *
972     * This is much too complex, as implemented.  It uses this AsyncTask to check for a mailbox,
973     * then (if not found) a Controller call to refresh mailboxes from the server, and a handler
974     * to relaunch this task (a 2nd time) to read the results of the network refresh.  The core
975     * problem is that we have two different non-UI-thread jobs (reading DB and reading network)
976     * and two different paradigms for dealing with them.  Some unification would be needed here
977     * to make this cleaner.
978     *
979     * TODO: If this problem spreads to other operations, find a cleaner way to handle it.
980     */
981    private class FindMailboxTask extends AsyncTask<Void, Void, Long> {
982
983        private long mAccountId;
984        private int mMailboxType;
985        private boolean mOkToRecurse;
986
987        /**
988         * Special constructor to cache some local info
989         */
990        public FindMailboxTask(long accountId, int mailboxType, boolean okToRecurse) {
991            mAccountId = accountId;
992            mMailboxType = mailboxType;
993            mOkToRecurse = okToRecurse;
994        }
995
996        @Override
997        protected Long doInBackground(Void... params) {
998            // See if we can find the requested mailbox in the DB.
999            long mailboxId = Mailbox.findMailboxOfType(MessageList.this, mAccountId, mMailboxType);
1000            if (mailboxId == Mailbox.NO_MAILBOX && mOkToRecurse) {
1001                // Not found - launch network lookup
1002                mControllerCallback.mWaitForMailboxType = mMailboxType;
1003                mController.updateMailboxList(mAccountId, mControllerCallback);
1004            }
1005            return mailboxId;
1006        }
1007
1008        @Override
1009        protected void onPostExecute(Long mailboxId) {
1010            if (mailboxId != Mailbox.NO_MAILBOX) {
1011                mMailboxId = mailboxId;
1012                mSetTitleTask = new SetTitleTask(mMailboxId);
1013                mSetTitleTask.execute();
1014                mLoadMessagesTask = new LoadMessagesTask(mMailboxId, mAccountId);
1015                mLoadMessagesTask.execute();
1016            }
1017        }
1018    }
1019
1020    /**
1021     * Async task for loading a single folder out of the UI thread
1022     *
1023     * The code here (for merged boxes) is a placeholder/hack and should be replaced.  Some
1024     * specific notes:
1025     * TODO:  Move the double query into a specialized URI that returns all inbox messages
1026     * and do the dirty work in raw SQL in the provider.
1027     * TODO:  Generalize the query generation so we can reuse it in MessageView (for next/prev)
1028     */
1029    private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> {
1030
1031        private long mMailboxKey;
1032        private long mAccountKey;
1033
1034        /**
1035         * Special constructor to cache some local info
1036         */
1037        public LoadMessagesTask(long mailboxKey, long accountKey) {
1038            mMailboxKey = mailboxKey;
1039            mAccountKey = accountKey;
1040        }
1041
1042        @Override
1043        protected Cursor doInBackground(Void... params) {
1044            String selection =
1045                Utility.buildMailboxIdSelection(MessageList.this.mResolver, mMailboxKey);
1046            Cursor c = MessageList.this.managedQuery(
1047                    EmailContent.Message.CONTENT_URI,
1048                    MessageList.this.mListAdapter.PROJECTION,
1049                    selection, null,
1050                    EmailContent.MessageColumns.TIMESTAMP + " DESC");
1051            return c;
1052        }
1053
1054        @Override
1055        protected void onPostExecute(Cursor cursor) {
1056            if (cursor.isClosed()) {
1057                return;
1058            }
1059            MessageList.this.mListAdapter.changeCursor(cursor);
1060            // changeCursor occurs the jumping of position in ListView, so it's need to restore
1061            // the position;
1062            restoreListPosition();
1063            autoRefreshStaleMailbox();
1064            // Reset the "new messages" count in the service, since we're seeing them now
1065            if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) {
1066                MailService.resetNewMessageCount(MessageList.this, -1);
1067            } else if (mMailboxKey >= 0 && mAccountKey != -1) {
1068                MailService.resetNewMessageCount(MessageList.this, mAccountKey);
1069            }
1070        }
1071    }
1072
1073    private class SetTitleTask extends AsyncTask<Void, Void, String[]> {
1074
1075        private long mMailboxKey;
1076
1077        public SetTitleTask(long mailboxKey) {
1078            mMailboxKey = mailboxKey;
1079        }
1080
1081        @Override
1082        protected String[] doInBackground(Void... params) {
1083            // Check special Mailboxes
1084            if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) {
1085                return new String[] {null,
1086                        getString(R.string.account_folder_list_summary_inbox)};
1087            } else if (mMailboxKey == Mailbox.QUERY_ALL_FAVORITES) {
1088                return new String[] {null,
1089                        getString(R.string.account_folder_list_summary_starred)};
1090            } else if (mMailboxKey == Mailbox.QUERY_ALL_DRAFTS) {
1091                return new String[] {null,
1092                        getString(R.string.account_folder_list_summary_drafts)};
1093            } else if (mMailboxKey == Mailbox.QUERY_ALL_OUTBOX) {
1094                return new String[] {null,
1095                        getString(R.string.account_folder_list_summary_outbox)};
1096            }
1097            String accountName = null;
1098            String mailboxName = null;
1099            String accountKey = null;
1100            Cursor c = MessageList.this.mResolver.query(Mailbox.CONTENT_URI,
1101                    MAILBOX_NAME_PROJECTION, ID_SELECTION,
1102                    new String[] { Long.toString(mMailboxKey) }, null);
1103            try {
1104                if (c.moveToFirst()) {
1105                    mailboxName = Utility.FolderProperties.getInstance(MessageList.this)
1106                            .getDisplayName(c.getInt(MAILBOX_NAME_COLUMN_TYPE));
1107                    if (mailboxName == null) {
1108                        mailboxName = c.getString(MAILBOX_NAME_COLUMN_ID);
1109                    }
1110                    accountKey = c.getString(MAILBOX_NAME_COLUMN_ACCOUNT_KEY);
1111                }
1112            } finally {
1113                c.close();
1114            }
1115            if (accountKey != null) {
1116                c = MessageList.this.mResolver.query(Account.CONTENT_URI,
1117                        ACCOUNT_NAME_PROJECTION, ID_SELECTION, new String[] { accountKey },
1118                        null);
1119                try {
1120                    if (c.moveToFirst()) {
1121                        accountName = c.getString(ACCOUNT_DISPLAY_NAME_COLUMN_ID);
1122                    }
1123                } finally {
1124                    c.close();
1125                }
1126            }
1127            return new String[] {accountName, mailboxName};
1128        }
1129
1130        @Override
1131        protected void onPostExecute(String[] names) {
1132            if (names[0] != null) {
1133                mRightTitle.setText(names[0]);
1134            }
1135            if (names[1] != null) {
1136                mLeftTitle.setText(names[1]);
1137            }
1138        }
1139    }
1140
1141    /**
1142     * Handler for UI-thread operations (when called from callbacks or any other threads)
1143     */
1144    class MessageListHandler extends Handler {
1145        private static final int MSG_PROGRESS = 1;
1146        private static final int MSG_LOOKUP_MAILBOX_TYPE = 2;
1147        private static final int MSG_ERROR_BANNER = 3;
1148
1149        @Override
1150        public void handleMessage(android.os.Message msg) {
1151            switch (msg.what) {
1152                case MSG_PROGRESS:
1153                    boolean visible = (msg.arg1 != 0);
1154                    if (visible) {
1155                        mProgressIcon.setVisibility(View.VISIBLE);
1156                    } else {
1157                        mProgressIcon.setVisibility(View.GONE);
1158                    }
1159                    if (mListFooterProgress != null) {
1160                        mListFooterProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
1161                    }
1162                    setListFooterText(visible);
1163                    break;
1164                case MSG_LOOKUP_MAILBOX_TYPE:
1165                    // kill running async task, if any
1166                    if (mFindMailboxTask != null &&
1167                            mFindMailboxTask.getStatus() != FindMailboxTask.Status.FINISHED) {
1168                        mFindMailboxTask.cancel(true);
1169                        mFindMailboxTask = null;
1170                    }
1171                    // start new one.  do not recurse back to controller.
1172                    long accountId = ((Long)msg.obj).longValue();
1173                    int mailboxType = msg.arg1;
1174                    mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false);
1175                    mFindMailboxTask.execute();
1176                    break;
1177                case MSG_ERROR_BANNER:
1178                    String message = (String) msg.obj;
1179                    boolean isVisible = mErrorBanner.getVisibility() == View.VISIBLE;
1180                    if (message != null) {
1181                        mErrorBanner.setText(message);
1182                        if (!isVisible) {
1183                            mErrorBanner.setVisibility(View.VISIBLE);
1184                            mErrorBanner.startAnimation(
1185                                    AnimationUtils.loadAnimation(
1186                                            MessageList.this, R.anim.header_appear));
1187                        }
1188                    } else {
1189                        if (isVisible) {
1190                            mErrorBanner.setVisibility(View.GONE);
1191                            mErrorBanner.startAnimation(
1192                                    AnimationUtils.loadAnimation(
1193                                            MessageList.this, R.anim.header_disappear));
1194                        }
1195                    }
1196                    break;
1197                default:
1198                    super.handleMessage(msg);
1199            }
1200        }
1201
1202        /**
1203         * Call from any thread to start/stop progress indicator(s)
1204         * @param progress true to start, false to stop
1205         */
1206        public void progress(boolean progress) {
1207            android.os.Message msg = android.os.Message.obtain();
1208            msg.what = MSG_PROGRESS;
1209            msg.arg1 = progress ? 1 : 0;
1210            sendMessage(msg);
1211        }
1212
1213        /**
1214         * Called from any thread to look for a mailbox of a specific type.  This is designed
1215         * to be called from the Controller's MailboxList callback;  It instructs the async task
1216         * not to recurse, in case the mailbox is not found after this.
1217         *
1218         * See FindMailboxTask for more notes on this handler.
1219         */
1220        public void lookupMailboxType(long accountId, int mailboxType) {
1221            android.os.Message msg = android.os.Message.obtain();
1222            msg.what = MSG_LOOKUP_MAILBOX_TYPE;
1223            msg.arg1 = mailboxType;
1224            msg.obj = Long.valueOf(accountId);
1225            sendMessage(msg);
1226        }
1227
1228        /**
1229         * Called from any thread to show or hide the connection error banner.
1230         * @param message error text or null to hide the box
1231         */
1232        public void showErrorBanner(String message) {
1233            android.os.Message msg = android.os.Message.obtain();
1234            msg.what = MSG_ERROR_BANNER;
1235            msg.obj = message;
1236            sendMessage(msg);
1237        }
1238    }
1239
1240    /**
1241     * Callback for async Controller results.
1242     */
1243    private class ControllerResults implements Controller.Result {
1244
1245        // This is used to alter the connection banner operation for sending messages
1246        MessagingException mSendMessageException;
1247
1248        // These are preset for use by updateMailboxListCallback
1249        int mWaitForMailboxType = -1;
1250
1251        // TODO check accountKey and only react to relevant notifications
1252        public void updateMailboxListCallback(MessagingException result, long accountKey,
1253                int progress) {
1254            // no updateBanner here, we are only listing a single mailbox
1255            updateProgress(result, progress);
1256            if (progress == 100) {
1257                mHandler.lookupMailboxType(accountKey, mWaitForMailboxType);
1258            }
1259        }
1260
1261        // TODO check accountKey and only react to relevant notifications
1262        public void updateMailboxCallback(MessagingException result, long accountKey,
1263                long mailboxKey, int progress, int numNewMessages) {
1264            updateBanner(result, progress, mailboxKey);
1265            if (result != null || progress == 100) {
1266                Email.updateMailboxRefreshTime(mMailboxId);
1267            }
1268            updateProgress(result, progress);
1269        }
1270
1271        public void loadMessageForViewCallback(MessagingException result, long messageId,
1272                int progress) {
1273        }
1274
1275        public void loadAttachmentCallback(MessagingException result, long messageId,
1276                long attachmentId, int progress) {
1277        }
1278
1279        public void serviceCheckMailCallback(MessagingException result, long accountId,
1280                long mailboxId, int progress, long tag) {
1281        }
1282
1283        /**
1284         * We alter the updateBanner hysteresis here to capture any failures and handle
1285         * them just once at the end.  This callback is overly overloaded:
1286         *  result == null, messageId == -1, progress == 0:     start batch send
1287         *  result == null, messageId == xx, progress == 0:     start sending one message
1288         *  result == xxxx, messageId == xx, progress == 0;     failed sending one message
1289         *  result == null, messageId == -1, progres == 100;    finish sending batch
1290         */
1291        public void sendMailCallback(MessagingException result, long accountId, long messageId,
1292                int progress) {
1293            if (mListFooterMode == LIST_FOOTER_MODE_SEND) {
1294                // reset captured error when we start sending one or more messages
1295                if (messageId == -1 && result == null && progress == 0) {
1296                    mSendMessageException = null;
1297                }
1298                // capture first exception that comes along
1299                if (result != null && mSendMessageException == null) {
1300                    mSendMessageException = result;
1301                }
1302                // if we're completing the sequence, change the banner state
1303                if (messageId == -1 && progress == 100) {
1304                    updateBanner(mSendMessageException, progress, mMailboxId);
1305                }
1306                // always update the spinner, which has less state to worry about
1307                updateProgress(result, progress);
1308            }
1309        }
1310
1311        private void updateProgress(MessagingException result, int progress) {
1312            if (result != null || progress == 100) {
1313                mHandler.progress(false);
1314            } else if (progress == 0) {
1315                mHandler.progress(true);
1316            }
1317        }
1318
1319        /**
1320         * Show or hide the connection error banner, and convert the various MessagingException
1321         * variants into localizable text.  There is hysteresis in the show/hide logic:  Once shown,
1322         * the banner will remain visible until some progress is made on the connection.  The
1323         * goal is to keep it from flickering during retries in a bad connection state.
1324         *
1325         * @param result
1326         * @param progress
1327         */
1328        private void updateBanner(MessagingException result, int progress, long mailboxKey) {
1329            if (mailboxKey != mMailboxId) {
1330                return;
1331            }
1332            if (result != null) {
1333                int id = R.string.status_network_error;
1334                if (result instanceof AuthenticationFailedException) {
1335                    id = R.string.account_setup_failed_dlg_auth_message;
1336                } else if (result instanceof CertificateValidationException) {
1337                    id = R.string.account_setup_failed_dlg_certificate_message;
1338                } else {
1339                    switch (result.getExceptionType()) {
1340                        case MessagingException.IOERROR:
1341                            id = R.string.account_setup_failed_ioerror;
1342                            break;
1343                        case MessagingException.TLS_REQUIRED:
1344                            id = R.string.account_setup_failed_tls_required;
1345                            break;
1346                        case MessagingException.AUTH_REQUIRED:
1347                            id = R.string.account_setup_failed_auth_required;
1348                            break;
1349                        case MessagingException.GENERAL_SECURITY:
1350                            id = R.string.account_setup_failed_security;
1351                            break;
1352                    }
1353                }
1354                mHandler.showErrorBanner(getString(id));
1355            } else if (progress > 0) {
1356                mHandler.showErrorBanner(null);
1357            }
1358        }
1359    }
1360
1361    /**
1362     * This class implements the adapter for displaying messages based on cursors.
1363     */
1364    /* package */ class MessageListAdapter extends CursorAdapter {
1365
1366        public static final int COLUMN_ID = 0;
1367        public static final int COLUMN_MAILBOX_KEY = 1;
1368        public static final int COLUMN_ACCOUNT_KEY = 2;
1369        public static final int COLUMN_DISPLAY_NAME = 3;
1370        public static final int COLUMN_SUBJECT = 4;
1371        public static final int COLUMN_DATE = 5;
1372        public static final int COLUMN_READ = 6;
1373        public static final int COLUMN_FAVORITE = 7;
1374        public static final int COLUMN_ATTACHMENTS = 8;
1375
1376        public final String[] PROJECTION = new String[] {
1377            EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
1378            MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP,
1379            MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT,
1380        };
1381
1382        Context mContext;
1383        private LayoutInflater mInflater;
1384        private Drawable mAttachmentIcon;
1385        private Drawable mFavoriteIconOn;
1386        private Drawable mFavoriteIconOff;
1387        private Drawable mSelectedIconOn;
1388        private Drawable mSelectedIconOff;
1389
1390        private java.text.DateFormat mDateFormat;
1391        private java.text.DateFormat mDayFormat;
1392        private java.text.DateFormat mTimeFormat;
1393
1394        private HashSet<Long> mChecked = new HashSet<Long>();
1395
1396        public MessageListAdapter(Context context) {
1397            super(context, null);
1398            mContext = context;
1399            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1400
1401            Resources resources = context.getResources();
1402            mAttachmentIcon = resources.getDrawable(R.drawable.ic_mms_attachment_small);
1403            mFavoriteIconOn = resources.getDrawable(android.R.drawable.star_on);
1404            mFavoriteIconOff = resources.getDrawable(android.R.drawable.star_off);
1405            mSelectedIconOn = resources.getDrawable(R.drawable.btn_check_buttonless_on);
1406            mSelectedIconOff = resources.getDrawable(R.drawable.btn_check_buttonless_off);
1407
1408            mDateFormat = android.text.format.DateFormat.getDateFormat(context);    // short date
1409            mDayFormat = android.text.format.DateFormat.getDateFormat(context);     // TODO: day
1410            mTimeFormat = android.text.format.DateFormat.getTimeFormat(context);    // 12/24 time
1411        }
1412
1413        public Set<Long> getSelectedSet() {
1414            return mChecked;
1415        }
1416
1417        @Override
1418        public void bindView(View view, Context context, Cursor cursor) {
1419            // Reset the view (in case it was recycled) and prepare for binding
1420            MessageListItem itemView = (MessageListItem) view;
1421            itemView.bindViewInit(this, true);
1422
1423            // Load the public fields in the view (for later use)
1424            itemView.mMessageId = cursor.getLong(COLUMN_ID);
1425            itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY);
1426            itemView.mAccountId = cursor.getLong(COLUMN_ACCOUNT_KEY);
1427            itemView.mRead = cursor.getInt(COLUMN_READ) != 0;
1428            itemView.mFavorite = cursor.getInt(COLUMN_FAVORITE) != 0;
1429            itemView.mSelected = mChecked.contains(Long.valueOf(itemView.mMessageId));
1430
1431            // Load the UI
1432            View chipView = view.findViewById(R.id.chip);
1433            int chipResId = mColorChipResIds[(int)itemView.mAccountId % mColorChipResIds.length];
1434            chipView.setBackgroundResource(chipResId);
1435
1436            TextView fromView = (TextView) view.findViewById(R.id.from);
1437            String text = cursor.getString(COLUMN_DISPLAY_NAME);
1438            fromView.setText(text);
1439
1440            TextView subjectView = (TextView) view.findViewById(R.id.subject);
1441            text = cursor.getString(COLUMN_SUBJECT);
1442            subjectView.setText(text);
1443
1444            boolean hasAttachments = cursor.getInt(COLUMN_ATTACHMENTS) != 0;
1445            subjectView.setCompoundDrawablesWithIntrinsicBounds(null, null,
1446                    hasAttachments ? mAttachmentIcon : null, null);
1447
1448            // TODO ui spec suggests "time", "day", "date" - implement "day"
1449            TextView dateView = (TextView) view.findViewById(R.id.date);
1450            long timestamp = cursor.getLong(COLUMN_DATE);
1451            Date date = new Date(timestamp);
1452            if (Utility.isDateToday(date)) {
1453                text = mTimeFormat.format(date);
1454            } else {
1455                text = mDateFormat.format(date);
1456            }
1457            dateView.setText(text);
1458
1459            if (itemView.mRead) {
1460                subjectView.setTypeface(Typeface.DEFAULT);
1461                fromView.setTypeface(Typeface.DEFAULT);
1462                view.setBackgroundDrawable(context.getResources().getDrawable(
1463                        R.drawable.message_list_item_background_read));
1464            } else {
1465                subjectView.setTypeface(Typeface.DEFAULT_BOLD);
1466                fromView.setTypeface(Typeface.DEFAULT_BOLD);
1467                view.setBackgroundDrawable(context.getResources().getDrawable(
1468                        R.drawable.message_list_item_background_unread));
1469            }
1470
1471            ImageView selectedView = (ImageView) view.findViewById(R.id.selected);
1472            selectedView.setImageDrawable(itemView.mSelected ? mSelectedIconOn : mSelectedIconOff);
1473
1474            ImageView favoriteView = (ImageView) view.findViewById(R.id.favorite);
1475            favoriteView.setImageDrawable(itemView.mFavorite ? mFavoriteIconOn : mFavoriteIconOff);
1476        }
1477
1478        @Override
1479        public View newView(Context context, Cursor cursor, ViewGroup parent) {
1480            return mInflater.inflate(R.layout.message_list_item, parent, false);
1481        }
1482
1483        /**
1484         * This is used as a callback from the list items, to set the selected state
1485         *
1486         * @param itemView the item being changed
1487         * @param newSelected the new value of the selected flag (checkbox state)
1488         */
1489        public void updateSelected(MessageListItem itemView, boolean newSelected) {
1490            ImageView selectedView = (ImageView) itemView.findViewById(R.id.selected);
1491            selectedView.setImageDrawable(newSelected ? mSelectedIconOn : mSelectedIconOff);
1492
1493            // Set checkbox state in list, and show/hide panel if necessary
1494            Long id = Long.valueOf(itemView.mMessageId);
1495            if (newSelected) {
1496                mChecked.add(id);
1497            } else {
1498                mChecked.remove(id);
1499            }
1500
1501            MessageList.this.showMultiPanel(mChecked.size() > 0);
1502        }
1503
1504        /**
1505         * This is used as a callback from the list items, to set the favorite state
1506         *
1507         * @param itemView the item being changed
1508         * @param newFavorite the new value of the favorite flag (star state)
1509         */
1510        public void updateFavorite(MessageListItem itemView, boolean newFavorite) {
1511            ImageView favoriteView = (ImageView) itemView.findViewById(R.id.favorite);
1512            favoriteView.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
1513            onSetMessageFavorite(itemView.mMessageId, newFavorite);
1514        }
1515    }
1516}
1517