MessageList.java revision bcf32320e2600e96c8a9e997a8903bfc3893b35e
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.activity;
18
19import com.android.email.Controller;
20import com.android.email.ControllerResultUiThreadWrapper;
21import com.android.email.Email;
22import com.android.email.R;
23import com.android.email.Utility;
24import com.android.email.activity.setup.AccountSecurity;
25import com.android.email.activity.setup.AccountSettings;
26import com.android.email.mail.AuthenticationFailedException;
27import com.android.email.mail.CertificateValidationException;
28import com.android.email.mail.MessagingException;
29import com.android.email.provider.EmailContent;
30import com.android.email.provider.EmailContent.Account;
31import com.android.email.provider.EmailContent.AccountColumns;
32import com.android.email.provider.EmailContent.Mailbox;
33import com.android.email.provider.EmailContent.MailboxColumns;
34import com.android.email.service.MailService;
35
36import android.app.Activity;
37import android.app.NotificationManager;
38import android.content.ContentResolver;
39import android.content.ContentUris;
40import android.content.Context;
41import android.content.Intent;
42import android.database.Cursor;
43import android.net.Uri;
44import android.os.AsyncTask;
45import android.os.Bundle;
46import android.os.Handler;
47import android.view.Menu;
48import android.view.MenuItem;
49import android.view.View;
50import android.view.View.OnClickListener;
51import android.view.animation.Animation;
52import android.view.animation.AnimationUtils;
53import android.view.animation.Animation.AnimationListener;
54import android.widget.Button;
55import android.widget.ProgressBar;
56import android.widget.TextView;
57
58public class MessageList extends Activity implements OnClickListener,
59        AnimationListener, MessageListFragment.Callback {
60    // Intent extras (internal to this activity)
61    private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID";
62    private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE";
63    private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID";
64
65    private static final int REQUEST_SECURITY = 0;
66
67    // UI support
68    private MessageListFragment mListFragment;
69    private View mMultiSelectPanel;
70    private Button mReadUnreadButton;
71    private Button mFavoriteButton;
72    private Button mDeleteButton;
73    private TextView mErrorBanner;
74
75    private final Controller mController = Controller.getInstance(getApplication());
76    private ControllerResultUiThreadWrapper<ControllerResults> mControllerCallback;
77
78    private TextView mLeftTitle;
79    private ProgressBar mProgressIcon;
80
81    // DB access
82    private ContentResolver mResolver;
83    private FindMailboxTask mFindMailboxTask;
84    private SetTitleTask mSetTitleTask;
85
86    private static final int MAILBOX_NAME_COLUMN_ID = 0;
87    private static final int MAILBOX_NAME_COLUMN_ACCOUNT_KEY = 1;
88    private static final int MAILBOX_NAME_COLUMN_TYPE = 2;
89    private static final String[] MAILBOX_NAME_PROJECTION = new String[] {
90            MailboxColumns.DISPLAY_NAME, MailboxColumns.ACCOUNT_KEY,
91            MailboxColumns.TYPE};
92
93    private static final int ACCOUNT_DISPLAY_NAME_COLUMN_ID = 0;
94    private static final String[] ACCOUNT_NAME_PROJECTION = new String[] {
95            AccountColumns.DISPLAY_NAME };
96
97    private static final String ID_SELECTION = EmailContent.RECORD_ID + "=?";
98
99    /* package */ MessageListFragment getListFragmentForTest() {
100        return mListFragment;
101    }
102
103    /**
104     * Open a specific mailbox.
105     *
106     * TODO This should just shortcut to a more generic version that can accept a list of
107     * accounts/mailboxes (e.g. merged inboxes).
108     *
109     * @param context
110     * @param id mailbox key
111     */
112    public static void actionHandleMailbox(Context context, long id) {
113        context.startActivity(createIntent(context, -1, id, -1));
114    }
115
116    /**
117     * Open a specific mailbox by account & type
118     *
119     * @param context The caller's context (for generating an intent)
120     * @param accountId The account to open
121     * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX)
122     */
123    public static void actionHandleAccount(Context context, long accountId, int mailboxType) {
124        context.startActivity(createIntent(context, accountId, -1, mailboxType));
125    }
126
127    /**
128     * Open the inbox of the account with a UUID.  It's used to handle old style
129     * (Android <= 1.6) desktop shortcut intents.
130     */
131    public static void actionOpenAccountInboxUuid(Context context, String accountUuid) {
132        Intent i = createIntent(context, -1, -1, Mailbox.TYPE_INBOX);
133        i.setData(Account.getShortcutSafeUriFromUuid(accountUuid));
134        context.startActivity(i);
135    }
136
137    /**
138     * Return an intent to open a specific mailbox by account & type.
139     *
140     * @param context The caller's context (for generating an intent)
141     * @param accountId The account to open, or -1
142     * @param mailboxId the ID of the mailbox to open, or -1
143     * @param mailboxType the type of mailbox to open (e.g. @see Mailbox.TYPE_INBOX) or -1
144     */
145    public static Intent createIntent(Context context, long accountId, long mailboxId,
146            int mailboxType) {
147        Intent intent = new Intent(context, MessageList.class);
148        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
149        if (accountId != -1) intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
150        if (mailboxId != -1) intent.putExtra(EXTRA_MAILBOX_ID, mailboxId);
151        if (mailboxType != -1) intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType);
152        return intent;
153    }
154
155    /**
156     * Create and return an intent for a desktop shortcut for an account.
157     *
158     * @param context Calling context for building the intent
159     * @param account The account of interest
160     * @param mailboxType The folder name to open (typically Mailbox.TYPE_INBOX)
161     * @return an Intent which can be used to view that account
162     */
163    public static Intent createAccountIntentForShortcut(Context context, Account account,
164            int mailboxType) {
165        Intent i = createIntent(context, -1, -1, mailboxType);
166        i.setData(account.getShortcutSafeUri());
167        return i;
168    }
169
170    @Override
171    public void onCreate(Bundle icicle) {
172        super.onCreate(icicle);
173        setContentView(R.layout.message_list);
174
175        mControllerCallback = new ControllerResultUiThreadWrapper<ControllerResults>(
176                new Handler(), new ControllerResults());
177        mListFragment = (MessageListFragment) findFragmentById(R.id.message_list_fragment);
178        mMultiSelectPanel = findViewById(R.id.footer_organize);
179        mReadUnreadButton = (Button) findViewById(R.id.btn_read_unread);
180        mFavoriteButton = (Button) findViewById(R.id.btn_multi_favorite);
181        mDeleteButton = (Button) findViewById(R.id.btn_multi_delete);
182        mLeftTitle = (TextView) findViewById(R.id.title_left_text);
183        mProgressIcon = (ProgressBar) findViewById(R.id.title_progress_icon);
184        mErrorBanner = (TextView) findViewById(R.id.connection_error_text);
185
186        mReadUnreadButton.setOnClickListener(this);
187        mFavoriteButton.setOnClickListener(this);
188        mDeleteButton.setOnClickListener(this);
189        ((Button) findViewById(R.id.account_title_button)).setOnClickListener(this);
190
191        mListFragment.setCallback(this);
192
193        mResolver = getContentResolver();
194
195        // Show the appropriate account/mailbox specified by an {@link Intent}.
196        selectAccountAndMailbox(getIntent());
197    }
198
199    /**
200     * Show the appropriate account/mailbox specified by an {@link Intent}.
201     */
202    private void selectAccountAndMailbox(Intent intent) {
203        long mailboxId = intent.getLongExtra(EXTRA_MAILBOX_ID, -1);
204        if (mailboxId != -1) {
205            // Specific mailbox ID was provided - go directly to it
206            mSetTitleTask = new SetTitleTask(mailboxId);
207            mSetTitleTask.execute();
208            mListFragment.openMailbox(-1, mailboxId);
209        } else {
210            int mailboxType = intent.getIntExtra(EXTRA_MAILBOX_TYPE, Mailbox.TYPE_INBOX);
211            Uri uri = intent.getData();
212            // TODO Possible ANR.  getAccountIdFromShortcutSafeUri accesses DB.
213            long accountId = (uri == null) ? -1
214                    : Account.getAccountIdFromShortcutSafeUri(this, uri);
215
216            // TODO Both branches have almost identical code.  Unify them.
217            // (And why not "okay to recurse" in the first case?)
218            if (accountId != -1) {
219                // A content URI was provided - try to look up the account
220                mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false);
221                mFindMailboxTask.execute();
222            } else {
223                // Go by account id + type
224                accountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1);
225                mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, true);
226                mFindMailboxTask.execute();
227            }
228        }
229        // TODO set title to "account > mailbox (#unread)"
230    }
231
232    @Override
233    public void onPause() {
234        super.onPause();
235        mController.removeResultCallback(mControllerCallback);
236    }
237
238    @Override
239    public void onResume() {
240        super.onResume();
241        mController.addResultCallback(mControllerCallback);
242
243        // clear notifications here
244        NotificationManager notificationManager = (NotificationManager)
245                getSystemService(Context.NOTIFICATION_SERVICE);
246        notificationManager.cancel(MailService.NOTIFICATION_ID_NEW_MESSAGES);
247
248        // Exit immediately if the accounts list has changed (e.g. externally deleted)
249        if (Email.getNotifyUiAccountsChanged()) {
250            Welcome.actionStart(this);
251            finish();
252            return;
253        }
254    }
255
256    @Override
257    protected void onDestroy() {
258        super.onDestroy();
259
260        Utility.cancelTaskInterrupt(mFindMailboxTask);
261        mFindMailboxTask = null;
262        Utility.cancelTaskInterrupt(mSetTitleTask);
263        mSetTitleTask = null;
264    }
265
266    /**
267     * Called when the list fragment can't find mailbox/account.
268     */
269    public void onMailboxNotFound() {
270        finish();
271    }
272
273    public void onMessageOpen(final long messageId, final long mailboxId) {
274        final Context context = this; // Make the code shorter.
275        Utility.runAsync(new Runnable() {
276            public void run() {
277                EmailContent.Mailbox mailbox = EmailContent.Mailbox.restoreMailboxWithId(context,
278                        mailboxId);
279                if (mailbox == null) {
280                    return;
281                }
282
283                if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) {
284                    MessageCompose.actionEditDraft(context, messageId);
285                } else {
286                    final boolean disableReply = (mailbox.mType == EmailContent.Mailbox.TYPE_TRASH);
287                    // WARNING: here we pass getMailboxId(), which can be the negative id of
288                    // a compound mailbox, instead of the mailboxId of the particular message that
289                    // is opened.  This is to support the next/prev buttons on the message view
290                    // properly even for combined mailboxes.
291                    MessageView.actionView(context, messageId, mListFragment.getMailboxId(),
292                            disableReply);
293                }
294            }
295        });
296    }
297
298    public void onClick(View v) {
299        switch (v.getId()) {
300            case R.id.btn_read_unread:
301                mListFragment.onMultiToggleRead();
302                break;
303            case R.id.btn_multi_favorite:
304                mListFragment.onMultiToggleFavorite();
305                break;
306            case R.id.btn_multi_delete:
307                mListFragment.onMultiDelete();
308                break;
309            case R.id.account_title_button:
310                onAccounts();
311                break;
312        }
313    }
314
315    public void onAnimationEnd(Animation animation) {
316        mListFragment.updateListPosition();
317    }
318
319    public void onAnimationRepeat(Animation animation) {
320    }
321
322    public void onAnimationStart(Animation animation) {
323    }
324
325    @Override
326    public boolean onPrepareOptionsMenu(Menu menu) {
327        // Re-create menu every time.  (We may not know the mailbox id yet)
328        menu.clear();
329        if (mListFragment.isMagicMailbox()) {
330            getMenuInflater().inflate(R.menu.message_list_option_smart_folder, menu);
331        } else {
332            getMenuInflater().inflate(R.menu.message_list_option, menu);
333        }
334        boolean showDeselect = mListFragment.getSelectedCount() > 0;
335        menu.setGroupVisible(R.id.deselect_all_group, showDeselect);
336        return true;
337    }
338
339    @Override
340    public boolean onOptionsItemSelected(MenuItem item) {
341        switch (item.getItemId()) {
342            case R.id.refresh:
343                mListFragment.onRefresh();
344                return true;
345            case R.id.folders:
346                onFolders();
347                return true;
348            case R.id.accounts:
349                onAccounts();
350                return true;
351            case R.id.compose:
352                onCompose();
353                return true;
354            case R.id.account_settings:
355                onEditAccount();
356                return true;
357            case R.id.deselect_all:
358                mListFragment.onDeselectAll();
359                return true;
360            default:
361                return super.onOptionsItemSelected(item);
362        }
363    }
364
365    private void onFolders() {
366        if (!mListFragment.isMagicMailbox()) { // Magic boxes don't have "folders" option.
367            // TODO smaller projection
368            Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mListFragment.getMailboxId());
369            if (mailbox != null) {
370                MailboxList.actionHandleAccount(this, mailbox.mAccountKey);
371                finish();
372            }
373        }
374    }
375
376    private void onAccounts() {
377        AccountFolderList.actionShowAccounts(this);
378        finish();
379    }
380
381    private void onCompose() {
382        MessageCompose.actionCompose(this, mListFragment.getAccountId());
383    }
384
385    private void onEditAccount() {
386        if (!mListFragment.isMagicMailbox()) { // Magic boxes don't have "accout settings" option.
387            AccountSettings.actionSettings(this, mListFragment.getAccountId());
388        }
389    }
390
391    /**
392     * Show multi-selection panel, if one or more messages are selected.   Button labels will be
393     * updated too.
394     */
395    public void onSelectionChanged() {
396        showMultiPanel(mListFragment.getSelectedCount() > 0);
397    }
398
399    private void updateFooterButtonNames () {
400        // Show "unread_action" when one or more read messages are selected.
401        if (mListFragment.doesSelectionContainReadMessage()) {
402            mReadUnreadButton.setText(R.string.unread_action);
403        } else {
404            mReadUnreadButton.setText(R.string.read_action);
405        }
406        // Show "set_star_action" when one or more un-starred messages are selected.
407        if (mListFragment.doesSelectionContainNonStarredMessage()) {
408            mFavoriteButton.setText(R.string.set_star_action);
409        } else {
410            mFavoriteButton.setText(R.string.remove_star_action);
411        }
412    }
413
414    /**
415     * Show or hide the panel of multi-select options
416     */
417    private void showMultiPanel(boolean show) {
418        if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) {
419            mMultiSelectPanel.setVisibility(View.VISIBLE);
420            Animation animation = AnimationUtils.loadAnimation(this, R.anim.footer_appear);
421            animation.setAnimationListener(this);
422            mMultiSelectPanel.startAnimation(animation);
423        } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) {
424            mMultiSelectPanel.setVisibility(View.GONE);
425            mMultiSelectPanel.startAnimation(
426                        AnimationUtils.loadAnimation(this, R.anim.footer_disappear));
427        }
428        if (show) {
429            updateFooterButtonNames();
430        }
431    }
432
433    /**
434     * Async task for finding a single mailbox by type (possibly even going to the network).
435     *
436     * This is much too complex, as implemented.  It uses this AsyncTask to check for a mailbox,
437     * then (if not found) a Controller call to refresh mailboxes from the server, and a handler
438     * to relaunch this task (a 2nd time) to read the results of the network refresh.  The core
439     * problem is that we have two different non-UI-thread jobs (reading DB and reading network)
440     * and two different paradigms for dealing with them.  Some unification would be needed here
441     * to make this cleaner.
442     *
443     * TODO: If this problem spreads to other operations, find a cleaner way to handle it.
444     */
445    private class FindMailboxTask extends AsyncTask<Void, Void, Long> {
446
447        private final long mAccountId;
448        private final int mMailboxType;
449        private final boolean mOkToRecurse;
450
451        private static final int ACTION_DEFAULT = 0;
452        private static final int SHOW_WELCOME_ACTIVITY = 1;
453        private static final int SHOW_SECURITY_ACTIVITY = 2;
454        private static final int START_NETWORK_LOOK_UP = 3;
455        private int mAction = ACTION_DEFAULT;
456
457        /**
458         * Special constructor to cache some local info
459         */
460        public FindMailboxTask(long accountId, int mailboxType, boolean okToRecurse) {
461            mAccountId = accountId;
462            mMailboxType = mailboxType;
463            mOkToRecurse = okToRecurse;
464        }
465
466        @Override
467        protected Long doInBackground(Void... params) {
468            // Quick check that account is not in security hold
469            if (mAccountId != -1 && Account.isSecurityHold(MessageList.this, mAccountId)) {
470                mAction = SHOW_SECURITY_ACTIVITY;
471                return Mailbox.NO_MAILBOX;
472            }
473            // See if we can find the requested mailbox in the DB.
474            long mailboxId = Mailbox.findMailboxOfType(MessageList.this, mAccountId, mMailboxType);
475            if (mailboxId == Mailbox.NO_MAILBOX) {
476                // Mailbox not found.  Does the account really exists?
477                final boolean accountExists = Account.isValidId(MessageList.this, mAccountId);
478                if (accountExists && mOkToRecurse) {
479                    // launch network lookup
480                    mAction = START_NETWORK_LOOK_UP;
481                } else {
482                    // We don't want to do the network lookup, or the account doesn't exist in the
483                    // first place.
484                    mAction = SHOW_WELCOME_ACTIVITY;
485                }
486            }
487            return mailboxId;
488        }
489
490        @Override
491        protected void onPostExecute(Long mailboxId) {
492            switch (mAction) {
493                case SHOW_SECURITY_ACTIVITY:
494                    // launch the security setup activity
495                    Intent i = AccountSecurity.actionUpdateSecurityIntent(
496                            MessageList.this, mAccountId);
497                    MessageList.this.startActivityForResult(i, REQUEST_SECURITY);
498                    return;
499                case SHOW_WELCOME_ACTIVITY:
500                    // Let the Welcome activity show the default screen.
501                    Welcome.actionStart(MessageList.this);
502                    finish();
503                    return;
504                case START_NETWORK_LOOK_UP:
505                    mControllerCallback.getWrappee().presetMailboxListCallback(
506                            mMailboxType, mAccountId);
507
508                    // TODO updateMailboxList accessed DB, so we shouldn't call on the UI thread,
509                    // but we should fix the Controller side.  (Other Controller methods too access
510                    // DB but are called on the UI thread.)
511                    mController.updateMailboxList(mAccountId);
512                    return;
513                default:
514                    // At this point, mailboxId != NO_MAILBOX
515                    mSetTitleTask = new SetTitleTask(mailboxId);
516                    mSetTitleTask.execute();
517                    mListFragment.openMailbox(mAccountId, mailboxId);
518                    return;
519            }
520        }
521    }
522
523    /**
524     * Handle the eventual result from the security update activity
525     *
526     * Note, this is extremely coarse, and it simply returns the user to the Accounts list.
527     * Anything more requires refactoring of this Activity.
528     */
529    @Override
530    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
531        switch (requestCode) {
532            case REQUEST_SECURITY:
533                onAccounts();
534        }
535        super.onActivityResult(requestCode, resultCode, data);
536    }
537
538    private class SetTitleTask extends AsyncTask<Void, Void, Object[]> {
539
540        private long mMailboxKey;
541
542        public SetTitleTask(long mailboxKey) {
543            mMailboxKey = mailboxKey;
544        }
545
546        @Override
547        protected Object[] doInBackground(Void... params) {
548            // Check special Mailboxes
549            int resIdSpecialMailbox = 0;
550            if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) {
551                resIdSpecialMailbox = R.string.account_folder_list_summary_inbox;
552            } else if (mMailboxKey == Mailbox.QUERY_ALL_FAVORITES) {
553                resIdSpecialMailbox = R.string.account_folder_list_summary_starred;
554            } else if (mMailboxKey == Mailbox.QUERY_ALL_DRAFTS) {
555                resIdSpecialMailbox = R.string.account_folder_list_summary_drafts;
556            } else if (mMailboxKey == Mailbox.QUERY_ALL_OUTBOX) {
557                resIdSpecialMailbox = R.string.account_folder_list_summary_outbox;
558            }
559            if (resIdSpecialMailbox != 0) {
560                return new Object[] {null, getString(resIdSpecialMailbox), 0};
561            }
562
563            String accountName = null;
564            String mailboxName = null;
565            String accountKey = null;
566            Cursor c = MessageList.this.mResolver.query(Mailbox.CONTENT_URI,
567                    MAILBOX_NAME_PROJECTION, ID_SELECTION,
568                    new String[] { Long.toString(mMailboxKey) }, null);
569            try {
570                if (c.moveToFirst()) {
571                    mailboxName = Utility.FolderProperties.getInstance(MessageList.this)
572                            .getDisplayName(c.getInt(MAILBOX_NAME_COLUMN_TYPE));
573                    if (mailboxName == null) {
574                        mailboxName = c.getString(MAILBOX_NAME_COLUMN_ID);
575                    }
576                    accountKey = c.getString(MAILBOX_NAME_COLUMN_ACCOUNT_KEY);
577                }
578            } finally {
579                c.close();
580            }
581            if (accountKey != null) {
582                c = MessageList.this.mResolver.query(Account.CONTENT_URI,
583                        ACCOUNT_NAME_PROJECTION, ID_SELECTION, new String[] { accountKey },
584                        null);
585                try {
586                    if (c.moveToFirst()) {
587                        accountName = c.getString(ACCOUNT_DISPLAY_NAME_COLUMN_ID);
588                    }
589                } finally {
590                    c.close();
591                }
592            }
593            int nAccounts = EmailContent.count(MessageList.this, Account.CONTENT_URI, null, null);
594            return new Object[] {accountName, mailboxName, nAccounts};
595        }
596
597        @Override
598        protected void onPostExecute(Object[] result) {
599            if (result == null) {
600                return;
601            }
602
603            final int nAccounts = (Integer) result[2];
604            if (result[0] != null) {
605                setTitleAccountName((String) result[0], nAccounts > 1);
606            }
607
608            if (result[1] != null) {
609                mLeftTitle.setText((String) result[1]);
610            }
611        }
612    }
613
614    private void setTitleAccountName(String accountName, boolean showAccountsButton) {
615        TextView accountsButton = (TextView) findViewById(R.id.account_title_button);
616        TextView textPlain = (TextView) findViewById(R.id.title_right_text);
617        if (showAccountsButton) {
618            accountsButton.setVisibility(View.VISIBLE);
619            textPlain.setVisibility(View.GONE);
620            accountsButton.setText(accountName);
621        } else {
622            accountsButton.setVisibility(View.GONE);
623            textPlain.setVisibility(View.VISIBLE);
624            textPlain.setText(accountName);
625        }
626    }
627
628    private void showProgressIcon(boolean show) {
629        int visibility = show ? View.VISIBLE : View.GONE;
630        mProgressIcon.setVisibility(visibility);
631        mListFragment.showProgressIcon(show);
632    }
633
634    private void lookupMailboxType(long accountId, int mailboxType) {
635        // kill running async task, if any
636        Utility.cancelTaskInterrupt(mFindMailboxTask);
637        // start new one.  do not recurse back to controller.
638        mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false);
639        mFindMailboxTask.execute();
640    }
641
642    private void showErrorBanner(String message) {
643        boolean isVisible = mErrorBanner.getVisibility() == View.VISIBLE;
644        if (message != null) {
645            mErrorBanner.setText(message);
646            if (!isVisible) {
647                mErrorBanner.setVisibility(View.VISIBLE);
648                mErrorBanner.startAnimation(
649                        AnimationUtils.loadAnimation(
650                                MessageList.this, R.anim.header_appear));
651            }
652        } else {
653            if (isVisible) {
654                mErrorBanner.setVisibility(View.GONE);
655                mErrorBanner.startAnimation(
656                        AnimationUtils.loadAnimation(
657                                MessageList.this, R.anim.header_disappear));
658            }
659        }
660    }
661
662    /**
663     * Controller results listener.  We wrap it with {@link ControllerResultUiThreadWrapper},
664     * so all methods are called on the UI thread.
665     */
666    private class ControllerResults extends Controller.Result {
667
668        // This is used to alter the connection banner operation for sending messages
669        MessagingException mSendMessageException;
670
671        // These values are set by FindMailboxTask.
672        private int mWaitForMailboxType = -1;
673        private long mWaitForMailboxAccount = -1;
674
675        public void presetMailboxListCallback(int mailboxType, long accountId) {
676            mWaitForMailboxType = mailboxType;
677            mWaitForMailboxAccount = accountId;
678        }
679
680        @Override
681        public void updateMailboxListCallback(MessagingException result,
682                long accountKey, int progress) {
683            // updateMailboxList is never the end goal in MessageList, so we don't show
684            // these errors.  There are a couple of corner cases that we miss reporting, but
685            // this is better than reporting a number of non-problem intermediate states.
686            // updateBanner(result, progress, mMailboxId);
687
688            updateProgress(result, progress);
689            if (progress == 100 && accountKey == mWaitForMailboxAccount) {
690                mWaitForMailboxAccount = -1;
691                lookupMailboxType(accountKey, mWaitForMailboxType);
692            }
693        }
694
695        // TODO check accountKey and only react to relevant notifications
696        @Override
697        public void updateMailboxCallback(MessagingException result, long accountKey,
698                long mailboxKey, int progress, int numNewMessages) {
699            updateBanner(result, progress, mailboxKey);
700            if (result != null || progress == 100) {
701                Email.updateMailboxRefreshTime(mailboxKey);
702            }
703            updateProgress(result, progress);
704        }
705
706        /**
707         * We alter the updateBanner hysteresis here to capture any failures and handle
708         * them just once at the end.  This callback is overly overloaded:
709         *  result == null, messageId == -1, progress == 0:     start batch send
710         *  result == null, messageId == xx, progress == 0:     start sending one message
711         *  result == xxxx, messageId == xx, progress == 0;     failed sending one message
712         *  result == null, messageId == -1, progres == 100;    finish sending batch
713         */
714        @Override
715        public void sendMailCallback(MessagingException result, long accountId, long messageId,
716                int progress) {
717            if (mListFragment.isOutbox()) {
718                // reset captured error when we start sending one or more messages
719                if (messageId == -1 && result == null && progress == 0) {
720                    mSendMessageException = null;
721                }
722                // capture first exception that comes along
723                if (result != null && mSendMessageException == null) {
724                    mSendMessageException = result;
725                }
726                // if we're completing the sequence, change the banner state
727                if (messageId == -1 && progress == 100) {
728                    updateBanner(mSendMessageException, progress, mListFragment.getMailboxId());
729                }
730                // always update the spinner, which has less state to worry about
731                updateProgress(result, progress);
732            }
733        }
734
735        private void updateProgress(MessagingException result, int progress) {
736            showProgressIcon(result == null && progress < 100);
737        }
738
739        /**
740         * Show or hide the connection error banner, and convert the various MessagingException
741         * variants into localizable text.  There is hysteresis in the show/hide logic:  Once shown,
742         * the banner will remain visible until some progress is made on the connection.  The
743         * goal is to keep it from flickering during retries in a bad connection state.
744         *
745         * @param result
746         * @param progress
747         */
748        private void updateBanner(MessagingException result, int progress, long mailboxKey) {
749            if (mailboxKey != mListFragment.getMailboxId()) {
750                return;
751            }
752            if (result != null) {
753                int id = R.string.status_network_error;
754                if (result instanceof AuthenticationFailedException) {
755                    id = R.string.account_setup_failed_dlg_auth_message;
756                } else if (result instanceof CertificateValidationException) {
757                    id = R.string.account_setup_failed_dlg_certificate_message;
758                } else {
759                    switch (result.getExceptionType()) {
760                        case MessagingException.IOERROR:
761                            id = R.string.account_setup_failed_ioerror;
762                            break;
763                        case MessagingException.TLS_REQUIRED:
764                            id = R.string.account_setup_failed_tls_required;
765                            break;
766                        case MessagingException.AUTH_REQUIRED:
767                            id = R.string.account_setup_failed_auth_required;
768                            break;
769                        case MessagingException.GENERAL_SECURITY:
770                            id = R.string.account_setup_failed_security;
771                            break;
772                        // TODO Generate a unique string for this case, which is the case
773                        // where the security policy needs to be updated.
774                        case MessagingException.SECURITY_POLICIES_REQUIRED:
775                            id = R.string.account_setup_failed_security;
776                            break;
777                    }
778                }
779                showErrorBanner(getString(id));
780            } else if (progress > 0) {
781                showErrorBanner(null);
782            }
783        }
784    }
785}
786