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