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