EmailActivity.java revision 347ae23b6932fe994b03909bf90854888c438517
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
66    /** Loader IDs starting with this is safe to use from UIControllers. */
67    static final int UI_CONTROLLER_LOADER_ID_BASE = 100;
68
69    /** Loader IDs starting with this is safe to use from ActionBarController. */
70    static final int ACTION_BAR_CONTROLLER_LOADER_ID_BASE = 200;
71
72    private static final int MAILBOX_SYNC_FREQUENCY_DIALOG = 1;
73    private static final int MAILBOX_SYNC_LOOKBACK_DIALOG = 2;
74
75    private Context mContext;
76    private Controller mController;
77    private Controller.Result mControllerResult;
78
79    private UIControllerBase mUIController;
80
81    private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
82
83    /** Banner to display errors */
84    private BannerController mErrorBanner;
85    /** Id of the account that had a messaging exception most recently. */
86    private long mLastErrorAccountId;
87
88    // STOPSHIP Temporary mailbox settings UI
89    private int mDialogSelection = -1;
90
91    /**
92     * Create an intent to launch and open account's inbox.
93     *
94     * @param accountId If -1, default account will be used.
95     */
96    public static Intent createOpenAccountIntent(Activity fromActivity, long accountId) {
97        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
98        if (accountId != -1) {
99            i.putExtra(EXTRA_ACCOUNT_ID, accountId);
100        }
101        return i;
102    }
103
104    /**
105     * Create an intent to launch and open a mailbox.
106     *
107     * @param accountId must not be -1.
108     * @param mailboxId must not be -1.  Magic mailboxes IDs (such as
109     * {@link Mailbox#QUERY_ALL_INBOXES}) don't work.
110     */
111    public static Intent createOpenMailboxIntent(Activity fromActivity, long accountId,
112            long mailboxId) {
113        if (accountId == -1 || mailboxId == -1) {
114            throw new IllegalArgumentException();
115        }
116        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
117        i.putExtra(EXTRA_ACCOUNT_ID, accountId);
118        i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
119        return i;
120    }
121
122    /**
123     * Create an intent to launch and open a message.
124     *
125     * @param accountId must not be -1.
126     * @param mailboxId must not be -1.  Magic mailboxes IDs (such as
127     * {@link Mailbox#QUERY_ALL_INBOXES}) don't work.
128     * @param messageId must not be -1.
129     */
130    public static Intent createOpenMessageIntent(Activity fromActivity, long accountId,
131            long mailboxId, long messageId) {
132        if (accountId == -1 || mailboxId == -1 || messageId == -1) {
133            throw new IllegalArgumentException();
134        }
135        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
136        i.putExtra(EXTRA_ACCOUNT_ID, accountId);
137        i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
138        i.putExtra(EXTRA_MESSAGE_ID, messageId);
139        return i;
140    }
141
142    /**
143     * Initialize {@link #mUIController}.
144     */
145    private void initUIController() {
146        mUIController = UiUtilities.useTwoPane(this)
147                ? new UIControllerTwoPane(this) : new UIControllerOnePane(this);
148    }
149
150    @Override
151    protected void onCreate(Bundle savedInstanceState) {
152        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onCreate");
153
154        // UIController is used in onPrepareOptionsMenu(), which can be called from within
155        // super.onCreate(), so we need to initialize it here.
156        initUIController();
157
158        super.onCreate(savedInstanceState);
159        ActivityHelper.debugSetWindowFlags(this);
160        setContentView(mUIController.getLayoutId());
161
162        mUIController.onActivityViewReady();
163
164        mContext = getApplicationContext();
165        mController = Controller.getInstance(this);
166        mControllerResult = new ControllerResultUiThreadWrapper<ControllerResult>(new Handler(),
167                new ControllerResult());
168        mController.addResultCallback(mControllerResult);
169
170        // Set up views
171        // TODO Probably better to extract mErrorMessageView related code into a separate class,
172        // so that it'll be easy to reuse for the phone activities.
173        TextView errorMessage = (TextView) findViewById(R.id.error_message);
174        errorMessage.setOnClickListener(this);
175        int errorBannerHeight = getResources().getDimensionPixelSize(R.dimen.error_message_height);
176        mErrorBanner = new BannerController(this, errorMessage, errorBannerHeight);
177
178        if (savedInstanceState != null) {
179            mUIController.restoreInstanceState(savedInstanceState);
180        } else {
181            // This needs to be done after installRestoredFragments.
182            // See UIControllerTwoPane.preFragmentTransactionCheck()
183            initFromIntent();
184        }
185        mUIController.onActivityCreated();
186    }
187
188    private void initFromIntent() {
189        final Intent i = getIntent();
190        final long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1);
191        final long mailboxId = i.getLongExtra(EXTRA_MAILBOX_ID, -1);
192        final long messageId = i.getLongExtra(EXTRA_MESSAGE_ID, -1);
193        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
194            Log.d(Logging.LOG_TAG, String.format("initFromIntent: %d %d", accountId, mailboxId));
195        }
196
197        if (accountId != -1) {
198            mUIController.open(accountId, mailboxId, messageId);
199        }
200    }
201
202    @Override
203    protected void onSaveInstanceState(Bundle outState) {
204        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
205            Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
206        }
207        super.onSaveInstanceState(outState);
208        mUIController.onSaveInstanceState(outState);
209    }
210
211    // FragmentInstallable
212    @Override
213    public void onInstallFragment(Fragment fragment) {
214        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
215            Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment);
216        }
217        mUIController.onInstallFragment(fragment);
218    }
219
220    // FragmentInstallable
221    @Override
222    public void onUninstallFragment(Fragment fragment) {
223        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
224            Log.d(Logging.LOG_TAG, this + " onUninstallFragment fragment=" + fragment);
225        }
226        mUIController.onUninstallFragment(fragment);
227    }
228
229    @Override
230    protected void onStart() {
231        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStart");
232        super.onStart();
233        mUIController.onActivityStart();
234
235        // STOPSHIP Temporary search UI
236        Intent intent = getIntent();
237        if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
238            // TODO Very temporary (e.g. no database access in UI thread)
239            Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA);
240            if (appData == null) return; // ??
241            final long accountId = appData.getLong(EXTRA_ACCOUNT_ID);
242            final long mailboxId = appData.getLong(EXTRA_MAILBOX_ID);
243            final String queryString = intent.getStringExtra(SearchManager.QUERY);
244            Log.d(Logging.LOG_TAG, queryString);
245            // Switch to search mailbox
246            // TODO How to handle search from within the search mailbox??
247            final Controller controller = Controller.getInstance(mContext);
248            final Mailbox searchMailbox = controller.getSearchMailbox(accountId);
249            if (searchMailbox == null) return;
250
251            // Delete contents, add a placeholder
252            ContentResolver resolver = mContext.getContentResolver();
253            resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailbox.mId,
254                    null);
255            ContentValues cv = new ContentValues();
256            cv.put(Mailbox.DISPLAY_NAME, queryString);
257            resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailbox.mId), cv,
258                    null, null);
259            Message msg = new Message();
260            msg.mMailboxKey = searchMailbox.mId;
261            msg.mAccountKey = accountId;
262            msg.mDisplayName = "Searching for " + queryString;
263            msg.mTimeStamp = Long.MAX_VALUE; // Sort on top
264            msg.save(mContext);
265
266            startActivity(createOpenMessageIntent(EmailActivity.this,
267                    accountId, searchMailbox.mId, msg.mId));
268            EmailAsyncTask.runAsyncParallel(new Runnable() {
269                @Override
270                public void run() {
271                    SearchParams searchSpec = new SearchParams(SearchParams.ALL_MAILBOXES,
272                            queryString);
273                    controller.searchMessages(accountId, searchSpec, searchMailbox.mId);
274                }});
275            return;
276        }
277    }
278
279    @Override
280    protected void onResume() {
281        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onResume");
282        super.onResume();
283        mUIController.onActivityResume();
284        /**
285         * In {@link MessageList#onResume()}, we go back to {@link Welcome} if an account
286         * has been added/removed. We don't need to do that here, because we fetch the most
287         * up-to-date account list. Additionally, we detect and do the right thing if all
288         * of the accounts have been removed.
289         */
290    }
291
292    @Override
293    protected void onPause() {
294        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onPause");
295        super.onPause();
296        mUIController.onActivityPause();
297    }
298
299    @Override
300    protected void onStop() {
301        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStop");
302        super.onStop();
303        mUIController.onActivityStop();
304    }
305
306    @Override
307    protected void onDestroy() {
308        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onDestroy");
309        mController.removeResultCallback(mControllerResult);
310        mTaskTracker.cancellAllInterrupt();
311        mUIController.onActivityDestroy();
312        super.onDestroy();
313    }
314
315    @Override
316    public void onBackPressed() {
317        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
318            Log.d(Logging.LOG_TAG, this + " onBackPressed");
319        }
320        if (!mUIController.onBackPressed(true)) {
321            // Not handled by UIController -- perform the default. i.e. close the app.
322            super.onBackPressed();
323        }
324    }
325
326    @Override
327    public void onClick(View v) {
328        switch (v.getId()) {
329            case R.id.error_message:
330                dismissErrorMessage();
331                break;
332        }
333    }
334
335    /**
336     * Force dismiss the error banner.
337     */
338    private void dismissErrorMessage() {
339        mErrorBanner.dismiss();
340    }
341
342    @Override
343    public boolean onCreateOptionsMenu(Menu menu) {
344        return mUIController.onCreateOptionsMenu(getMenuInflater(), menu);
345    }
346
347    @Override
348    public boolean onPrepareOptionsMenu(Menu menu) {
349        // STOPSHIP Temporary search/sync options UI
350        // Only show search/sync options for EAS 12.0 and later
351        boolean isEas = false;
352        boolean canSearch = false;
353        long accountId = mUIController.getActualAccountId();
354        if (accountId > 0) {
355            // Move database operations out of the UI thread
356            if ("eas".equals(Account.getProtocol(mContext, accountId))) {
357                isEas = true;
358                Account account = Account.restoreAccountWithId(mContext, accountId);
359                if (account != null) {
360                    // We should set a flag in the account indicating ability to handle search
361                    String protocolVersion = account.mProtocolVersion;
362                    if (Double.parseDouble(protocolVersion) >= 12.0) {
363                        canSearch = true;
364                    }
365                }
366            }
367        }
368        // Should use an isSearchable call to prevent search on inappropriate accounts/boxes
369        menu.findItem(R.id.search).setVisible(canSearch);
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