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