MessageList.java revision 899c5b866192a4c4a12413446d10e5d98dbf94fa
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.NotificationController;
23import com.android.email.R;
24import com.android.email.Utility;
25import com.android.email.activity.setup.AccountSecurity;
26import com.android.email.activity.setup.AccountSettingsXL;
27import com.android.email.mail.MessagingException;
28import com.android.email.provider.EmailContent;
29import com.android.email.provider.EmailContent.Account;
30import com.android.email.provider.EmailContent.AccountColumns;
31import com.android.email.provider.EmailContent.Mailbox;
32import com.android.email.provider.EmailContent.MailboxColumns;
33
34import android.app.Activity;
35import android.content.ContentResolver;
36import android.content.Context;
37import android.content.Intent;
38import android.database.Cursor;
39import android.net.Uri;
40import android.os.AsyncTask;
41import android.os.Bundle;
42import android.os.Handler;
43import android.view.Menu;
44import android.view.MenuItem;
45import android.view.View;
46import android.view.View.OnClickListener;
47import android.view.animation.Animation;
48import android.view.animation.Animation.AnimationListener;
49import android.view.animation.AnimationUtils;
50import android.widget.Button;
51import android.widget.ProgressBar;
52import android.widget.TextView;
53
54public class MessageList extends Activity implements OnClickListener,
55        AnimationListener, MessageListFragment.Callback {
56    // Intent extras (internal to this activity)
57    private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID";
58    private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE";
59    private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID";
60
61    private static final int REQUEST_SECURITY = 0;
62
63    // UI support
64    private MessageListFragment mListFragment;
65    private View mMultiSelectPanel;
66    private Button mReadUnreadButton;
67    private Button mFavoriteButton;
68    private Button mDeleteButton;
69    private TextView mErrorBanner;
70
71    private final Controller mController = Controller.getInstance(getApplication());
72    private ControllerResultUiThreadWrapper<ControllerResults> mControllerCallback;
73
74    private TextView mLeftTitle;
75    private ProgressBar mProgressIcon;
76
77    // DB access
78    private ContentResolver mResolver;
79    private SetTitleTask mSetTitleTask;
80
81    private MailboxFinder mMailboxFinder;
82    private MailboxFinderCallback mMailboxFinderCallback = new MailboxFinderCallback();
83
84    private static final int MAILBOX_NAME_COLUMN_ID = 0;
85    private static final int MAILBOX_NAME_COLUMN_ACCOUNT_KEY = 1;
86    private static final int MAILBOX_NAME_COLUMN_TYPE = 2;
87    private static final String[] MAILBOX_NAME_PROJECTION = new String[] {
88            MailboxColumns.DISPLAY_NAME, MailboxColumns.ACCOUNT_KEY,
89            MailboxColumns.TYPE};
90
91    private static final int ACCOUNT_DISPLAY_NAME_COLUMN_ID = 0;
92    private static final String[] ACCOUNT_NAME_PROJECTION = new String[] {
93            AccountColumns.DISPLAY_NAME };
94
95    private static final String ID_SELECTION = EmailContent.RECORD_ID + "=?";
96
97    /* package */ MessageListFragment getListFragmentForTest() {
98        return mListFragment;
99    }
100
101    /**
102     * Open a specific mailbox.
103     *
104     * TODO This should just shortcut to a more generic version that can accept a list of
105     * accounts/mailboxes (e.g. merged inboxes).
106     *
107     * @param context
108     * @param id mailbox key
109     */
110    public static void actionHandleMailbox(Context context, long id) {
111        context.startActivity(createIntent(context, -1, id, -1));
112    }
113
114    /**
115     * Open a specific mailbox by account & type
116     *
117     * @param context The caller's context (for generating an intent)
118     * @param accountId The account to open
119     * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX)
120     */
121    public static void actionHandleAccount(Context context, long accountId, int mailboxType) {
122        context.startActivity(createIntent(context, accountId, -1, mailboxType));
123    }
124
125    /**
126     * Open the inbox of the account with a UUID.  It's used to handle old style
127     * (Android <= 1.6) desktop shortcut intents.
128     */
129    public static void actionOpenAccountInboxUuid(Context context, String accountUuid) {
130        Intent i = createIntent(context, -1, -1, Mailbox.TYPE_INBOX);
131        i.setData(Account.getShortcutSafeUriFromUuid(accountUuid));
132        context.startActivity(i);
133    }
134
135    /**
136     * Return an intent to open a specific mailbox by account & type.
137     *
138     * @param context The caller's context (for generating an intent)
139     * @param accountId The account to open, or -1
140     * @param mailboxId the ID of the mailbox to open, or -1
141     * @param mailboxType the type of mailbox to open (e.g. @see Mailbox.TYPE_INBOX) or -1
142     */
143    public static Intent createIntent(Context context, long accountId, long mailboxId,
144            int mailboxType) {
145        Intent intent = new Intent(context, MessageList.class);
146        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
147        if (accountId != -1) intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
148        if (mailboxId != -1) intent.putExtra(EXTRA_MAILBOX_ID, mailboxId);
149        if (mailboxType != -1) intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType);
150        return intent;
151    }
152
153    /**
154     * Create and return an intent for a desktop shortcut for an account.
155     *
156     * @param context Calling context for building the intent
157     * @param account The account of interest
158     * @param mailboxType The folder name to open (typically Mailbox.TYPE_INBOX)
159     * @return an Intent which can be used to view that account
160     */
161    public static Intent createAccountIntentForShortcut(Context context, Account account,
162            int mailboxType) {
163        Intent i = createIntent(context, -1, -1, mailboxType);
164        i.setData(account.getShortcutSafeUri());
165        return i;
166    }
167
168    @Override
169    public void onCreate(Bundle icicle) {
170        super.onCreate(icicle);
171        setContentView(R.layout.message_list);
172
173        mControllerCallback = new ControllerResultUiThreadWrapper<ControllerResults>(
174                new Handler(), new ControllerResults());
175        mListFragment = (MessageListFragment) findFragmentById(R.id.message_list_fragment);
176        mMultiSelectPanel = findViewById(R.id.footer_organize);
177        mReadUnreadButton = (Button) findViewById(R.id.btn_read_unread);
178        mFavoriteButton = (Button) findViewById(R.id.btn_multi_favorite);
179        mDeleteButton = (Button) findViewById(R.id.btn_multi_delete);
180        mLeftTitle = (TextView) findViewById(R.id.title_left_text);
181        mProgressIcon = (ProgressBar) findViewById(R.id.title_progress_icon);
182        mErrorBanner = (TextView) findViewById(R.id.connection_error_text);
183
184        mReadUnreadButton.setOnClickListener(this);
185        mFavoriteButton.setOnClickListener(this);
186        mDeleteButton.setOnClickListener(this);
187        ((Button) findViewById(R.id.account_title_button)).setOnClickListener(this);
188
189        mListFragment.setCallback(this);
190
191        mResolver = getContentResolver();
192
193        // Show the appropriate account/mailbox specified by an {@link Intent}.
194        selectAccountAndMailbox(getIntent());
195    }
196
197    /**
198     * Show the appropriate account/mailbox specified by an {@link Intent}.
199     */
200    private void selectAccountAndMailbox(Intent intent) {
201        long mailboxId = intent.getLongExtra(EXTRA_MAILBOX_ID, -1);
202        if (mailboxId != -1) {
203            // Specific mailbox ID was provided - go directly to it
204            mSetTitleTask = new SetTitleTask(mailboxId);
205            mSetTitleTask.execute();
206            mListFragment.openMailbox(mailboxId);
207        } else {
208            int mailboxType = intent.getIntExtra(EXTRA_MAILBOX_TYPE, Mailbox.TYPE_INBOX);
209            Uri uri = intent.getData();
210            // TODO Possible ANR.  getAccountIdFromShortcutSafeUri accesses DB.
211            long accountId = (uri == null) ? -1
212                    : Account.getAccountIdFromShortcutSafeUri(this, uri);
213            if (accountId == -1) {
214                accountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1);
215            }
216            if (accountId == -1) {
217                launchWelcomeAndFinish();
218                return;
219            }
220            mMailboxFinder = new MailboxFinder(this, accountId, mailboxType,
221                    mMailboxFinderCallback);
222            mMailboxFinder.startLookup();
223        }
224        // TODO set title to "account > mailbox (#unread)"
225    }
226
227    @Override
228    public void onPause() {
229        super.onPause();
230        mController.removeResultCallback(mControllerCallback);
231    }
232
233    @Override
234    public void onResume() {
235        super.onResume();
236        mController.addResultCallback(mControllerCallback);
237
238        // clear notifications here
239        NotificationController.getInstance(this).cancelNewMessageNotification(
240                mListFragment.getAccountId());
241
242        // Exit immediately if the accounts list has changed (e.g. externally deleted)
243        if (Email.getNotifyUiAccountsChanged()) {
244            Welcome.actionStart(this);
245            finish();
246            return;
247        }
248    }
249
250    @Override
251    protected void onDestroy() {
252        super.onDestroy();
253
254        if (mMailboxFinder != null) {
255            mMailboxFinder.cancel();
256            mMailboxFinder = null;
257        }
258        Utility.cancelTaskInterrupt(mSetTitleTask);
259        mSetTitleTask = null;
260    }
261
262
263    private void launchWelcomeAndFinish() {
264        Welcome.actionStart(this);
265        finish();
266    }
267
268    /**
269     * Called when the list fragment can't find mailbox/account.
270     */
271    public void onMailboxNotFound() {
272        finish();
273    }
274
275    @Override
276    public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId, int type) {
277        if (type == MessageListFragment.Callback.TYPE_DRAFT) {
278            MessageCompose.actionEditDraft(this, messageId);
279        } else {
280            // WARNING: here we pass "listMailboxId", which can be the negative id of
281            // a compound mailbox, instead of the mailboxId of the particular message that
282            // is opened.  This is to support the next/prev buttons on the message view
283            // properly even for combined mailboxes.
284            MessageView.actionView(this, messageId, listMailboxId);
285        }
286    }
287
288    @Override
289    public void onEnterSelectionMode(boolean enter) {
290    }
291
292    public void onClick(View v) {
293        switch (v.getId()) {
294            case R.id.btn_read_unread:
295                mListFragment.onMultiToggleRead();
296                break;
297            case R.id.btn_multi_favorite:
298                mListFragment.onMultiToggleFavorite();
299                break;
300            case R.id.btn_multi_delete:
301                mListFragment.onMultiDelete();
302                break;
303            case R.id.account_title_button:
304                onAccounts();
305                break;
306        }
307    }
308
309    public void onAnimationEnd(Animation animation) {
310        // TODO: If the button panel hides the only selected item, scroll the list to make it
311        // visible again.
312    }
313
314    public void onAnimationRepeat(Animation animation) {
315    }
316
317    public void onAnimationStart(Animation animation) {
318    }
319
320    @Override
321    public boolean onPrepareOptionsMenu(Menu menu) {
322        // Re-create menu every time.  (We may not know the mailbox id yet)
323        menu.clear();
324        if (mListFragment.isMagicMailbox()) {
325            getMenuInflater().inflate(R.menu.message_list_option_smart_folder, menu);
326        } else {
327            getMenuInflater().inflate(R.menu.message_list_option, menu);
328        }
329        boolean showDeselect = mListFragment.getSelectedCount() > 0;
330        menu.setGroupVisible(R.id.deselect_all_group, showDeselect);
331        return true;
332    }
333
334    @Override
335    public boolean onOptionsItemSelected(MenuItem item) {
336        switch (item.getItemId()) {
337            case R.id.refresh:
338                mListFragment.onRefresh();
339                return true;
340            case R.id.folders:
341                onFolders();
342                return true;
343            case R.id.accounts:
344                onAccounts();
345                return true;
346            case R.id.compose:
347                onCompose();
348                return true;
349            case R.id.account_settings:
350                onEditAccount();
351                return true;
352            case R.id.deselect_all:
353                mListFragment.onDeselectAll();
354                return true;
355            default:
356                return super.onOptionsItemSelected(item);
357        }
358    }
359
360    private void onFolders() {
361        if (!mListFragment.isMagicMailbox()) { // Magic boxes don't have "folders" option.
362            // TODO smaller projection
363            Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mListFragment.getMailboxId());
364            if (mailbox != null) {
365                MailboxList.actionHandleAccount(this, mailbox.mAccountKey);
366                finish();
367            }
368        }
369    }
370
371    private void onAccounts() {
372        AccountFolderList.actionShowAccounts(this);
373        finish();
374    }
375
376    private void onCompose() {
377        MessageCompose.actionCompose(this, mListFragment.getAccountId());
378    }
379
380    private void onEditAccount() {
381        AccountSettingsXL.actionSettings(this, mListFragment.getAccountId());
382    }
383
384    /**
385     * Show multi-selection panel, if one or more messages are selected.   Button labels will be
386     * updated too.
387     *
388     * @deprecated not used any longer.  remove them.
389     */
390    public void onSelectionChanged() {
391        showMultiPanel(mListFragment.getSelectedCount() > 0);
392    }
393
394    /**
395     * @deprecated not used any longer.  remove them.  (with associated resources, strings,
396     * members, etc)
397     */
398    private void updateFooterButtonNames () {
399        // Show "unread_action" when one or more read messages are selected.
400        if (mListFragment.doesSelectionContainReadMessage()) {
401            mReadUnreadButton.setText(R.string.unread_action);
402        } else {
403            mReadUnreadButton.setText(R.string.read_action);
404        }
405        // Show "set_star_action" when one or more un-starred messages are selected.
406        if (mListFragment.doesSelectionContainNonStarredMessage()) {
407            mFavoriteButton.setText(R.string.set_star_action);
408        } else {
409            mFavoriteButton.setText(R.string.remove_star_action);
410        }
411    }
412
413    /**
414     * Show or hide the panel of multi-select options
415     *
416     * @deprecated not used any longer.  remove them.
417     */
418    private void showMultiPanel(boolean show) {
419        if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) {
420            mMultiSelectPanel.setVisibility(View.VISIBLE);
421            Animation animation = AnimationUtils.loadAnimation(this, R.anim.footer_appear);
422            animation.setAnimationListener(this);
423            mMultiSelectPanel.startAnimation(animation);
424        } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) {
425            mMultiSelectPanel.setVisibility(View.GONE);
426            mMultiSelectPanel.startAnimation(
427                        AnimationUtils.loadAnimation(this, R.anim.footer_disappear));
428        }
429        if (show) {
430            updateFooterButtonNames();
431        }
432    }
433
434    /**
435     * Handle the eventual result from the security update activity
436     *
437     * Note, this is extremely coarse, and it simply returns the user to the Accounts list.
438     * Anything more requires refactoring of this Activity.
439     */
440    @Override
441    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
442        switch (requestCode) {
443            case REQUEST_SECURITY:
444                onAccounts();
445        }
446        super.onActivityResult(requestCode, resultCode, data);
447    }
448
449    private class SetTitleTask extends AsyncTask<Void, Void, Object[]> {
450
451        private long mMailboxKey;
452
453        public SetTitleTask(long mailboxKey) {
454            mMailboxKey = mailboxKey;
455        }
456
457        @Override
458        protected Object[] doInBackground(Void... params) {
459            // Check special Mailboxes
460            int resIdSpecialMailbox = 0;
461            if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) {
462                resIdSpecialMailbox = R.string.account_folder_list_summary_inbox;
463            } else if (mMailboxKey == Mailbox.QUERY_ALL_FAVORITES) {
464                resIdSpecialMailbox = R.string.account_folder_list_summary_starred;
465            } else if (mMailboxKey == Mailbox.QUERY_ALL_DRAFTS) {
466                resIdSpecialMailbox = R.string.account_folder_list_summary_drafts;
467            } else if (mMailboxKey == Mailbox.QUERY_ALL_OUTBOX) {
468                resIdSpecialMailbox = R.string.account_folder_list_summary_outbox;
469            }
470            if (resIdSpecialMailbox != 0) {
471                return new Object[] {null, getString(resIdSpecialMailbox), 0};
472            }
473
474            String accountName = null;
475            String mailboxName = null;
476            String accountKey = null;
477            Cursor c = MessageList.this.mResolver.query(Mailbox.CONTENT_URI,
478                    MAILBOX_NAME_PROJECTION, ID_SELECTION,
479                    new String[] { Long.toString(mMailboxKey) }, null);
480            try {
481                if (c.moveToFirst()) {
482                    mailboxName = Utility.FolderProperties.getInstance(MessageList.this)
483                            .getDisplayName(c.getInt(MAILBOX_NAME_COLUMN_TYPE));
484                    if (mailboxName == null) {
485                        mailboxName = c.getString(MAILBOX_NAME_COLUMN_ID);
486                    }
487                    accountKey = c.getString(MAILBOX_NAME_COLUMN_ACCOUNT_KEY);
488                }
489            } finally {
490                c.close();
491            }
492            if (accountKey != null) {
493                c = MessageList.this.mResolver.query(Account.CONTENT_URI,
494                        ACCOUNT_NAME_PROJECTION, ID_SELECTION, new String[] { accountKey },
495                        null);
496                try {
497                    if (c.moveToFirst()) {
498                        accountName = c.getString(ACCOUNT_DISPLAY_NAME_COLUMN_ID);
499                    }
500                } finally {
501                    c.close();
502                }
503            }
504            int nAccounts = EmailContent.count(MessageList.this, Account.CONTENT_URI, null, null);
505            return new Object[] {accountName, mailboxName, nAccounts};
506        }
507
508        @Override
509        protected void onPostExecute(Object[] result) {
510            if (result == null) {
511                return;
512            }
513
514            final int nAccounts = (Integer) result[2];
515            if (result[0] != null) {
516                setTitleAccountName((String) result[0], nAccounts > 1);
517            }
518
519            if (result[1] != null) {
520                mLeftTitle.setText((String) result[1]);
521            }
522        }
523    }
524
525    private void setTitleAccountName(String accountName, boolean showAccountsButton) {
526        TextView accountsButton = (TextView) findViewById(R.id.account_title_button);
527        TextView textPlain = (TextView) findViewById(R.id.title_right_text);
528        if (showAccountsButton) {
529            accountsButton.setVisibility(View.VISIBLE);
530            textPlain.setVisibility(View.GONE);
531            accountsButton.setText(accountName);
532        } else {
533            accountsButton.setVisibility(View.GONE);
534            textPlain.setVisibility(View.VISIBLE);
535            textPlain.setText(accountName);
536        }
537    }
538
539    private void showProgressIcon(boolean show) {
540        int visibility = show ? View.VISIBLE : View.GONE;
541        mProgressIcon.setVisibility(visibility);
542    }
543
544    private void showErrorBanner(String message) {
545        boolean isVisible = mErrorBanner.getVisibility() == View.VISIBLE;
546        if (message != null) {
547            mErrorBanner.setText(message);
548            if (!isVisible) {
549                mErrorBanner.setVisibility(View.VISIBLE);
550                mErrorBanner.startAnimation(
551                        AnimationUtils.loadAnimation(
552                                MessageList.this, R.anim.header_appear));
553            }
554        } else {
555            if (isVisible) {
556                mErrorBanner.setVisibility(View.GONE);
557                mErrorBanner.startAnimation(
558                        AnimationUtils.loadAnimation(
559                                MessageList.this, R.anim.header_disappear));
560            }
561        }
562    }
563
564    /**
565     * Controller results listener.  We wrap it with {@link ControllerResultUiThreadWrapper},
566     * so all methods are called on the UI thread.
567     */
568    private class ControllerResults extends Controller.Result {
569
570        // This is used to alter the connection banner operation for sending messages
571        private MessagingException mSendMessageException;
572
573        // TODO check accountKey and only react to relevant notifications
574        @Override
575        public void updateMailboxCallback(MessagingException result, long accountKey,
576                long mailboxKey, int progress, int numNewMessages) {
577            updateBanner(result, progress, mailboxKey);
578            updateProgress(result, progress);
579        }
580
581        /**
582         * We alter the updateBanner hysteresis here to capture any failures and handle
583         * them just once at the end.  This callback is overly overloaded:
584         *  result == null, messageId == -1, progress == 0:     start batch send
585         *  result == null, messageId == xx, progress == 0:     start sending one message
586         *  result == xxxx, messageId == xx, progress == 0;     failed sending one message
587         *  result == null, messageId == -1, progres == 100;    finish sending batch
588         */
589        @Override
590        public void sendMailCallback(MessagingException result, long accountId, long messageId,
591                int progress) {
592            if (mListFragment.isOutbox()) {
593                // reset captured error when we start sending one or more messages
594                if (messageId == -1 && result == null && progress == 0) {
595                    mSendMessageException = null;
596                }
597                // capture first exception that comes along
598                if (result != null && mSendMessageException == null) {
599                    mSendMessageException = result;
600                }
601                // if we're completing the sequence, change the banner state
602                if (messageId == -1 && progress == 100) {
603                    updateBanner(mSendMessageException, progress, mListFragment.getMailboxId());
604                }
605                // always update the spinner, which has less state to worry about
606                updateProgress(result, progress);
607            }
608        }
609
610        private void updateProgress(MessagingException result, int progress) {
611            showProgressIcon(result == null && progress < 100);
612        }
613
614        /**
615         * Show or hide the connection error banner, and convert the various MessagingException
616         * variants into localizable text.  There is hysteresis in the show/hide logic:  Once shown,
617         * the banner will remain visible until some progress is made on the connection.  The
618         * goal is to keep it from flickering during retries in a bad connection state.
619         *
620         * @param result
621         * @param progress
622         */
623        private void updateBanner(MessagingException result, int progress, long mailboxKey) {
624            if (mailboxKey != mListFragment.getMailboxId()) {
625                return;
626            }
627            if (result != null) {
628                showErrorBanner(result.getUiErrorMessage(MessageList.this));
629            } else if (progress > 0) {
630                showErrorBanner(null);
631            }
632        }
633    }
634
635    private class MailboxFinderCallback implements MailboxFinder.Callback {
636        @Override
637        public void onMailboxFound(long accountId, long mailboxId) {
638            mSetTitleTask = new SetTitleTask(mailboxId);
639            mSetTitleTask.execute();
640            mListFragment.openMailbox(mailboxId);
641        }
642
643        @Override
644        public void onAccountNotFound() {
645            // Let the Welcome activity show the default screen.
646            launchWelcomeAndFinish();
647        }
648
649        @Override
650        public void onMailboxNotFound(long accountId) {
651            // Let the Welcome activity show the default screen.
652            launchWelcomeAndFinish();
653        }
654
655        @Override
656        public void onAccountSecurityHold(long accountId) {
657            // launch the security setup activity
658            Intent i = AccountSecurity.actionUpdateSecurityIntent(
659                    MessageList.this, accountId);
660            MessageList.this.startActivityForResult(i, REQUEST_SECURITY);
661        }
662    }
663}
664