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