EmailActivity.java revision e10215eaff7c06b44b95de87ea46030065ecbee5
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.DialogInterface;
26import android.content.Intent;
27import android.os.Bundle;
28import android.os.Handler;
29import android.text.TextUtils;
30import android.util.Log;
31import android.view.Menu;
32import android.view.MenuItem;
33import android.view.View;
34import android.widget.TextView;
35
36import com.android.email.Controller;
37import com.android.email.ControllerResultUiThreadWrapper;
38import com.android.email.Email;
39import com.android.email.MessageListContext;
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.utility.EmailAsyncTask;
49import com.google.common.base.Preconditions;
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 Controller mController;
75    private Controller.Result mControllerResult;
76
77    private UIControllerBase mUIController;
78
79    private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
80
81    /** Banner to display errors */
82    private BannerController mErrorBanner;
83    /** Id of the account that had a messaging exception most recently. */
84    private long mLastErrorAccountId;
85
86    // STOPSHIP Temporary mailbox settings UI
87    private int mDialogSelection = -1;
88
89    /**
90     * Create an intent to launch and open account's inbox.
91     *
92     * @param accountId If -1, default account will be used.
93     */
94    public static Intent createOpenAccountIntent(Activity fromActivity, long accountId) {
95        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
96        if (accountId != -1) {
97            i.putExtra(EXTRA_ACCOUNT_ID, accountId);
98        }
99        return i;
100    }
101
102    /**
103     * Create an intent to launch and open a mailbox.
104     *
105     * @param accountId must not be -1.
106     * @param mailboxId must not be -1.  Magic mailboxes IDs (such as
107     * {@link Mailbox#QUERY_ALL_INBOXES}) don't work.
108     */
109    public static Intent createOpenMailboxIntent(Activity fromActivity, long accountId,
110            long mailboxId) {
111        if (accountId == -1 || mailboxId == -1) {
112            throw new IllegalArgumentException();
113        }
114        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
115        i.putExtra(EXTRA_ACCOUNT_ID, accountId);
116        i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
117        return i;
118    }
119
120    /**
121     * Create an intent to launch and open a message.
122     *
123     * @param accountId must not be -1.
124     * @param mailboxId must not be -1.  Magic mailboxes IDs (such as
125     * {@link Mailbox#QUERY_ALL_INBOXES}) don't work.
126     * @param messageId must not be -1.
127     */
128    public static Intent createOpenMessageIntent(Activity fromActivity, long accountId,
129            long mailboxId, long messageId) {
130        if (accountId == -1 || mailboxId == -1 || messageId == -1) {
131            throw new IllegalArgumentException();
132        }
133        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
134        i.putExtra(EXTRA_ACCOUNT_ID, accountId);
135        i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
136        i.putExtra(EXTRA_MESSAGE_ID, messageId);
137        return i;
138    }
139
140    /**
141     * Create an intent to launch search activity.
142     *
143     * @param accountId ID of the account for the mailbox.  Must not be {@link Account#NO_ACCOUNT}.
144     * @param mailboxId ID of the mailbox to search, or {@link Mailbox#NO_MAILBOX} to perform
145     *     global search.
146     * @param query query string.
147     */
148    public static Intent createSearchIntent(Activity fromActivity, long accountId,
149            long mailboxId, String query) {
150        Preconditions.checkArgument(Account.isNormalAccount(accountId),
151                "Can only search in normal accounts");
152
153        // Note that a search doesn't use a restart intent, as we want another instance of
154        // the activity to sit on the stack for search.
155        Intent i = new Intent(fromActivity, EmailActivity.class);
156        i.putExtra(EXTRA_ACCOUNT_ID, accountId);
157        i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
158        i.putExtra(EXTRA_QUERY_STRING, query);
159        i.setAction(Intent.ACTION_SEARCH);
160        return i;
161    }
162
163    /**
164     * Initialize {@link #mUIController}.
165     */
166    private void initUIController() {
167        mUIController = UiUtilities.useTwoPane(this)
168                ? 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        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 MessageListContext viewContext = MessageListContext.forIntent(this, intent);
209        final long messageId = intent.getLongExtra(EXTRA_MESSAGE_ID, Message.NO_MESSAGE);
210
211        if (viewContext.isSearch()) {
212            EmailAsyncTask.runAsyncParallel(new Runnable() {
213                @Override
214                public void run() {
215                    try {
216                        Controller controller = Controller.getInstance(EmailActivity.this);
217                        controller.searchMessages(
218                                viewContext.mAccountId, viewContext.getSearchParams());
219                    } catch (MessagingException e) {
220                        // TODO: handle.
221                        Log.e(Logging.LOG_TAG, "Got exception while searching " + e);
222                    }
223                }});
224        }
225
226        mUIController.open(viewContext, messageId);
227    }
228
229    @Override
230    protected void onSaveInstanceState(Bundle outState) {
231        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
232            Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
233        }
234        super.onSaveInstanceState(outState);
235        mUIController.onSaveInstanceState(outState);
236    }
237
238    // FragmentInstallable
239    @Override
240    public void onInstallFragment(Fragment fragment) {
241        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
242            Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment);
243        }
244        mUIController.onInstallFragment(fragment);
245    }
246
247    // FragmentInstallable
248    @Override
249    public void onUninstallFragment(Fragment fragment) {
250        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
251            Log.d(Logging.LOG_TAG, this + " onUninstallFragment fragment=" + fragment);
252        }
253        mUIController.onUninstallFragment(fragment);
254    }
255
256    @Override
257    protected void onStart() {
258        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStart");
259        super.onStart();
260        mUIController.onActivityStart();
261    }
262
263    @Override
264    protected void onResume() {
265        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onResume");
266        super.onResume();
267        mUIController.onActivityResume();
268        /**
269         * In {@link MessageList#onResume()}, we go back to {@link Welcome} if an account
270         * has been added/removed. We don't need to do that here, because we fetch the most
271         * up-to-date account list. Additionally, we detect and do the right thing if all
272         * of the accounts have been removed.
273         */
274    }
275
276    @Override
277    protected void onPause() {
278        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onPause");
279        super.onPause();
280        mUIController.onActivityPause();
281    }
282
283    @Override
284    protected void onStop() {
285        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStop");
286        super.onStop();
287        mUIController.onActivityStop();
288    }
289
290    @Override
291    protected void onDestroy() {
292        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onDestroy");
293        mController.removeResultCallback(mControllerResult);
294        mTaskTracker.cancellAllInterrupt();
295        mUIController.onActivityDestroy();
296        super.onDestroy();
297    }
298
299    @Override
300    public void onBackPressed() {
301        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
302            Log.d(Logging.LOG_TAG, this + " onBackPressed");
303        }
304        if (!mUIController.onBackPressed(true)) {
305            // Not handled by UIController -- perform the default. i.e. close the app.
306            super.onBackPressed();
307        }
308    }
309
310    @Override
311    public void onClick(View v) {
312        switch (v.getId()) {
313            case R.id.error_message:
314                dismissErrorMessage();
315                break;
316        }
317    }
318
319    /**
320     * Force dismiss the error banner.
321     */
322    private void dismissErrorMessage() {
323        mErrorBanner.dismiss();
324    }
325
326    @Override
327    public boolean onCreateOptionsMenu(Menu menu) {
328        return mUIController.onCreateOptionsMenu(getMenuInflater(), menu);
329    }
330
331    @Override
332    public boolean onPrepareOptionsMenu(Menu menu) {
333        return mUIController.onPrepareOptionsMenu(getMenuInflater(), menu);
334    }
335
336    /**
337     * Called when the search key is pressd.
338     *
339     * Use the below command to emulate the key press on devices without the search key.
340     * adb shell input keyevent 84
341     */
342    @Override
343    public boolean onSearchRequested() {
344        if (Email.DEBUG) {
345            Log.d(Logging.LOG_TAG, this + " onSearchRequested");
346        }
347        mUIController.onSearchRequested();
348        return true; // Event handled.
349    }
350
351    // STOPSHIP Set column from user options
352    private void setMailboxColumn(long mailboxId, String column, String value) {
353        if (mailboxId > 0) {
354            ContentValues cv = new ContentValues();
355            cv.put(column, value);
356            getContentResolver().update(
357                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
358                    cv, null, null);
359            mUIController.onRefresh();
360        }
361    }
362    // STOPSHIP Temporary mailbox settings UI.  If this ends up being useful, it should
363    // be moved to Utility (emailcommon)
364    private int findInStringArray(String[] array, String item) {
365        int i = 0;
366        for (String str: array) {
367            if (str.equals(item)) {
368                return i;
369            }
370            i++;
371        }
372        return -1;
373    }
374
375    // STOPSHIP Temporary mailbox settings UI
376    private final DialogInterface.OnClickListener mSelectionListener =
377        new DialogInterface.OnClickListener() {
378            public void onClick(DialogInterface dialog, int which) {
379                mDialogSelection = which;
380            }
381    };
382
383    // STOPSHIP Temporary mailbox settings UI
384    private final DialogInterface.OnClickListener mCancelListener =
385        new DialogInterface.OnClickListener() {
386            public void onClick(DialogInterface dialog, int which) {
387            }
388    };
389
390    // STOPSHIP Temporary mailbox settings UI
391    @Override
392    @Deprecated
393    protected Dialog onCreateDialog(int id, Bundle args) {
394        final long mailboxId = mUIController.getMailboxSettingsMailboxId();
395        if (mailboxId < 0) {
396            return null;
397        }
398        final Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mailboxId);
399        if (mailbox == null) return null;
400        switch (id) {
401            case MAILBOX_SYNC_FREQUENCY_DIALOG:
402                String freq = Integer.toString(mailbox.mSyncInterval);
403                final String[] freqValues = getResources().getStringArray(
404                        R.array.account_settings_check_frequency_values_push);
405                int selection = findInStringArray(freqValues, freq);
406                // If not found, this is a push mailbox; trust me on this
407                if (selection == -1) selection = 0;
408                return new AlertDialog.Builder(this)
409                    .setIconAttribute(android.R.attr.dialogIcon)
410                    .setTitle(R.string.mailbox_options_check_frequency_label)
411                    .setSingleChoiceItems(R.array.account_settings_check_frequency_entries_push,
412                            selection,
413                            mSelectionListener)
414                    .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
415                        public void onClick(DialogInterface dialog, int which) {
416                            setMailboxColumn(mailboxId, MailboxColumns.SYNC_INTERVAL,
417                                    freqValues[mDialogSelection]);
418                        }})
419                    .setNegativeButton(R.string.cancel_action, mCancelListener)
420                   .create();
421
422            case MAILBOX_SYNC_LOOKBACK_DIALOG:
423                freq = Integer.toString(mailbox.mSyncLookback);
424                final String[] windowValues = getResources().getStringArray(
425                        R.array.account_settings_mail_window_values);
426                selection = findInStringArray(windowValues, freq);
427                return new AlertDialog.Builder(this)
428                    .setIconAttribute(android.R.attr.dialogIcon)
429                    .setTitle(R.string.mailbox_options_lookback_label)
430                    .setSingleChoiceItems(R.array.account_settings_mail_window_entries,
431                            selection,
432                            mSelectionListener)
433                    .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
434                        public void onClick(DialogInterface dialog, int which) {
435                            setMailboxColumn(mailboxId, MailboxColumns.SYNC_LOOKBACK,
436                                    windowValues[mDialogSelection]);
437                        }})
438                    .setNegativeButton(R.string.cancel_action, mCancelListener)
439                   .create();
440        }
441        return null;
442    }
443
444    @Override
445    @SuppressWarnings("deprecation")
446    public boolean onOptionsItemSelected(MenuItem item) {
447        if (mUIController.onOptionsItemSelected(item)) {
448            return true;
449        }
450        switch (item.getItemId()) {
451            // STOPSHIP Temporary mailbox settings UI
452            case R.id.sync_lookback:
453                showDialog(MAILBOX_SYNC_LOOKBACK_DIALOG);
454                return true;
455            // STOPSHIP Temporary mailbox settings UI
456            case R.id.sync_frequency:
457                showDialog(MAILBOX_SYNC_FREQUENCY_DIALOG);
458                return true;
459        }
460        return super.onOptionsItemSelected(item);
461    }
462
463
464    /**
465     * A {@link Controller.Result} to detect connection status.
466     */
467    private class ControllerResult extends Controller.Result {
468        @Override
469        public void sendMailCallback(
470                MessagingException result, long accountId, long messageId, int progress) {
471            handleError(result, accountId, progress);
472        }
473
474        @Override
475        public void serviceCheckMailCallback(
476                MessagingException result, long accountId, long mailboxId, int progress, long tag) {
477            handleError(result, accountId, progress);
478        }
479
480        @Override
481        public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId,
482                int progress, int numNewMessages, ArrayList<Long> addedMessages) {
483            handleError(result, accountId, progress);
484        }
485
486        @Override
487        public void updateMailboxListCallback(
488                MessagingException result, long accountId, int progress) {
489            handleError(result, accountId, progress);
490        }
491
492        @Override
493        public void loadAttachmentCallback(MessagingException result, long accountId,
494                long messageId, long attachmentId, int progress) {
495            handleError(result, accountId, progress);
496        }
497
498        @Override
499        public void loadMessageForViewCallback(MessagingException result, long accountId,
500                long messageId, int progress) {
501            handleError(result, accountId, progress);
502        }
503
504        private void handleError(final MessagingException result, final long accountId,
505                int progress) {
506            if (accountId == -1) {
507                return;
508            }
509            if (result == null) {
510                if (progress > 0) {
511                    // Connection now working; clear the error message banner
512                    if (mLastErrorAccountId == accountId) {
513                        dismissErrorMessage();
514                    }
515                }
516            } else {
517                // Connection error; show the error message banner
518                new EmailAsyncTask<Void, Void, String>(mTaskTracker) {
519                    @Override
520                    protected String doInBackground(Void... params) {
521                        Account account =
522                            Account.restoreAccountWithId(EmailActivity.this, accountId);
523                        return (account == null) ? null : account.mDisplayName;
524                    }
525
526                    @Override
527                    protected void onPostExecute(String accountName) {
528                        String message =
529                            MessagingExceptionStrings.getErrorString(EmailActivity.this, result);
530                        if (!TextUtils.isEmpty(accountName)) {
531                            // TODO Use properly designed layout. Don't just concatenate strings;
532                            // which is generally poor for I18N.
533                            message = message + "   (" + accountName + ")";
534                        }
535                        if (mErrorBanner.show(message)) {
536                            mLastErrorAccountId = accountId;
537                        }
538                    }
539                }.executeParallel();
540            }
541        }
542    }
543}
544