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