EmailActivity.java revision 53ea83ebf91f820692e8fa8e781f5cc982dd94db
1/*
2 * Copyright (C) 2010 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.emailcommon.Logging;
25import com.android.emailcommon.mail.MessagingException;
26import com.android.emailcommon.provider.EmailContent.Account;
27import com.android.emailcommon.provider.EmailContent.MailboxColumns;
28import com.android.emailcommon.provider.EmailContent.Message;
29import com.android.emailcommon.provider.Mailbox;
30import com.android.emailcommon.utility.EmailAsyncTask;
31import com.android.emailcommon.utility.Utility;
32
33import android.app.Activity;
34import android.app.AlertDialog;
35import android.app.Dialog;
36import android.app.Fragment;
37import android.app.SearchManager;
38import android.content.ContentResolver;
39import android.content.ContentUris;
40import android.content.ContentValues;
41import android.content.Context;
42import android.content.DialogInterface;
43import android.content.Intent;
44import android.os.Bundle;
45import android.os.Handler;
46import android.text.TextUtils;
47import android.util.Log;
48import android.view.Menu;
49import android.view.MenuItem;
50import android.view.View;
51import android.widget.TextView;
52
53import java.security.InvalidParameterException;
54import java.util.ArrayList;
55
56/**
57 * The main Email activity, which is used on both the tablet and the phone.
58 *
59 * Because this activity is device agnostic, so most of the UI aren't owned by this, but by
60 * the UIController.
61 */
62public class EmailActivity extends Activity implements View.OnClickListener {
63    private static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID";
64    private static final String EXTRA_MAILBOX_ID = "MAILBOX_ID";
65    private static final String EXTRA_MESSAGE_ID = "MESSAGE_ID";
66    private static final String EXTRA_FORCE_PANE_MODE = "FORCE_PANE_MODE";
67
68    /** Loader IDs starting with this is safe to use from UIControllers. */
69    static final int UI_CONTROLLER_LOADER_ID_BASE = 100;
70
71    /** Loader IDs starting with this is safe to use from ActionBarController. */
72    static final int ACTION_BAR_CONTROLLER_LOADER_ID_BASE = 200;
73
74    private static final int MAILBOX_SYNC_FREQUENCY_DIALOG = 1;
75    private static final int MAILBOX_SYNC_LOOKBACK_DIALOG = 2;
76
77    private Context mContext;
78    private Controller mController;
79    private Controller.Result mControllerResult;
80
81    private UIControllerBase mUIController;
82
83    private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
84
85    /** Banner to display errors */
86    private BannerController mErrorBanner;
87    /** Id of the account that had a messaging exception most recently. */
88    private long mLastErrorAccountId;
89
90    // STOPSHIP Temporary mailbox settings UI
91    private int mDialogSelection = -1;
92
93    /**
94     * Create an intent to launch and open account's inbox.
95     *
96     * @param accountId If -1, default account will be used.
97     */
98    public static Intent createOpenAccountIntent(Activity fromActivity, long accountId) {
99        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
100        if (accountId != -1) {
101            i.putExtra(EXTRA_ACCOUNT_ID, accountId);
102        }
103        return i;
104    }
105
106    /**
107     * Create an intent to launch and open a mailbox.
108     *
109     * @param accountId must not be -1.
110     * @param mailboxId must not be -1.  Magic mailboxes IDs (such as
111     * {@link Mailbox#QUERY_ALL_INBOXES}) don't work.
112     */
113    public static Intent createOpenMailboxIntent(Activity fromActivity, long accountId,
114            long mailboxId) {
115        if (accountId == -1 || mailboxId == -1) {
116            throw new InvalidParameterException();
117        }
118        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
119        i.putExtra(EXTRA_ACCOUNT_ID, accountId);
120        i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
121        return i;
122    }
123
124    /**
125     * Create an intent to launch and open a message.
126     *
127     * @param accountId must not be -1.
128     * @param mailboxId must not be -1.  Magic mailboxes IDs (such as
129     * {@link Mailbox#QUERY_ALL_INBOXES}) don't work.
130     * @param messageId must not be -1.
131     */
132    public static Intent createOpenMessageIntent(Activity fromActivity, long accountId,
133            long mailboxId, long messageId) {
134        if (accountId == -1 || mailboxId == -1 || messageId == -1) {
135            throw new InvalidParameterException();
136        }
137        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
138        i.putExtra(EXTRA_ACCOUNT_ID, accountId);
139        i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
140        i.putExtra(EXTRA_MESSAGE_ID, messageId);
141        return i;
142    }
143
144    /**
145     * Set a debug flag to an intent to force open in 1-pane or 2-pane.
146     *
147     * @param useTwoPane true to open in 2-pane.  false to open in 1-pane.
148     */
149    public static void forcePaneMode(Intent i, boolean useTwoPane) {
150        i.putExtra(EXTRA_FORCE_PANE_MODE, useTwoPane ? 2 : 1);
151    }
152
153    /**
154     * Initialize {@link #mUIController}.
155     */
156    private void initUIController() {
157        final boolean twoPane;
158        switch (getIntent().getIntExtra(EXTRA_FORCE_PANE_MODE, -1)) {
159            case 1:
160                twoPane = false;
161                break;
162            case 2:
163                twoPane = true;
164                break;
165            default:
166                twoPane = getResources().getBoolean(R.bool.use_two_pane);
167                break;
168        }
169        mUIController = twoPane ? new UIControllerTwoPane(this) : new UIControllerOnePane(this);
170    }
171
172    @Override
173    protected void onCreate(Bundle savedInstanceState) {
174        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onCreate");
175
176        // UIController is used in onPrepareOptionsMenu(), which can be called from within
177        // super.onCreate(), so we need to initialize it here.
178        initUIController();
179
180        super.onCreate(savedInstanceState);
181        ActivityHelper.debugSetWindowFlags(this);
182        setContentView(mUIController.getLayoutId());
183
184        mUIController.onActivityViewReady();
185
186        mContext = getApplicationContext();
187        mController = Controller.getInstance(this);
188        mControllerResult = new ControllerResultUiThreadWrapper<ControllerResult>(new Handler(),
189                new ControllerResult());
190        mController.addResultCallback(mControllerResult);
191
192        // Set up views
193        // TODO Probably better to extract mErrorMessageView related code into a separate class,
194        // so that it'll be easy to reuse for the phone activities.
195        TextView errorMessage = (TextView) findViewById(R.id.error_message);
196        errorMessage.setOnClickListener(this);
197        int errorBannerHeight = getResources().getDimensionPixelSize(R.dimen.error_message_height);
198        mErrorBanner = new BannerController(this, errorMessage, errorBannerHeight);
199
200        // Install restored fragments.
201        mUIController.installRestoredFragments();
202
203        if (savedInstanceState != null) {
204            mUIController.restoreInstanceState(savedInstanceState);
205        } else {
206            // This needs to be done after installRestoredFragments.
207            // See UIControllerTwoPane.preFragmentTransactionCheck()
208            initFromIntent();
209        }
210        mUIController.onActivityCreated();
211    }
212
213    private void initFromIntent() {
214        final Intent i = getIntent();
215        final long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1);
216        final long mailboxId = i.getLongExtra(EXTRA_MAILBOX_ID, -1);
217        final long messageId = i.getLongExtra(EXTRA_MESSAGE_ID, -1);
218        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
219            Log.d(Logging.LOG_TAG, String.format("initFromIntent: %d %d", accountId, mailboxId));
220        }
221
222        if (accountId != -1) {
223            mUIController.open(accountId, mailboxId, messageId);
224        }
225    }
226
227    @Override
228    protected void onSaveInstanceState(Bundle outState) {
229        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
230            Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
231        }
232        super.onSaveInstanceState(outState);
233        mUIController.onSaveInstanceState(outState);
234    }
235
236    @Override
237    public void onAttachFragment(Fragment fragment) {
238        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
239            Log.d(Logging.LOG_TAG, this + " onAttachFragment fragment=" + fragment);
240        }
241        super.onAttachFragment(fragment);
242        mUIController.onAttachFragment(fragment);
243    }
244
245    @Override
246    protected void onStart() {
247        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStart");
248        super.onStart();
249        mUIController.onActivityStart();
250
251        // STOPSHIP Temporary search UI
252        Intent intent = getIntent();
253        if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
254            // TODO Very temporary (e.g. no database access in UI thread)
255            Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA);
256            if (appData == null) return; // ??
257            final long accountId = appData.getLong(EXTRA_ACCOUNT_ID);
258            final long mailboxId = appData.getLong(EXTRA_MAILBOX_ID);
259            final String queryString = intent.getStringExtra(SearchManager.QUERY);
260            Log.d(Logging.LOG_TAG, queryString);
261            // Switch to search mailbox
262            // TODO How to handle search from within the search mailbox??
263            final Controller controller = Controller.getInstance(mContext);
264            final Mailbox searchMailbox = controller.getSearchMailbox(accountId);
265            if (searchMailbox == null) return;
266
267            // Delete contents, add a placeholder
268            ContentResolver resolver = mContext.getContentResolver();
269            resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailbox.mId,
270                    null);
271            ContentValues cv = new ContentValues();
272            cv.put(Mailbox.DISPLAY_NAME, queryString);
273            resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailbox.mId), cv,
274                    null, null);
275            Message msg = new Message();
276            msg.mMailboxKey = searchMailbox.mId;
277            msg.mAccountKey = accountId;
278            msg.mDisplayName = "Searching for " + queryString;
279            msg.mTimeStamp = Long.MAX_VALUE; // Sort on top
280            msg.save(mContext);
281
282            startActivity(createOpenMessageIntent(EmailActivity.this,
283                    accountId, searchMailbox.mId, msg.mId));
284            Utility.runAsync(new Runnable() {
285                @Override
286                public void run() {
287                    controller.searchMessages(accountId, mailboxId, true, queryString, 10, 0,
288                            searchMailbox.mId);
289                }});
290            return;
291        }
292    }
293
294    @Override
295    protected void onResume() {
296        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onResume");
297        super.onResume();
298        mUIController.onActivityResume();
299        /**
300         * In {@link MessageList#onResume()}, we go back to {@link Welcome} if an account
301         * has been added/removed. We don't need to do that here, because we fetch the most
302         * up-to-date account list. Additionally, we detect and do the right thing if all
303         * of the accounts have been removed.
304         */
305    }
306
307    @Override
308    protected void onPause() {
309        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onPause");
310        super.onPause();
311        mUIController.onActivityPause();
312    }
313
314    @Override
315    protected void onStop() {
316        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStop");
317        super.onStop();
318        mUIController.onActivityStop();
319    }
320
321    @Override
322    protected void onDestroy() {
323        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onDestroy");
324        mController.removeResultCallback(mControllerResult);
325        mTaskTracker.cancellAllInterrupt();
326        mUIController.onActivityDestroy();
327        super.onDestroy();
328    }
329
330    @Override
331    public void onBackPressed() {
332        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
333            Log.d(Logging.LOG_TAG, this + " onBackPressed");
334        }
335        if (!mUIController.onBackPressed(true)) {
336            // Not handled by UIController -- perform the default. i.e. close the app.
337            super.onBackPressed();
338        }
339    }
340
341    @Override
342    public void onClick(View v) {
343        switch (v.getId()) {
344            case R.id.error_message:
345                dismissErrorMessage();
346                break;
347        }
348    }
349
350    /**
351     * Force dismiss the error banner.
352     */
353    private void dismissErrorMessage() {
354        mErrorBanner.dismiss();
355    }
356
357    @Override
358    public boolean onCreateOptionsMenu(Menu menu) {
359        return mUIController.onCreateOptionsMenu(getMenuInflater(), menu);
360    }
361
362    @Override
363    public boolean onPrepareOptionsMenu(Menu menu) {
364        // STOPSHIP Temporary search/sync options UI
365        // Only show search/sync options for EAS
366        boolean isEas = false;
367        long accountId = mUIController.getActualAccountId();
368        if (accountId > 0) {
369            if ("eas".equals(Account.getProtocol(mContext, accountId))) {
370                isEas = true;
371            }
372        }
373        // Should use an isSearchable call to prevent search on inappropriate accounts/boxes
374        menu.findItem(R.id.search).setVisible(isEas);
375        // Should use an isSyncable call to prevent drafts/outbox from allowing this
376        menu.findItem(R.id.sync_lookback).setVisible(isEas);
377        menu.findItem(R.id.sync_frequency).setVisible(isEas);
378
379        return mUIController.onPrepareOptionsMenu(getMenuInflater(), menu);
380    }
381
382    @Override
383    public boolean onSearchRequested() {
384        Bundle bundle = new Bundle();
385        bundle.putLong(EXTRA_ACCOUNT_ID, mUIController.getActualAccountId());
386        bundle.putLong(EXTRA_MAILBOX_ID, mUIController.getSearchMailboxId());
387        startSearch(null, false, bundle, false);
388        return true;
389    }
390
391    // STOPSHIP Set column from user options
392    private void setMailboxColumn(long mailboxId, String column, String value) {
393        if (mailboxId > 0) {
394            ContentValues cv = new ContentValues();
395            cv.put(column, value);
396            getContentResolver().update(
397                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
398                    cv, null, null);
399            mUIController.onRefresh();
400        }
401    }
402    // STOPSHIP Temporary mailbox settings UI.  If this ends up being useful, it should
403    // be moved to Utility (emailcommon)
404    private int findInStringArray(String[] array, String item) {
405        int i = 0;
406        for (String str: array) {
407            if (str.equals(item)) {
408                return i;
409            }
410            i++;
411        }
412        return -1;
413    }
414
415    // STOPSHIP Temporary mailbox settings UI
416    private final DialogInterface.OnClickListener mSelectionListener =
417        new DialogInterface.OnClickListener() {
418            public void onClick(DialogInterface dialog, int which) {
419                mDialogSelection = which;
420            }
421    };
422
423    // STOPSHIP Temporary mailbox settings UI
424    private final DialogInterface.OnClickListener mCancelListener =
425        new DialogInterface.OnClickListener() {
426            public void onClick(DialogInterface dialog, int which) {
427            }
428    };
429
430    // STOPSHIP Temporary mailbox settings UI
431    @Override
432    @Deprecated
433    protected Dialog onCreateDialog(int id, Bundle args) {
434        final long mailboxId = mUIController.getMailboxSettingsMailboxId();
435        if (mailboxId < 0) {
436            return null;
437        }
438        final Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mailboxId);
439        if (mailbox == null) return null;
440        switch (id) {
441            case MAILBOX_SYNC_FREQUENCY_DIALOG:
442                String freq = Integer.toString(mailbox.mSyncInterval);
443                final String[] freqValues = getResources().getStringArray(
444                        R.array.account_settings_check_frequency_values_push);
445                int selection = findInStringArray(freqValues, freq);
446                // If not found, this is a push mailbox; trust me on this
447                if (selection == -1) selection = 0;
448                return new AlertDialog.Builder(this)
449                    .setIconAttribute(android.R.attr.dialogIcon)
450                    .setTitle(R.string.mailbox_options_check_frequency_label)
451                    .setSingleChoiceItems(R.array.account_settings_check_frequency_entries_push,
452                            selection,
453                            mSelectionListener)
454                    .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
455                        public void onClick(DialogInterface dialog, int which) {
456                            setMailboxColumn(mailboxId, MailboxColumns.SYNC_INTERVAL,
457                                    freqValues[mDialogSelection]);
458                        }})
459                    .setNegativeButton(R.string.cancel_action, mCancelListener)
460                   .create();
461
462            case MAILBOX_SYNC_LOOKBACK_DIALOG:
463                freq = Integer.toString(mailbox.mSyncLookback);
464                final String[] windowValues = getResources().getStringArray(
465                        R.array.account_settings_mail_window_values);
466                selection = findInStringArray(windowValues, freq);
467                return new AlertDialog.Builder(this)
468                    .setIconAttribute(android.R.attr.dialogIcon)
469                    .setTitle(R.string.mailbox_options_lookback_label)
470                    .setSingleChoiceItems(R.array.account_settings_mail_window_entries,
471                            selection,
472                            mSelectionListener)
473                    .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
474                        public void onClick(DialogInterface dialog, int which) {
475                            setMailboxColumn(mailboxId, MailboxColumns.SYNC_LOOKBACK,
476                                    windowValues[mDialogSelection]);
477                        }})
478                    .setNegativeButton(R.string.cancel_action, mCancelListener)
479                   .create();
480        }
481        return null;
482    }
483
484    @Override
485    @SuppressWarnings("deprecation")
486    public boolean onOptionsItemSelected(MenuItem item) {
487        if (mUIController.onOptionsItemSelected(item)) {
488            return true;
489        }
490        switch (item.getItemId()) {
491            // STOPSHIP Temporary mailbox settings UI
492            case R.id.sync_lookback:
493                showDialog(MAILBOX_SYNC_LOOKBACK_DIALOG);
494                return true;
495            // STOPSHIP Temporary mailbox settings UI
496            case R.id.sync_frequency:
497                showDialog(MAILBOX_SYNC_FREQUENCY_DIALOG);
498                return true;
499            case R.id.search:
500                onSearchRequested();
501                return true;
502        }
503        return super.onOptionsItemSelected(item);
504    }
505
506
507    /**
508     * A {@link Controller.Result} to detect connection status.
509     */
510    private class ControllerResult extends Controller.Result {
511        @Override
512        public void sendMailCallback(
513                MessagingException result, long accountId, long messageId, int progress) {
514            handleError(result, accountId, progress);
515        }
516
517        @Override
518        public void serviceCheckMailCallback(
519                MessagingException result, long accountId, long mailboxId, int progress, long tag) {
520            handleError(result, accountId, progress);
521        }
522
523        @Override
524        public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId,
525                int progress, int numNewMessages, ArrayList<Long> addedMessages) {
526            handleError(result, accountId, progress);
527        }
528
529        @Override
530        public void updateMailboxListCallback(
531                MessagingException result, long accountId, int progress) {
532            handleError(result, accountId, progress);
533        }
534
535        @Override
536        public void loadAttachmentCallback(MessagingException result, long accountId,
537                long messageId, long attachmentId, int progress) {
538            handleError(result, accountId, progress);
539        }
540
541        @Override
542        public void loadMessageForViewCallback(MessagingException result, long accountId,
543                long messageId, int progress) {
544            handleError(result, accountId, progress);
545        }
546
547        private void handleError(final MessagingException result, final long accountId,
548                int progress) {
549            if (accountId == -1) {
550                return;
551            }
552            if (result == null) {
553                if (progress > 0) {
554                    // Connection now working; clear the error message banner
555                    if (mLastErrorAccountId == accountId) {
556                        dismissErrorMessage();
557                    }
558                }
559            } else {
560                // Connection error; show the error message banner
561                new EmailAsyncTask<Void, Void, String>(mTaskTracker) {
562                    @Override
563                    protected String doInBackground(Void... params) {
564                        Account account =
565                            Account.restoreAccountWithId(EmailActivity.this, accountId);
566                        return (account == null) ? null : account.mDisplayName;
567                    }
568
569                    @Override
570                    protected void onPostExecute(String accountName) {
571                        String message =
572                            MessagingExceptionStrings.getErrorString(EmailActivity.this, result);
573                        if (!TextUtils.isEmpty(accountName)) {
574                            // TODO Use properly designed layout. Don't just concatenate strings;
575                            // which is generally poor for I18N.
576                            message = message + "   (" + accountName + ")";
577                        }
578                        if (mErrorBanner.show(message)) {
579                            mLastErrorAccountId = accountId;
580                        }
581                    }
582                }.executeParallel();
583            }
584        }
585    }
586}
587