MessageList.java revision 9e53322ee7b2a46cb762898845303cccec88fb50
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.MessagingExceptionStrings;
23import com.android.email.R;
24import com.android.email.activity.setup.AccountSecurity;
25import com.android.email.activity.setup.AccountSettingsXL;
26import com.android.emailcommon.mail.MessagingException;
27import com.android.emailcommon.provider.EmailContent.Account;
28import com.android.emailcommon.provider.EmailContent.Mailbox;
29
30import android.app.Activity;
31import android.content.Context;
32import android.content.Intent;
33import android.net.Uri;
34import android.os.Bundle;
35import android.os.Handler;
36import android.view.Menu;
37import android.view.MenuItem;
38import android.view.View;
39import android.view.animation.AnimationUtils;
40import android.widget.TextView;
41
42public class MessageList extends Activity implements MessageListFragment.Callback {
43    // Intent extras (internal to this activity)
44    private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity._ACCOUNT_ID";
45    private static final String EXTRA_MAILBOX_TYPE = "com.android.email.activity.MAILBOX_TYPE";
46    private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID";
47
48    private static final int REQUEST_SECURITY = 0;
49
50    // UI support
51    private MessageListFragment mListFragment;
52    private TextView mErrorBanner;
53
54    private final Controller mController = Controller.getInstance(getApplication());
55    private ControllerResultUiThreadWrapper<ControllerResults> mControllerCallback;
56
57    private MailboxFinder mMailboxFinder;
58    private final MailboxFinderCallback mMailboxFinderCallback = new MailboxFinderCallback();
59
60    /* package */ MessageListFragment getListFragmentForTest() {
61        return mListFragment;
62    }
63
64    /**
65     * Open a specific mailbox.
66     *
67     * TODO This should just shortcut to a more generic version that can accept a list of
68     * accounts/mailboxes (e.g. merged inboxes).
69     *
70     * @param context
71     * @param id mailbox key
72     */
73    public static void actionHandleMailbox(Context context, long id) {
74        context.startActivity(createIntent(context, -1, id, -1));
75    }
76
77    /**
78     * Open a specific mailbox by account & type
79     *
80     * @param context The caller's context (for generating an intent)
81     * @param accountId The account to open
82     * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX)
83     */
84    public static void actionHandleAccount(Context context, long accountId, int mailboxType) {
85        context.startActivity(createIntent(context, accountId, -1, mailboxType));
86    }
87
88    /**
89     * Open the inbox of the account with a UUID.  It's used to handle old style
90     * (Android <= 1.6) desktop shortcut intents.
91     */
92    public static void actionOpenAccountInboxUuid(Context context, String accountUuid) {
93        Intent i = createIntent(context, -1, -1, Mailbox.TYPE_INBOX);
94        i.setData(Account.getShortcutSafeUriFromUuid(accountUuid));
95        context.startActivity(i);
96    }
97
98    /**
99     * Return an intent to open a specific mailbox by account & type.
100     *
101     * @param context The caller's context (for generating an intent)
102     * @param accountId The account to open, or -1
103     * @param mailboxId the ID of the mailbox to open, or -1
104     * @param mailboxType the type of mailbox to open (e.g. @see Mailbox.TYPE_INBOX) or -1
105     */
106    public static Intent createIntent(Context context, long accountId, long mailboxId,
107            int mailboxType) {
108        Intent intent = new Intent(context, MessageList.class);
109        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
110        if (accountId != -1) intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
111        if (mailboxId != -1) intent.putExtra(EXTRA_MAILBOX_ID, mailboxId);
112        if (mailboxType != -1) intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType);
113        return intent;
114    }
115
116    /**
117     * Create and return an intent for a desktop shortcut for an account.
118     *
119     * @param context Calling context for building the intent
120     * @param account The account of interest
121     * @param mailboxType The folder name to open (typically Mailbox.TYPE_INBOX)
122     * @return an Intent which can be used to view that account
123     */
124    public static Intent createAccountIntentForShortcut(Context context, Account account,
125            int mailboxType) {
126        Intent i = createIntent(context, -1, -1, mailboxType);
127        i.setData(account.getShortcutSafeUri());
128        return i;
129    }
130
131    @Override
132    public void onCreate(Bundle icicle) {
133        super.onCreate(icicle);
134        ActivityHelper.debugSetWindowFlags(this);
135        setContentView(R.layout.message_list);
136
137        mControllerCallback = new ControllerResultUiThreadWrapper<ControllerResults>(
138                new Handler(), new ControllerResults());
139        mListFragment = (MessageListFragment) getFragmentManager()
140                .findFragmentById(R.id.message_list_fragment);
141        mErrorBanner = (TextView) findViewById(R.id.connection_error_text);
142
143        mListFragment.setCallback(this);
144
145        // Show the appropriate account/mailbox specified by an {@link Intent}.
146        selectAccountAndMailbox(getIntent());
147    }
148
149    /**
150     * Show the appropriate account/mailbox specified by an {@link Intent}.
151     */
152    private void selectAccountAndMailbox(Intent intent) {
153        long mailboxId = intent.getLongExtra(EXTRA_MAILBOX_ID, -1);
154        if (mailboxId != -1) {
155            mListFragment.openMailbox(mailboxId);
156        } else {
157            int mailboxType = intent.getIntExtra(EXTRA_MAILBOX_TYPE, Mailbox.TYPE_INBOX);
158            Uri uri = intent.getData();
159            // TODO Possible ANR.  getAccountIdFromShortcutSafeUri accesses DB.
160            long accountId = (uri == null) ? -1
161                    : Account.getAccountIdFromShortcutSafeUri(this, uri);
162            if (accountId == -1) {
163                accountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1);
164            }
165            if (accountId == -1) {
166                launchWelcomeAndFinish();
167                return;
168            }
169            mMailboxFinder = new MailboxFinder(this, accountId, mailboxType,
170                    mMailboxFinderCallback);
171            mMailboxFinder.startLookup();
172        }
173    }
174
175    @Override
176    public void onPause() {
177        super.onPause();
178        mController.removeResultCallback(mControllerCallback);
179    }
180
181    @Override
182    public void onResume() {
183        super.onResume();
184        mController.addResultCallback(mControllerCallback);
185
186        // Exit immediately if the accounts list has changed (e.g. externally deleted)
187        if (Email.getNotifyUiAccountsChanged()) {
188            Welcome.actionStart(this);
189            finish();
190            return;
191        }
192    }
193
194    @Override
195    protected void onDestroy() {
196        super.onDestroy();
197
198        if (mMailboxFinder != null) {
199            mMailboxFinder.cancel();
200            mMailboxFinder = null;
201        }
202    }
203
204
205    private void launchWelcomeAndFinish() {
206        Welcome.actionStart(this);
207        finish();
208    }
209
210    /**
211     * Called when the list fragment can't find mailbox/account.
212     */
213    public void onMailboxNotFound() {
214        finish();
215    }
216
217    @Override
218    public void onMessageOpen(long messageId, long messageMailboxId, long listMailboxId, int type) {
219        if (type == MessageListFragment.Callback.TYPE_DRAFT) {
220            MessageCompose.actionEditDraft(this, messageId);
221        } else {
222            // WARNING: here we pass "listMailboxId", which can be the negative id of
223            // a compound mailbox, instead of the mailboxId of the particular message that
224            // is opened.  This is to support the next/prev buttons on the message view
225            // properly even for combined mailboxes.
226            MessageView.actionView(this, messageId, listMailboxId);
227        }
228    }
229
230    @Override
231    public void onEnterSelectionMode(boolean enter) {
232    }
233
234    @Override
235    public boolean onCreateOptionsMenu(Menu menu) {
236        getMenuInflater().inflate(R.menu.message_list_option, menu);
237        return true;
238    }
239
240    @Override
241    public boolean onPrepareOptionsMenu(Menu menu) {
242        // TODO Disable "refresh" for combined mailboxes
243        return true;
244    }
245
246    @Override
247    public boolean onOptionsItemSelected(MenuItem item) {
248        switch (item.getItemId()) {
249            case R.id.refresh:
250                mListFragment.onRefresh(true);
251                return true;
252            case R.id.folders:
253                onFolders();
254                return true;
255            case R.id.accounts:
256                onAccounts();
257                return true;
258            case R.id.compose:
259                onCompose();
260                return true;
261            case R.id.account_settings:
262                onEditAccount();
263                return true;
264            default:
265                return super.onOptionsItemSelected(item);
266        }
267    }
268
269    private void onFolders() {
270        if (!mListFragment.isMagicMailbox()) { // Magic boxes don't have "folders" option.
271            // TODO smaller projection
272            Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mListFragment.getMailboxId());
273            if (mailbox != null) {
274                MailboxList.actionHandleAccount(this, mailbox.mAccountKey);
275                finish();
276            }
277        }
278    }
279
280    private void onAccounts() {
281        AccountFolderList.actionShowAccounts(this);
282        finish();
283    }
284
285    private void onCompose() {
286        MessageCompose.actionCompose(this, mListFragment.getAccountId());
287    }
288
289    private void onEditAccount() {
290        AccountSettingsXL.actionSettings(this, mListFragment.getAccountId());
291    }
292
293    /**
294     * Handle the eventual result from the security update activity
295     *
296     * Note, this is extremely coarse, and it simply returns the user to the Accounts list.
297     * Anything more requires refactoring of this Activity.
298     */
299    @Override
300    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
301        switch (requestCode) {
302            case REQUEST_SECURITY:
303                onAccounts();
304        }
305        super.onActivityResult(requestCode, resultCode, data);
306    }
307
308    private void showProgressIcon(boolean show) {
309        // TODO Show "refreshing" icon somewhere. (It's on the action bar on xlarge.)
310    }
311
312    private void showErrorBanner(String message) {
313        boolean isVisible = mErrorBanner.getVisibility() == View.VISIBLE;
314        if (message != null) {
315            mErrorBanner.setText(message);
316            if (!isVisible) {
317                mErrorBanner.setVisibility(View.VISIBLE);
318                mErrorBanner.startAnimation(
319                        AnimationUtils.loadAnimation(
320                                MessageList.this, R.anim.header_appear));
321            }
322        } else {
323            if (isVisible) {
324                mErrorBanner.setVisibility(View.GONE);
325                mErrorBanner.startAnimation(
326                        AnimationUtils.loadAnimation(
327                                MessageList.this, R.anim.header_disappear));
328            }
329        }
330    }
331
332    /**
333     * TODO This should probably be removed -- use RefreshManager instead to update the progress
334     * icon and the error banner.
335     *
336     * Controller results listener.  We wrap it with {@link ControllerResultUiThreadWrapper},
337     * so all methods are called on the UI thread.
338     */
339    private class ControllerResults extends Controller.Result {
340
341        // This is used to alter the connection banner operation for sending messages
342        private MessagingException mSendMessageException;
343
344        // TODO check accountKey and only react to relevant notifications
345        @Override
346        public void updateMailboxCallback(MessagingException result, long accountKey,
347                long mailboxKey, int progress, int numNewMessages) {
348            updateBanner(result, progress, mailboxKey);
349            updateProgress(result, progress);
350        }
351
352        /**
353         * We alter the updateBanner hysteresis here to capture any failures and handle
354         * them just once at the end.  This callback is overly overloaded:
355         *  result == null, messageId == -1, progress == 0:     start batch send
356         *  result == null, messageId == xx, progress == 0:     start sending one message
357         *  result == xxxx, messageId == xx, progress == 0;     failed sending one message
358         *  result == null, messageId == -1, progres == 100;    finish sending batch
359         */
360        @Override
361        public void sendMailCallback(MessagingException result, long accountId, long messageId,
362                int progress) {
363            if (mListFragment.isOutbox()) {
364                // reset captured error when we start sending one or more messages
365                if (messageId == -1 && result == null && progress == 0) {
366                    mSendMessageException = null;
367                }
368                // capture first exception that comes along
369                if (result != null && mSendMessageException == null) {
370                    mSendMessageException = result;
371                }
372                // if we're completing the sequence, change the banner state
373                if (messageId == -1 && progress == 100) {
374                    updateBanner(mSendMessageException, progress, mListFragment.getMailboxId());
375                }
376                // always update the spinner, which has less state to worry about
377                updateProgress(result, progress);
378            }
379        }
380
381        private void updateProgress(MessagingException result, int progress) {
382            showProgressIcon(result == null && progress < 100);
383        }
384
385        /**
386         * Show or hide the connection error banner, and convert the various MessagingException
387         * variants into localizable text.  There is hysteresis in the show/hide logic:  Once shown,
388         * the banner will remain visible until some progress is made on the connection.  The
389         * goal is to keep it from flickering during retries in a bad connection state.
390         *
391         * @param result
392         * @param progress
393         */
394        private void updateBanner(MessagingException result, int progress, long mailboxKey) {
395            if (mailboxKey != mListFragment.getMailboxId()) {
396                return;
397            }
398            if (result != null) {
399                showErrorBanner(
400                        MessagingExceptionStrings.getErrorString(MessageList.this, result));
401            } else if (progress > 0) {
402                showErrorBanner(null);
403            }
404        }
405    }
406
407    private class MailboxFinderCallback implements MailboxFinder.Callback {
408        @Override
409        public void onMailboxFound(long accountId, long mailboxId) {
410            mListFragment.openMailbox(mailboxId);
411        }
412
413        @Override
414        public void onAccountNotFound() {
415            // Let the Welcome activity show the default screen.
416            launchWelcomeAndFinish();
417        }
418
419        @Override
420        public void onMailboxNotFound(long accountId) {
421            // Let the Welcome activity show the default screen.
422            launchWelcomeAndFinish();
423        }
424
425        @Override
426        public void onAccountSecurityHold(long accountId) {
427            // launch the security setup activity
428            Intent i = AccountSecurity.actionUpdateSecurityIntent(
429                    MessageList.this, accountId, true);
430            MessageList.this.startActivityForResult(i, REQUEST_SECURITY);
431        }
432    }
433}
434