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