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