EmailActivity.java revision 3d9b8e76f0a396370a5c0be99a34bf7c24bd20dd
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.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    @Override
233    public void onInstallFragment(Fragment fragment) {
234        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
235            Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment);
236        }
237        mUIController.onInstallFragment(fragment);
238    }
239
240    @Override
241    protected void onStart() {
242        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStart");
243        super.onStart();
244        mUIController.onActivityStart();
245
246        // STOPSHIP Temporary search UI
247        Intent intent = getIntent();
248        if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
249            // TODO Very temporary (e.g. no database access in UI thread)
250            Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA);
251            if (appData == null) return; // ??
252            final long accountId = appData.getLong(EXTRA_ACCOUNT_ID);
253            final long mailboxId = appData.getLong(EXTRA_MAILBOX_ID);
254            final String queryString = intent.getStringExtra(SearchManager.QUERY);
255            Log.d(Logging.LOG_TAG, queryString);
256            // Switch to search mailbox
257            // TODO How to handle search from within the search mailbox??
258            final Controller controller = Controller.getInstance(mContext);
259            final Mailbox searchMailbox = controller.getSearchMailbox(accountId);
260            if (searchMailbox == null) return;
261
262            // Delete contents, add a placeholder
263            ContentResolver resolver = mContext.getContentResolver();
264            resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailbox.mId,
265                    null);
266            ContentValues cv = new ContentValues();
267            cv.put(Mailbox.DISPLAY_NAME, queryString);
268            resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailbox.mId), cv,
269                    null, null);
270            Message msg = new Message();
271            msg.mMailboxKey = searchMailbox.mId;
272            msg.mAccountKey = accountId;
273            msg.mDisplayName = "Searching for " + queryString;
274            msg.mTimeStamp = Long.MAX_VALUE; // Sort on top
275            msg.save(mContext);
276
277            startActivity(createOpenMessageIntent(EmailActivity.this,
278                    accountId, searchMailbox.mId, msg.mId));
279            Utility.runAsync(new Runnable() {
280                @Override
281                public void run() {
282                    controller.searchMessages(accountId, mailboxId, true, queryString, 10, 0,
283                            searchMailbox.mId);
284                }});
285            return;
286        }
287    }
288
289    @Override
290    protected void onResume() {
291        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onResume");
292        super.onResume();
293        mUIController.onActivityResume();
294        /**
295         * In {@link MessageList#onResume()}, we go back to {@link Welcome} if an account
296         * has been added/removed. We don't need to do that here, because we fetch the most
297         * up-to-date account list. Additionally, we detect and do the right thing if all
298         * of the accounts have been removed.
299         */
300    }
301
302    @Override
303    protected void onPause() {
304        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onPause");
305        super.onPause();
306        mUIController.onActivityPause();
307    }
308
309    @Override
310    protected void onStop() {
311        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStop");
312        super.onStop();
313        mUIController.onActivityStop();
314    }
315
316    @Override
317    protected void onDestroy() {
318        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onDestroy");
319        mController.removeResultCallback(mControllerResult);
320        mTaskTracker.cancellAllInterrupt();
321        mUIController.onActivityDestroy();
322        super.onDestroy();
323    }
324
325    @Override
326    public void onBackPressed() {
327        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
328            Log.d(Logging.LOG_TAG, this + " onBackPressed");
329        }
330        if (!mUIController.onBackPressed(true)) {
331            // Not handled by UIController -- perform the default. i.e. close the app.
332            super.onBackPressed();
333        }
334    }
335
336    @Override
337    public void onClick(View v) {
338        switch (v.getId()) {
339            case R.id.error_message:
340                dismissErrorMessage();
341                break;
342        }
343    }
344
345    /**
346     * Force dismiss the error banner.
347     */
348    private void dismissErrorMessage() {
349        mErrorBanner.dismiss();
350    }
351
352    @Override
353    public boolean onCreateOptionsMenu(Menu menu) {
354        return mUIController.onCreateOptionsMenu(getMenuInflater(), menu);
355    }
356
357    @Override
358    public boolean onPrepareOptionsMenu(Menu menu) {
359        // STOPSHIP Temporary search/sync options UI
360        // Only show search/sync options for EAS
361        boolean isEas = false;
362        long accountId = mUIController.getActualAccountId();
363        if (accountId > 0) {
364            if ("eas".equals(Account.getProtocol(mContext, accountId))) {
365                isEas = true;
366            }
367        }
368        // Should use an isSearchable call to prevent search on inappropriate accounts/boxes
369        menu.findItem(R.id.search).setVisible(isEas);
370        // Should use an isSyncable call to prevent drafts/outbox from allowing this
371        menu.findItem(R.id.sync_lookback).setVisible(isEas);
372        menu.findItem(R.id.sync_frequency).setVisible(isEas);
373
374        return mUIController.onPrepareOptionsMenu(getMenuInflater(), menu);
375    }
376
377    @Override
378    public boolean onSearchRequested() {
379        Bundle bundle = new Bundle();
380        bundle.putLong(EXTRA_ACCOUNT_ID, mUIController.getActualAccountId());
381        bundle.putLong(EXTRA_MAILBOX_ID, mUIController.getSearchMailboxId());
382        startSearch(null, false, bundle, false);
383        return true;
384    }
385
386    // STOPSHIP Set column from user options
387    private void setMailboxColumn(long mailboxId, String column, String value) {
388        if (mailboxId > 0) {
389            ContentValues cv = new ContentValues();
390            cv.put(column, value);
391            getContentResolver().update(
392                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
393                    cv, null, null);
394            mUIController.onRefresh();
395        }
396    }
397    // STOPSHIP Temporary mailbox settings UI.  If this ends up being useful, it should
398    // be moved to Utility (emailcommon)
399    private int findInStringArray(String[] array, String item) {
400        int i = 0;
401        for (String str: array) {
402            if (str.equals(item)) {
403                return i;
404            }
405            i++;
406        }
407        return -1;
408    }
409
410    // STOPSHIP Temporary mailbox settings UI
411    private final DialogInterface.OnClickListener mSelectionListener =
412        new DialogInterface.OnClickListener() {
413            public void onClick(DialogInterface dialog, int which) {
414                mDialogSelection = which;
415            }
416    };
417
418    // STOPSHIP Temporary mailbox settings UI
419    private final DialogInterface.OnClickListener mCancelListener =
420        new DialogInterface.OnClickListener() {
421            public void onClick(DialogInterface dialog, int which) {
422            }
423    };
424
425    // STOPSHIP Temporary mailbox settings UI
426    @Override
427    @Deprecated
428    protected Dialog onCreateDialog(int id, Bundle args) {
429        final long mailboxId = mUIController.getMailboxSettingsMailboxId();
430        if (mailboxId < 0) {
431            return null;
432        }
433        final Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mailboxId);
434        if (mailbox == null) return null;
435        switch (id) {
436            case MAILBOX_SYNC_FREQUENCY_DIALOG:
437                String freq = Integer.toString(mailbox.mSyncInterval);
438                final String[] freqValues = getResources().getStringArray(
439                        R.array.account_settings_check_frequency_values_push);
440                int selection = findInStringArray(freqValues, freq);
441                // If not found, this is a push mailbox; trust me on this
442                if (selection == -1) selection = 0;
443                return new AlertDialog.Builder(this)
444                    .setIconAttribute(android.R.attr.dialogIcon)
445                    .setTitle(R.string.mailbox_options_check_frequency_label)
446                    .setSingleChoiceItems(R.array.account_settings_check_frequency_entries_push,
447                            selection,
448                            mSelectionListener)
449                    .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
450                        public void onClick(DialogInterface dialog, int which) {
451                            setMailboxColumn(mailboxId, MailboxColumns.SYNC_INTERVAL,
452                                    freqValues[mDialogSelection]);
453                        }})
454                    .setNegativeButton(R.string.cancel_action, mCancelListener)
455                   .create();
456
457            case MAILBOX_SYNC_LOOKBACK_DIALOG:
458                freq = Integer.toString(mailbox.mSyncLookback);
459                final String[] windowValues = getResources().getStringArray(
460                        R.array.account_settings_mail_window_values);
461                selection = findInStringArray(windowValues, freq);
462                return new AlertDialog.Builder(this)
463                    .setIconAttribute(android.R.attr.dialogIcon)
464                    .setTitle(R.string.mailbox_options_lookback_label)
465                    .setSingleChoiceItems(R.array.account_settings_mail_window_entries,
466                            selection,
467                            mSelectionListener)
468                    .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
469                        public void onClick(DialogInterface dialog, int which) {
470                            setMailboxColumn(mailboxId, MailboxColumns.SYNC_LOOKBACK,
471                                    windowValues[mDialogSelection]);
472                        }})
473                    .setNegativeButton(R.string.cancel_action, mCancelListener)
474                   .create();
475        }
476        return null;
477    }
478
479    @Override
480    @SuppressWarnings("deprecation")
481    public boolean onOptionsItemSelected(MenuItem item) {
482        if (mUIController.onOptionsItemSelected(item)) {
483            return true;
484        }
485        switch (item.getItemId()) {
486            // STOPSHIP Temporary mailbox settings UI
487            case R.id.sync_lookback:
488                showDialog(MAILBOX_SYNC_LOOKBACK_DIALOG);
489                return true;
490            // STOPSHIP Temporary mailbox settings UI
491            case R.id.sync_frequency:
492                showDialog(MAILBOX_SYNC_FREQUENCY_DIALOG);
493                return true;
494            case R.id.search:
495                onSearchRequested();
496                return true;
497        }
498        return super.onOptionsItemSelected(item);
499    }
500
501
502    /**
503     * A {@link Controller.Result} to detect connection status.
504     */
505    private class ControllerResult extends Controller.Result {
506        @Override
507        public void sendMailCallback(
508                MessagingException result, long accountId, long messageId, int progress) {
509            handleError(result, accountId, progress);
510        }
511
512        @Override
513        public void serviceCheckMailCallback(
514                MessagingException result, long accountId, long mailboxId, int progress, long tag) {
515            handleError(result, accountId, progress);
516        }
517
518        @Override
519        public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId,
520                int progress, int numNewMessages, ArrayList<Long> addedMessages) {
521            handleError(result, accountId, progress);
522        }
523
524        @Override
525        public void updateMailboxListCallback(
526                MessagingException result, long accountId, int progress) {
527            handleError(result, accountId, progress);
528        }
529
530        @Override
531        public void loadAttachmentCallback(MessagingException result, long accountId,
532                long messageId, long attachmentId, int progress) {
533            handleError(result, accountId, progress);
534        }
535
536        @Override
537        public void loadMessageForViewCallback(MessagingException result, long accountId,
538                long messageId, int progress) {
539            handleError(result, accountId, progress);
540        }
541
542        private void handleError(final MessagingException result, final long accountId,
543                int progress) {
544            if (accountId == -1) {
545                return;
546            }
547            if (result == null) {
548                if (progress > 0) {
549                    // Connection now working; clear the error message banner
550                    if (mLastErrorAccountId == accountId) {
551                        dismissErrorMessage();
552                    }
553                }
554            } else {
555                // Connection error; show the error message banner
556                new EmailAsyncTask<Void, Void, String>(mTaskTracker) {
557                    @Override
558                    protected String doInBackground(Void... params) {
559                        Account account =
560                            Account.restoreAccountWithId(EmailActivity.this, accountId);
561                        return (account == null) ? null : account.mDisplayName;
562                    }
563
564                    @Override
565                    protected void onPostExecute(String accountName) {
566                        String message =
567                            MessagingExceptionStrings.getErrorString(EmailActivity.this, result);
568                        if (!TextUtils.isEmpty(accountName)) {
569                            // TODO Use properly designed layout. Don't just concatenate strings;
570                            // which is generally poor for I18N.
571                            message = message + "   (" + accountName + ")";
572                        }
573                        if (mErrorBanner.show(message)) {
574                            mLastErrorAccountId = accountId;
575                        }
576                    }
577                }.executeParallel();
578            }
579        }
580    }
581}
582