MessageList.java revision 8ade2fe010797be45d5c0f9023e5d76bcc3b50a8
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.list);
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 onMessageReply(long messageId) {
303        MessageCompose.actionReply(this, messageId, false);
304    }
305
306    public void onMessageReplyAll(long messageId) {
307        MessageCompose.actionReply(this, messageId, true);
308    }
309
310    public void onMessageForward(long messageId) {
311        MessageCompose.actionForward(this, messageId);
312    }
313
314    public void onClick(View v) {
315        switch (v.getId()) {
316            case R.id.btn_read_unread:
317                mListFragment.onMultiToggleRead();
318                break;
319            case R.id.btn_multi_favorite:
320                mListFragment.onMultiToggleFavorite();
321                break;
322            case R.id.btn_multi_delete:
323                mListFragment.onMultiDelete();
324                break;
325            case R.id.account_title_button:
326                onAccounts();
327                break;
328        }
329    }
330
331    public void onAnimationEnd(Animation animation) {
332        mListFragment.updateListPosition();
333    }
334
335    public void onAnimationRepeat(Animation animation) {
336    }
337
338    public void onAnimationStart(Animation animation) {
339    }
340
341    @Override
342    public boolean onPrepareOptionsMenu(Menu menu) {
343        // Re-create menu every time.  (We may not know the mailbox id yet)
344        menu.clear();
345        if (mListFragment.isMagicMailbox()) {
346            getMenuInflater().inflate(R.menu.message_list_option_smart_folder, menu);
347        } else {
348            getMenuInflater().inflate(R.menu.message_list_option, menu);
349        }
350        boolean showDeselect = mListFragment.getSelectedCount() > 0;
351        menu.setGroupVisible(R.id.deselect_all_group, showDeselect);
352        return true;
353    }
354
355    @Override
356    public boolean onOptionsItemSelected(MenuItem item) {
357        switch (item.getItemId()) {
358            case R.id.refresh:
359                mListFragment.onRefresh();
360                return true;
361            case R.id.folders:
362                onFolders();
363                return true;
364            case R.id.accounts:
365                onAccounts();
366                return true;
367            case R.id.compose:
368                onCompose();
369                return true;
370            case R.id.account_settings:
371                onEditAccount();
372                return true;
373            case R.id.deselect_all:
374                mListFragment.onDeselectAll();
375                return true;
376            default:
377                return super.onOptionsItemSelected(item);
378        }
379    }
380
381    private void onFolders() {
382        if (!mListFragment.isMagicMailbox()) { // Magic boxes don't have "folders" option.
383            // TODO smaller projection
384            Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mListFragment.getMailboxId());
385            if (mailbox != null) {
386                MailboxList.actionHandleAccount(this, mailbox.mAccountKey);
387                finish();
388            }
389        }
390    }
391
392    private void onAccounts() {
393        AccountFolderList.actionShowAccounts(this);
394        finish();
395    }
396
397    private void onCompose() {
398        MessageCompose.actionCompose(this, mListFragment.getAccountId());
399    }
400
401    private void onEditAccount() {
402        if (!mListFragment.isMagicMailbox()) { // Magic boxes don't have "accout settings" option.
403            AccountSettings.actionSettings(this, mListFragment.getAccountId());
404        }
405    }
406
407    /**
408     * Show multi-selection panel, if one or more messages are selected.   Button labels will be
409     * updated too.
410     */
411    public void onSelectionChanged() {
412        showMultiPanel(mListFragment.getSelectedCount() > 0);
413    }
414
415    private void updateFooterButtonNames () {
416        // Show "unread_action" when one or more read messages are selected.
417        if (mListFragment.doesSelectionContainReadMessage()) {
418            mReadUnreadButton.setText(R.string.unread_action);
419        } else {
420            mReadUnreadButton.setText(R.string.read_action);
421        }
422        // Show "set_star_action" when one or more un-starred messages are selected.
423        if (mListFragment.doesSelectionContainNonStarredMessage()) {
424            mFavoriteButton.setText(R.string.set_star_action);
425        } else {
426            mFavoriteButton.setText(R.string.remove_star_action);
427        }
428    }
429
430    /**
431     * Show or hide the panel of multi-select options
432     */
433    private void showMultiPanel(boolean show) {
434        if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) {
435            mMultiSelectPanel.setVisibility(View.VISIBLE);
436            Animation animation = AnimationUtils.loadAnimation(this, R.anim.footer_appear);
437            animation.setAnimationListener(this);
438            mMultiSelectPanel.startAnimation(animation);
439        } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) {
440            mMultiSelectPanel.setVisibility(View.GONE);
441            mMultiSelectPanel.startAnimation(
442                        AnimationUtils.loadAnimation(this, R.anim.footer_disappear));
443        }
444        if (show) {
445            updateFooterButtonNames();
446        }
447    }
448
449    /**
450     * Async task for finding a single mailbox by type (possibly even going to the network).
451     *
452     * This is much too complex, as implemented.  It uses this AsyncTask to check for a mailbox,
453     * then (if not found) a Controller call to refresh mailboxes from the server, and a handler
454     * to relaunch this task (a 2nd time) to read the results of the network refresh.  The core
455     * problem is that we have two different non-UI-thread jobs (reading DB and reading network)
456     * and two different paradigms for dealing with them.  Some unification would be needed here
457     * to make this cleaner.
458     *
459     * TODO: If this problem spreads to other operations, find a cleaner way to handle it.
460     */
461    private class FindMailboxTask extends AsyncTask<Void, Void, Long> {
462
463        private final long mAccountId;
464        private final int mMailboxType;
465        private final boolean mOkToRecurse;
466
467        private static final int ACTION_DEFAULT = 0;
468        private static final int SHOW_WELCOME_ACTIVITY = 1;
469        private static final int SHOW_SECURITY_ACTIVITY = 2;
470        private static final int START_NETWORK_LOOK_UP = 3;
471        private int mAction = ACTION_DEFAULT;
472
473        /**
474         * Special constructor to cache some local info
475         */
476        public FindMailboxTask(long accountId, int mailboxType, boolean okToRecurse) {
477            mAccountId = accountId;
478            mMailboxType = mailboxType;
479            mOkToRecurse = okToRecurse;
480        }
481
482        @Override
483        protected Long doInBackground(Void... params) {
484            // Quick check that account is not in security hold
485            if (mAccountId != -1 && isSecurityHold(mAccountId)) {
486                mAction = SHOW_SECURITY_ACTIVITY;
487                return Mailbox.NO_MAILBOX;
488            }
489            // See if we can find the requested mailbox in the DB.
490            long mailboxId = Mailbox.findMailboxOfType(MessageList.this, mAccountId, mMailboxType);
491            if (mailboxId == Mailbox.NO_MAILBOX) {
492                // Mailbox not found.  Does the account really exists?
493                final boolean accountExists = Account.isValidId(MessageList.this, mAccountId);
494                if (accountExists && mOkToRecurse) {
495                    // launch network lookup
496                    mAction = START_NETWORK_LOOK_UP;
497                } else {
498                    // We don't want to do the network lookup, or the account doesn't exist in the
499                    // first place.
500                    mAction = SHOW_WELCOME_ACTIVITY;
501                }
502            }
503            return mailboxId;
504        }
505
506        @Override
507        protected void onPostExecute(Long mailboxId) {
508            switch (mAction) {
509                case SHOW_SECURITY_ACTIVITY:
510                    // launch the security setup activity
511                    Intent i = AccountSecurity.actionUpdateSecurityIntent(
512                            MessageList.this, mAccountId);
513                    MessageList.this.startActivityForResult(i, REQUEST_SECURITY);
514                    return;
515                case SHOW_WELCOME_ACTIVITY:
516                    // Let the Welcome activity show the default screen.
517                    Welcome.actionStart(MessageList.this);
518                    finish();
519                    return;
520                case START_NETWORK_LOOK_UP:
521                    mControllerCallback.getWrappee().presetMailboxListCallback(
522                            mMailboxType, mAccountId);
523
524                    // TODO updateMailboxList accessed DB, so we shouldn't call on the UI thread,
525                    // but we should fix the Controller side.  (Other Controller methods too access
526                    // DB but are called on the UI thread.)
527                    mController.updateMailboxList(mAccountId);
528                    return;
529                default:
530                    // At this point, mailboxId != NO_MAILBOX
531                    mSetTitleTask = new SetTitleTask(mailboxId);
532                    mSetTitleTask.execute();
533                    mListFragment.openMailbox(mAccountId, mailboxId);
534                    return;
535            }
536        }
537    }
538
539    /**
540     * Check a single account for security hold status.  Do not call from UI thread.
541     */
542    private boolean isSecurityHold(long accountId) {
543        Cursor c = MessageList.this.getContentResolver().query(
544                ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
545                ACCOUNT_INFO_PROJECTION, null, null, null);
546        try {
547            if (c.moveToFirst()) {
548                int flags = c.getInt(ACCOUNT_INFO_COLUMN_FLAGS);
549                if ((flags & Account.FLAGS_SECURITY_HOLD) != 0) {
550                    return true;
551                }
552            }
553        } finally {
554            c.close();
555        }
556        return false;
557    }
558
559    /**
560     * Handle the eventual result from the security update activity
561     *
562     * Note, this is extremely coarse, and it simply returns the user to the Accounts list.
563     * Anything more requires refactoring of this Activity.
564     */
565    @Override
566    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
567        switch (requestCode) {
568            case REQUEST_SECURITY:
569                onAccounts();
570        }
571        super.onActivityResult(requestCode, resultCode, data);
572    }
573
574    private class SetTitleTask extends AsyncTask<Void, Void, Object[]> {
575
576        private long mMailboxKey;
577
578        public SetTitleTask(long mailboxKey) {
579            mMailboxKey = mailboxKey;
580        }
581
582        @Override
583        protected Object[] doInBackground(Void... params) {
584            // Check special Mailboxes
585            int resIdSpecialMailbox = 0;
586            if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) {
587                resIdSpecialMailbox = R.string.account_folder_list_summary_inbox;
588            } else if (mMailboxKey == Mailbox.QUERY_ALL_FAVORITES) {
589                resIdSpecialMailbox = R.string.account_folder_list_summary_starred;
590            } else if (mMailboxKey == Mailbox.QUERY_ALL_DRAFTS) {
591                resIdSpecialMailbox = R.string.account_folder_list_summary_drafts;
592            } else if (mMailboxKey == Mailbox.QUERY_ALL_OUTBOX) {
593                resIdSpecialMailbox = R.string.account_folder_list_summary_outbox;
594            }
595            if (resIdSpecialMailbox != 0) {
596                return new Object[] {null, getString(resIdSpecialMailbox), 0};
597            }
598
599            String accountName = null;
600            String mailboxName = null;
601            String accountKey = null;
602            Cursor c = MessageList.this.mResolver.query(Mailbox.CONTENT_URI,
603                    MAILBOX_NAME_PROJECTION, ID_SELECTION,
604                    new String[] { Long.toString(mMailboxKey) }, null);
605            try {
606                if (c.moveToFirst()) {
607                    mailboxName = Utility.FolderProperties.getInstance(MessageList.this)
608                            .getDisplayName(c.getInt(MAILBOX_NAME_COLUMN_TYPE));
609                    if (mailboxName == null) {
610                        mailboxName = c.getString(MAILBOX_NAME_COLUMN_ID);
611                    }
612                    accountKey = c.getString(MAILBOX_NAME_COLUMN_ACCOUNT_KEY);
613                }
614            } finally {
615                c.close();
616            }
617            if (accountKey != null) {
618                c = MessageList.this.mResolver.query(Account.CONTENT_URI,
619                        ACCOUNT_NAME_PROJECTION, ID_SELECTION, new String[] { accountKey },
620                        null);
621                try {
622                    if (c.moveToFirst()) {
623                        accountName = c.getString(ACCOUNT_DISPLAY_NAME_COLUMN_ID);
624                    }
625                } finally {
626                    c.close();
627                }
628            }
629            int nAccounts = EmailContent.count(MessageList.this, Account.CONTENT_URI, null, null);
630            return new Object[] {accountName, mailboxName, nAccounts};
631        }
632
633        @Override
634        protected void onPostExecute(Object[] result) {
635            if (result == null) {
636                return;
637            }
638
639            final int nAccounts = (Integer) result[2];
640            if (result[0] != null) {
641                setTitleAccountName((String) result[0], nAccounts > 1);
642            }
643
644            if (result[1] != null) {
645                mLeftTitle.setText((String) result[1]);
646            }
647        }
648    }
649
650    private void setTitleAccountName(String accountName, boolean showAccountsButton) {
651        TextView accountsButton = (TextView) findViewById(R.id.account_title_button);
652        TextView textPlain = (TextView) findViewById(R.id.title_right_text);
653        if (showAccountsButton) {
654            accountsButton.setVisibility(View.VISIBLE);
655            textPlain.setVisibility(View.GONE);
656            accountsButton.setText(accountName);
657        } else {
658            accountsButton.setVisibility(View.GONE);
659            textPlain.setVisibility(View.VISIBLE);
660            textPlain.setText(accountName);
661        }
662    }
663
664    private void showProgressIcon(boolean show) {
665        int visibility = show ? View.VISIBLE : View.GONE;
666        mProgressIcon.setVisibility(visibility);
667        mListFragment.showProgressIcon(show);
668    }
669
670    private void lookupMailboxType(long accountId, int mailboxType) {
671        // kill running async task, if any
672        Utility.cancelTaskInterrupt(mFindMailboxTask);
673        // start new one.  do not recurse back to controller.
674        mFindMailboxTask = new FindMailboxTask(accountId, mailboxType, false);
675        mFindMailboxTask.execute();
676    }
677
678    private void showErrorBanner(String message) {
679        boolean isVisible = mErrorBanner.getVisibility() == View.VISIBLE;
680        if (message != null) {
681            mErrorBanner.setText(message);
682            if (!isVisible) {
683                mErrorBanner.setVisibility(View.VISIBLE);
684                mErrorBanner.startAnimation(
685                        AnimationUtils.loadAnimation(
686                                MessageList.this, R.anim.header_appear));
687            }
688        } else {
689            if (isVisible) {
690                mErrorBanner.setVisibility(View.GONE);
691                mErrorBanner.startAnimation(
692                        AnimationUtils.loadAnimation(
693                                MessageList.this, R.anim.header_disappear));
694            }
695        }
696    }
697
698    /**
699     * Controller results listener.  We wrap it with {@link ControllerResultUiThreadWrapper},
700     * so all methods are called on the UI thread.
701     */
702    private class ControllerResults extends Controller.Result {
703
704        // This is used to alter the connection banner operation for sending messages
705        MessagingException mSendMessageException;
706
707        // These values are set by FindMailboxTask.
708        private int mWaitForMailboxType = -1;
709        private long mWaitForMailboxAccount = -1;
710
711        public void presetMailboxListCallback(int mailboxType, long accountId) {
712            mWaitForMailboxType = mailboxType;
713            mWaitForMailboxAccount = accountId;
714        }
715
716        @Override
717        public void updateMailboxListCallback(MessagingException result,
718                long accountKey, int progress) {
719            // updateMailboxList is never the end goal in MessageList, so we don't show
720            // these errors.  There are a couple of corner cases that we miss reporting, but
721            // this is better than reporting a number of non-problem intermediate states.
722            // updateBanner(result, progress, mMailboxId);
723
724            updateProgress(result, progress);
725            if (progress == 100 && accountKey == mWaitForMailboxAccount) {
726                mWaitForMailboxAccount = -1;
727                lookupMailboxType(accountKey, mWaitForMailboxType);
728            }
729        }
730
731        // TODO check accountKey and only react to relevant notifications
732        @Override
733        public void updateMailboxCallback(MessagingException result, long accountKey,
734                long mailboxKey, int progress, int numNewMessages) {
735            updateBanner(result, progress, mailboxKey);
736            if (result != null || progress == 100) {
737                Email.updateMailboxRefreshTime(mailboxKey);
738            }
739            updateProgress(result, progress);
740        }
741
742        /**
743         * We alter the updateBanner hysteresis here to capture any failures and handle
744         * them just once at the end.  This callback is overly overloaded:
745         *  result == null, messageId == -1, progress == 0:     start batch send
746         *  result == null, messageId == xx, progress == 0:     start sending one message
747         *  result == xxxx, messageId == xx, progress == 0;     failed sending one message
748         *  result == null, messageId == -1, progres == 100;    finish sending batch
749         */
750        @Override
751        public void sendMailCallback(MessagingException result, long accountId, long messageId,
752                int progress) {
753            if (mListFragment.isOutbox()) {
754                // reset captured error when we start sending one or more messages
755                if (messageId == -1 && result == null && progress == 0) {
756                    mSendMessageException = null;
757                }
758                // capture first exception that comes along
759                if (result != null && mSendMessageException == null) {
760                    mSendMessageException = result;
761                }
762                // if we're completing the sequence, change the banner state
763                if (messageId == -1 && progress == 100) {
764                    updateBanner(mSendMessageException, progress, mListFragment.getMailboxId());
765                }
766                // always update the spinner, which has less state to worry about
767                updateProgress(result, progress);
768            }
769        }
770
771        private void updateProgress(MessagingException result, int progress) {
772            showProgressIcon(result == null && progress < 100);
773        }
774
775        /**
776         * Show or hide the connection error banner, and convert the various MessagingException
777         * variants into localizable text.  There is hysteresis in the show/hide logic:  Once shown,
778         * the banner will remain visible until some progress is made on the connection.  The
779         * goal is to keep it from flickering during retries in a bad connection state.
780         *
781         * @param result
782         * @param progress
783         */
784        private void updateBanner(MessagingException result, int progress, long mailboxKey) {
785            if (mailboxKey != mListFragment.getMailboxId()) {
786                return;
787            }
788            if (result != null) {
789                int id = R.string.status_network_error;
790                if (result instanceof AuthenticationFailedException) {
791                    id = R.string.account_setup_failed_dlg_auth_message;
792                } else if (result instanceof CertificateValidationException) {
793                    id = R.string.account_setup_failed_dlg_certificate_message;
794                } else {
795                    switch (result.getExceptionType()) {
796                        case MessagingException.IOERROR:
797                            id = R.string.account_setup_failed_ioerror;
798                            break;
799                        case MessagingException.TLS_REQUIRED:
800                            id = R.string.account_setup_failed_tls_required;
801                            break;
802                        case MessagingException.AUTH_REQUIRED:
803                            id = R.string.account_setup_failed_auth_required;
804                            break;
805                        case MessagingException.GENERAL_SECURITY:
806                            id = R.string.account_setup_failed_security;
807                            break;
808                        // TODO Generate a unique string for this case, which is the case
809                        // where the security policy needs to be updated.
810                        case MessagingException.SECURITY_POLICIES_REQUIRED:
811                            id = R.string.account_setup_failed_security;
812                            break;
813                    }
814                }
815                showErrorBanner(getString(id));
816            } else if (progress > 0) {
817                showErrorBanner(null);
818            }
819        }
820    }
821}
822