MessageList.java revision 241f18f237171a804f849262533d3cddb08e45c7
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        // Exit immediately if the accounts list has changed (e.g. externally deleted)
239        if (Email.getNotifyUiAccountsChanged()) {
240            Welcome.actionStart(this);
241            finish();
242            return;
243        }
244    }
245
246    @Override
247    protected void onDestroy() {
248        super.onDestroy();
249
250        if (mMailboxFinder != null) {
251            mMailboxFinder.cancel();
252            mMailboxFinder = null;
253        }
254        Utility.cancelTaskInterrupt(mSetTitleTask);
255        mSetTitleTask = null;
256    }
257
258
259    private void launchWelcomeAndFinish() {
260        Welcome.actionStart(this);
261        finish();
262    }
263
264    /**
265     * Called when the list fragment can't find mailbox/account.
266     */
267    public void onMailboxNotFound() {
268        finish();
269    }
270
271    @Override
272    public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId, int type) {
273        if (type == MessageListFragment.Callback.TYPE_DRAFT) {
274            MessageCompose.actionEditDraft(this, messageId);
275        } else {
276            // WARNING: here we pass "listMailboxId", which can be the negative id of
277            // a compound mailbox, instead of the mailboxId of the particular message that
278            // is opened.  This is to support the next/prev buttons on the message view
279            // properly even for combined mailboxes.
280            MessageView.actionView(this, messageId, listMailboxId);
281        }
282    }
283
284    @Override
285    public void onEnterSelectionMode(boolean enter) {
286    }
287
288    public void onClick(View v) {
289        switch (v.getId()) {
290            case R.id.btn_read_unread:
291                mListFragment.onMultiToggleRead();
292                break;
293            case R.id.btn_multi_favorite:
294                mListFragment.onMultiToggleFavorite();
295                break;
296            case R.id.btn_multi_delete:
297                mListFragment.onMultiDelete();
298                break;
299            case R.id.account_title_button:
300                onAccounts();
301                break;
302        }
303    }
304
305    public void onAnimationEnd(Animation animation) {
306        // TODO: If the button panel hides the only selected item, scroll the list to make it
307        // visible again.
308    }
309
310    public void onAnimationRepeat(Animation animation) {
311    }
312
313    public void onAnimationStart(Animation animation) {
314    }
315
316    @Override
317    public boolean onPrepareOptionsMenu(Menu menu) {
318        // Re-create menu every time.  (We may not know the mailbox id yet)
319        menu.clear();
320        if (mListFragment.isMagicMailbox()) {
321            getMenuInflater().inflate(R.menu.message_list_option_smart_folder, menu);
322        } else {
323            getMenuInflater().inflate(R.menu.message_list_option, menu);
324        }
325        boolean showDeselect = mListFragment.getSelectedCount() > 0;
326        menu.setGroupVisible(R.id.deselect_all_group, showDeselect);
327        return true;
328    }
329
330    @Override
331    public boolean onOptionsItemSelected(MenuItem item) {
332        switch (item.getItemId()) {
333            case R.id.refresh:
334                mListFragment.onRefresh();
335                return true;
336            case R.id.folders:
337                onFolders();
338                return true;
339            case R.id.accounts:
340                onAccounts();
341                return true;
342            case R.id.compose:
343                onCompose();
344                return true;
345            case R.id.account_settings:
346                onEditAccount();
347                return true;
348            case R.id.deselect_all:
349                mListFragment.onDeselectAll();
350                return true;
351            default:
352                return super.onOptionsItemSelected(item);
353        }
354    }
355
356    private void onFolders() {
357        if (!mListFragment.isMagicMailbox()) { // Magic boxes don't have "folders" option.
358            // TODO smaller projection
359            Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mListFragment.getMailboxId());
360            if (mailbox != null) {
361                MailboxList.actionHandleAccount(this, mailbox.mAccountKey);
362                finish();
363            }
364        }
365    }
366
367    private void onAccounts() {
368        AccountFolderList.actionShowAccounts(this);
369        finish();
370    }
371
372    private void onCompose() {
373        MessageCompose.actionCompose(this, mListFragment.getAccountId());
374    }
375
376    private void onEditAccount() {
377        AccountSettingsXL.actionSettings(this, mListFragment.getAccountId());
378    }
379
380    /**
381     * Show multi-selection panel, if one or more messages are selected.   Button labels will be
382     * updated too.
383     *
384     * @deprecated not used any longer.  remove them.
385     */
386    public void onSelectionChanged() {
387        showMultiPanel(mListFragment.getSelectedCount() > 0);
388    }
389
390    /**
391     * @deprecated not used any longer.  remove them.  (with associated resources, strings,
392     * members, etc)
393     */
394    private void updateFooterButtonNames () {
395        // Show "unread_action" when one or more read messages are selected.
396        if (mListFragment.doesSelectionContainReadMessage()) {
397            mReadUnreadButton.setText(R.string.unread_action);
398        } else {
399            mReadUnreadButton.setText(R.string.read_action);
400        }
401        // Show "set_star_action" when one or more un-starred messages are selected.
402        if (mListFragment.doesSelectionContainNonStarredMessage()) {
403            mFavoriteButton.setText(R.string.set_star_action);
404        } else {
405            mFavoriteButton.setText(R.string.remove_star_action);
406        }
407    }
408
409    /**
410     * Show or hide the panel of multi-select options
411     *
412     * @deprecated not used any longer.  remove them.
413     */
414    private void showMultiPanel(boolean show) {
415        if (show && mMultiSelectPanel.getVisibility() != View.VISIBLE) {
416            mMultiSelectPanel.setVisibility(View.VISIBLE);
417            Animation animation = AnimationUtils.loadAnimation(this, R.anim.footer_appear);
418            animation.setAnimationListener(this);
419            mMultiSelectPanel.startAnimation(animation);
420        } else if (!show && mMultiSelectPanel.getVisibility() != View.GONE) {
421            mMultiSelectPanel.setVisibility(View.GONE);
422            mMultiSelectPanel.startAnimation(
423                        AnimationUtils.loadAnimation(this, R.anim.footer_disappear));
424        }
425        if (show) {
426            updateFooterButtonNames();
427        }
428    }
429
430    /**
431     * Handle the eventual result from the security update activity
432     *
433     * Note, this is extremely coarse, and it simply returns the user to the Accounts list.
434     * Anything more requires refactoring of this Activity.
435     */
436    @Override
437    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
438        switch (requestCode) {
439            case REQUEST_SECURITY:
440                onAccounts();
441        }
442        super.onActivityResult(requestCode, resultCode, data);
443    }
444
445    private class SetTitleTask extends AsyncTask<Void, Void, Object[]> {
446
447        private long mMailboxKey;
448
449        public SetTitleTask(long mailboxKey) {
450            mMailboxKey = mailboxKey;
451        }
452
453        @Override
454        protected Object[] doInBackground(Void... params) {
455            // Check special Mailboxes
456            int resIdSpecialMailbox = 0;
457            if (mMailboxKey == Mailbox.QUERY_ALL_INBOXES) {
458                resIdSpecialMailbox = R.string.account_folder_list_summary_inbox;
459            } else if (mMailboxKey == Mailbox.QUERY_ALL_FAVORITES) {
460                resIdSpecialMailbox = R.string.account_folder_list_summary_starred;
461            } else if (mMailboxKey == Mailbox.QUERY_ALL_DRAFTS) {
462                resIdSpecialMailbox = R.string.account_folder_list_summary_drafts;
463            } else if (mMailboxKey == Mailbox.QUERY_ALL_OUTBOX) {
464                resIdSpecialMailbox = R.string.account_folder_list_summary_outbox;
465            }
466            if (resIdSpecialMailbox != 0) {
467                return new Object[] {null, getString(resIdSpecialMailbox), 0};
468            }
469
470            String accountName = null;
471            String mailboxName = null;
472            String accountKey = null;
473            Cursor c = MessageList.this.mResolver.query(Mailbox.CONTENT_URI,
474                    MAILBOX_NAME_PROJECTION, ID_SELECTION,
475                    new String[] { Long.toString(mMailboxKey) }, null);
476            try {
477                if (c.moveToFirst()) {
478                    mailboxName = Utility.FolderProperties.getInstance(MessageList.this)
479                            .getDisplayName(c.getInt(MAILBOX_NAME_COLUMN_TYPE));
480                    if (mailboxName == null) {
481                        mailboxName = c.getString(MAILBOX_NAME_COLUMN_ID);
482                    }
483                    accountKey = c.getString(MAILBOX_NAME_COLUMN_ACCOUNT_KEY);
484                }
485            } finally {
486                c.close();
487            }
488            if (accountKey != null) {
489                c = MessageList.this.mResolver.query(Account.CONTENT_URI,
490                        ACCOUNT_NAME_PROJECTION, ID_SELECTION, new String[] { accountKey },
491                        null);
492                try {
493                    if (c.moveToFirst()) {
494                        accountName = c.getString(ACCOUNT_DISPLAY_NAME_COLUMN_ID);
495                    }
496                } finally {
497                    c.close();
498                }
499            }
500            int nAccounts = EmailContent.count(MessageList.this, Account.CONTENT_URI, null, null);
501            return new Object[] {accountName, mailboxName, nAccounts};
502        }
503
504        @Override
505        protected void onPostExecute(Object[] result) {
506            if (result == null) {
507                return;
508            }
509
510            final int nAccounts = (Integer) result[2];
511            if (result[0] != null) {
512                setTitleAccountName((String) result[0], nAccounts > 1);
513            }
514
515            if (result[1] != null) {
516                mLeftTitle.setText((String) result[1]);
517            }
518        }
519    }
520
521    private void setTitleAccountName(String accountName, boolean showAccountsButton) {
522        TextView accountsButton = (TextView) findViewById(R.id.account_title_button);
523        TextView textPlain = (TextView) findViewById(R.id.title_right_text);
524        if (showAccountsButton) {
525            accountsButton.setVisibility(View.VISIBLE);
526            textPlain.setVisibility(View.GONE);
527            accountsButton.setText(accountName);
528        } else {
529            accountsButton.setVisibility(View.GONE);
530            textPlain.setVisibility(View.VISIBLE);
531            textPlain.setText(accountName);
532        }
533    }
534
535    private void showProgressIcon(boolean show) {
536        int visibility = show ? View.VISIBLE : View.GONE;
537        mProgressIcon.setVisibility(visibility);
538    }
539
540    private void showErrorBanner(String message) {
541        boolean isVisible = mErrorBanner.getVisibility() == View.VISIBLE;
542        if (message != null) {
543            mErrorBanner.setText(message);
544            if (!isVisible) {
545                mErrorBanner.setVisibility(View.VISIBLE);
546                mErrorBanner.startAnimation(
547                        AnimationUtils.loadAnimation(
548                                MessageList.this, R.anim.header_appear));
549            }
550        } else {
551            if (isVisible) {
552                mErrorBanner.setVisibility(View.GONE);
553                mErrorBanner.startAnimation(
554                        AnimationUtils.loadAnimation(
555                                MessageList.this, R.anim.header_disappear));
556            }
557        }
558    }
559
560    /**
561     * Controller results listener.  We wrap it with {@link ControllerResultUiThreadWrapper},
562     * so all methods are called on the UI thread.
563     */
564    private class ControllerResults extends Controller.Result {
565
566        // This is used to alter the connection banner operation for sending messages
567        private MessagingException mSendMessageException;
568
569        // TODO check accountKey and only react to relevant notifications
570        @Override
571        public void updateMailboxCallback(MessagingException result, long accountKey,
572                long mailboxKey, int progress, int numNewMessages) {
573            updateBanner(result, progress, mailboxKey);
574            updateProgress(result, progress);
575        }
576
577        /**
578         * We alter the updateBanner hysteresis here to capture any failures and handle
579         * them just once at the end.  This callback is overly overloaded:
580         *  result == null, messageId == -1, progress == 0:     start batch send
581         *  result == null, messageId == xx, progress == 0:     start sending one message
582         *  result == xxxx, messageId == xx, progress == 0;     failed sending one message
583         *  result == null, messageId == -1, progres == 100;    finish sending batch
584         */
585        @Override
586        public void sendMailCallback(MessagingException result, long accountId, long messageId,
587                int progress) {
588            if (mListFragment.isOutbox()) {
589                // reset captured error when we start sending one or more messages
590                if (messageId == -1 && result == null && progress == 0) {
591                    mSendMessageException = null;
592                }
593                // capture first exception that comes along
594                if (result != null && mSendMessageException == null) {
595                    mSendMessageException = result;
596                }
597                // if we're completing the sequence, change the banner state
598                if (messageId == -1 && progress == 100) {
599                    updateBanner(mSendMessageException, progress, mListFragment.getMailboxId());
600                }
601                // always update the spinner, which has less state to worry about
602                updateProgress(result, progress);
603            }
604        }
605
606        private void updateProgress(MessagingException result, int progress) {
607            showProgressIcon(result == null && progress < 100);
608        }
609
610        /**
611         * Show or hide the connection error banner, and convert the various MessagingException
612         * variants into localizable text.  There is hysteresis in the show/hide logic:  Once shown,
613         * the banner will remain visible until some progress is made on the connection.  The
614         * goal is to keep it from flickering during retries in a bad connection state.
615         *
616         * @param result
617         * @param progress
618         */
619        private void updateBanner(MessagingException result, int progress, long mailboxKey) {
620            if (mailboxKey != mListFragment.getMailboxId()) {
621                return;
622            }
623            if (result != null) {
624                showErrorBanner(result.getUiErrorMessage(MessageList.this));
625            } else if (progress > 0) {
626                showErrorBanner(null);
627            }
628        }
629    }
630
631    private class MailboxFinderCallback implements MailboxFinder.Callback {
632        @Override
633        public void onMailboxFound(long accountId, long mailboxId) {
634            mSetTitleTask = new SetTitleTask(mailboxId);
635            mSetTitleTask.execute();
636            mListFragment.openMailbox(mailboxId);
637        }
638
639        @Override
640        public void onAccountNotFound() {
641            // Let the Welcome activity show the default screen.
642            launchWelcomeAndFinish();
643        }
644
645        @Override
646        public void onMailboxNotFound(long accountId) {
647            // Let the Welcome activity show the default screen.
648            launchWelcomeAndFinish();
649        }
650
651        @Override
652        public void onAccountSecurityHold(long accountId) {
653            // launch the security setup activity
654            Intent i = AccountSecurity.actionUpdateSecurityIntent(
655                    MessageList.this, accountId);
656            MessageList.this.startActivityForResult(i, REQUEST_SECURITY);
657        }
658    }
659}
660