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