EmailActivity.java revision b0b6eb56f716f3ac0c153b1d4a1b7b2bdfba4335
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.MessageListContext;
41import com.android.email.MessagingExceptionStrings;
42import com.android.email.R;
43import com.android.emailcommon.Logging;
44import com.android.emailcommon.mail.MessagingException;
45import com.android.emailcommon.provider.Account;
46import com.android.emailcommon.provider.EmailContent.MailboxColumns;
47import com.android.emailcommon.provider.EmailContent.Message;
48import com.android.emailcommon.provider.Mailbox;
49import com.android.emailcommon.utility.EmailAsyncTask;
50import com.google.common.base.Preconditions;
51
52import java.util.ArrayList;
53
54/**
55 * The main Email activity, which is used on both the tablet and the phone.
56 *
57 * Because this activity is device agnostic, so most of the UI aren't owned by this, but by
58 * the UIController.
59 */
60public class EmailActivity extends Activity implements View.OnClickListener, FragmentInstallable {
61    public static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID";
62    public static final String EXTRA_MAILBOX_ID = "MAILBOX_ID";
63    public static final String EXTRA_MESSAGE_ID = "MESSAGE_ID";
64    public static final String EXTRA_QUERY_STRING = "QUERY_STRING";
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 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        Preconditions.checkArgument(Account.isNormalAccount(accountId),
152                "Can only search in normal accounts");
153
154        // Note that a search doesn't use a restart intent, as we want another instance of
155        // the activity to sit on the stack for search.
156        Intent i = new Intent(fromActivity, EmailActivity.class);
157        i.putExtra(EXTRA_ACCOUNT_ID, accountId);
158        i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
159        i.putExtra(EXTRA_QUERY_STRING, query);
160        i.setAction(Intent.ACTION_SEARCH);
161        return i;
162    }
163
164    /**
165     * Initialize {@link #mUIController}.
166     */
167    private void initUIController() {
168        mUIController = UiUtilities.useTwoPane(this)
169                ? new UIControllerTwoPane(this) : new UIControllerOnePane(this);
170    }
171
172    @Override
173    protected void onCreate(Bundle savedInstanceState) {
174        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onCreate");
175
176        // UIController is used in onPrepareOptionsMenu(), which can be called from within
177        // super.onCreate(), so we need to initialize it here.
178        initUIController();
179
180        super.onCreate(savedInstanceState);
181        ActivityHelper.debugSetWindowFlags(this);
182        setContentView(mUIController.getLayoutId());
183
184        mUIController.onActivityViewReady();
185
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.onRestoreInstanceState(savedInstanceState);
201        } else {
202            initFromIntent();
203        }
204        mUIController.onActivityCreated();
205    }
206
207    private void initFromIntent() {
208        final Intent intent = getIntent();
209        final MessageListContext viewContext = MessageListContext.forIntent(this, intent);
210        final long messageId = intent.getLongExtra(EXTRA_MESSAGE_ID, Message.NO_MESSAGE);
211
212        if (viewContext.isSearch()) {
213            EmailAsyncTask.runAsyncParallel(new Runnable() {
214                @Override
215                public void run() {
216                    try {
217                        Controller controller = Controller.getInstance(EmailActivity.this);
218                        controller.searchMessages(
219                                viewContext.mAccountId, viewContext.getSearchParams());
220                    } catch (MessagingException e) {
221                        // TODO: handle.
222                        Log.e(Logging.LOG_TAG, "Got exception while searching " + e);
223                    }
224                }});
225        }
226
227        mUIController.open(viewContext, messageId);
228    }
229
230    @Override
231    protected void onSaveInstanceState(Bundle outState) {
232        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
233            Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
234        }
235        super.onSaveInstanceState(outState);
236        mUIController.onSaveInstanceState(outState);
237    }
238
239    // FragmentInstallable
240    @Override
241    public void onInstallFragment(Fragment fragment) {
242        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
243            Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment);
244        }
245        mUIController.onInstallFragment(fragment);
246    }
247
248    // FragmentInstallable
249    @Override
250    public void onUninstallFragment(Fragment fragment) {
251        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
252            Log.d(Logging.LOG_TAG, this + " onUninstallFragment fragment=" + fragment);
253        }
254        mUIController.onUninstallFragment(fragment);
255    }
256
257    @Override
258    protected void onStart() {
259        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStart");
260        super.onStart();
261        mUIController.onActivityStart();
262    }
263
264    @Override
265    protected void onResume() {
266        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onResume");
267        super.onResume();
268        mUIController.onActivityResume();
269        /**
270         * In {@link MessageList#onResume()}, we go back to {@link Welcome} if an account
271         * has been added/removed. We don't need to do that here, because we fetch the most
272         * up-to-date account list. Additionally, we detect and do the right thing if all
273         * of the accounts have been removed.
274         */
275    }
276
277    @Override
278    protected void onPause() {
279        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onPause");
280        super.onPause();
281        mUIController.onActivityPause();
282    }
283
284    @Override
285    protected void onStop() {
286        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStop");
287        super.onStop();
288        mUIController.onActivityStop();
289    }
290
291    @Override
292    protected void onDestroy() {
293        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onDestroy");
294        mController.removeResultCallback(mControllerResult);
295        mTaskTracker.cancellAllInterrupt();
296        mUIController.onActivityDestroy();
297        super.onDestroy();
298    }
299
300    @Override
301    public void onBackPressed() {
302        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
303            Log.d(Logging.LOG_TAG, this + " onBackPressed");
304        }
305        if (!mUIController.onBackPressed(true)) {
306            // Not handled by UIController -- perform the default. i.e. close the app.
307            super.onBackPressed();
308        }
309    }
310
311    @Override
312    public void onClick(View v) {
313        switch (v.getId()) {
314            case R.id.error_message:
315                dismissErrorMessage();
316                break;
317        }
318    }
319
320    /**
321     * Force dismiss the error banner.
322     */
323    private void dismissErrorMessage() {
324        mErrorBanner.dismiss();
325    }
326
327    @Override
328    public boolean onCreateOptionsMenu(Menu menu) {
329        return mUIController.onCreateOptionsMenu(getMenuInflater(), menu);
330    }
331
332    @Override
333    public boolean onPrepareOptionsMenu(Menu menu) {
334        return mUIController.onPrepareOptionsMenu(getMenuInflater(), menu);
335    }
336
337    /**
338     * Called when the search key is pressd.
339     *
340     * Use the below command to emulate the key press on devices without the search key.
341     * adb shell input keyevent 84
342     */
343    @Override
344    public boolean onSearchRequested() {
345        if (Email.DEBUG) {
346            Log.d(Logging.LOG_TAG, this + " onSearchRequested");
347        }
348        mUIController.onSearchRequested();
349        return true; // Event handled.
350    }
351
352    // STOPSHIP Set column from user options
353    private void setMailboxColumn(long mailboxId, String column, String value) {
354        if (mailboxId > 0) {
355            ContentValues cv = new ContentValues();
356            cv.put(column, value);
357            getContentResolver().update(
358                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
359                    cv, null, null);
360            mUIController.onRefresh();
361        }
362    }
363    // STOPSHIP Temporary mailbox settings UI.  If this ends up being useful, it should
364    // be moved to Utility (emailcommon)
365    private int findInStringArray(String[] array, String item) {
366        int i = 0;
367        for (String str: array) {
368            if (str.equals(item)) {
369                return i;
370            }
371            i++;
372        }
373        return -1;
374    }
375
376    // STOPSHIP Temporary mailbox settings UI
377    private final DialogInterface.OnClickListener mSelectionListener =
378        new DialogInterface.OnClickListener() {
379            public void onClick(DialogInterface dialog, int which) {
380                mDialogSelection = which;
381            }
382    };
383
384    // STOPSHIP Temporary mailbox settings UI
385    private final DialogInterface.OnClickListener mCancelListener =
386        new DialogInterface.OnClickListener() {
387            public void onClick(DialogInterface dialog, int which) {
388            }
389    };
390
391    // STOPSHIP Temporary mailbox settings UI
392    @Override
393    @Deprecated
394    protected Dialog onCreateDialog(int id, Bundle args) {
395        final long mailboxId = mUIController.getMailboxSettingsMailboxId();
396        if (mailboxId < 0) {
397            return null;
398        }
399        final Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mailboxId);
400        if (mailbox == null) return null;
401        switch (id) {
402            case MAILBOX_SYNC_FREQUENCY_DIALOG:
403                String freq = Integer.toString(mailbox.mSyncInterval);
404                final String[] freqValues = getResources().getStringArray(
405                        R.array.account_settings_check_frequency_values_push);
406                int selection = findInStringArray(freqValues, freq);
407                // If not found, this is a push mailbox; trust me on this
408                if (selection == -1) selection = 0;
409                return new AlertDialog.Builder(this)
410                    .setIconAttribute(android.R.attr.dialogIcon)
411                    .setTitle(R.string.mailbox_options_check_frequency_label)
412                    .setSingleChoiceItems(R.array.account_settings_check_frequency_entries_push,
413                            selection,
414                            mSelectionListener)
415                    .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
416                        public void onClick(DialogInterface dialog, int which) {
417                            setMailboxColumn(mailboxId, MailboxColumns.SYNC_INTERVAL,
418                                    freqValues[mDialogSelection]);
419                        }})
420                    .setNegativeButton(R.string.cancel_action, mCancelListener)
421                   .create();
422
423            case MAILBOX_SYNC_LOOKBACK_DIALOG:
424                freq = Integer.toString(mailbox.mSyncLookback);
425                final String[] windowValues = getResources().getStringArray(
426                        R.array.account_settings_mail_window_values);
427                selection = findInStringArray(windowValues, freq);
428                return new AlertDialog.Builder(this)
429                    .setIconAttribute(android.R.attr.dialogIcon)
430                    .setTitle(R.string.mailbox_options_lookback_label)
431                    .setSingleChoiceItems(R.array.account_settings_mail_window_entries,
432                            selection,
433                            mSelectionListener)
434                    .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
435                        public void onClick(DialogInterface dialog, int which) {
436                            setMailboxColumn(mailboxId, MailboxColumns.SYNC_LOOKBACK,
437                                    windowValues[mDialogSelection]);
438                        }})
439                    .setNegativeButton(R.string.cancel_action, mCancelListener)
440                   .create();
441        }
442        return null;
443    }
444
445    @Override
446    @SuppressWarnings("deprecation")
447    public boolean onOptionsItemSelected(MenuItem item) {
448        if (mUIController.onOptionsItemSelected(item)) {
449            return true;
450        }
451        switch (item.getItemId()) {
452            // STOPSHIP Temporary mailbox settings UI
453            case R.id.sync_lookback:
454                showDialog(MAILBOX_SYNC_LOOKBACK_DIALOG);
455                return true;
456            // STOPSHIP Temporary mailbox settings UI
457            case R.id.sync_frequency:
458                showDialog(MAILBOX_SYNC_FREQUENCY_DIALOG);
459                return true;
460        }
461        return super.onOptionsItemSelected(item);
462    }
463
464
465    /**
466     * A {@link Controller.Result} to detect connection status.
467     */
468    private class ControllerResult extends Controller.Result {
469        @Override
470        public void sendMailCallback(
471                MessagingException result, long accountId, long messageId, int progress) {
472            handleError(result, accountId, progress);
473        }
474
475        @Override
476        public void serviceCheckMailCallback(
477                MessagingException result, long accountId, long mailboxId, int progress, long tag) {
478            handleError(result, accountId, progress);
479        }
480
481        @Override
482        public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId,
483                int progress, int numNewMessages, ArrayList<Long> addedMessages) {
484            handleError(result, accountId, progress);
485        }
486
487        @Override
488        public void updateMailboxListCallback(
489                MessagingException result, long accountId, int progress) {
490            handleError(result, accountId, progress);
491        }
492
493        @Override
494        public void loadAttachmentCallback(MessagingException result, long accountId,
495                long messageId, long attachmentId, int progress) {
496            handleError(result, accountId, progress);
497        }
498
499        @Override
500        public void loadMessageForViewCallback(MessagingException result, long accountId,
501                long messageId, int progress) {
502            handleError(result, accountId, progress);
503        }
504
505        private void handleError(final MessagingException result, final long accountId,
506                int progress) {
507            if (accountId == -1) {
508                return;
509            }
510            if (result == null) {
511                if (progress > 0) {
512                    // Connection now working; clear the error message banner
513                    if (mLastErrorAccountId == accountId) {
514                        dismissErrorMessage();
515                    }
516                }
517            } else {
518                // Connection error; show the error message banner
519                new EmailAsyncTask<Void, Void, String>(mTaskTracker) {
520                    @Override
521                    protected String doInBackground(Void... params) {
522                        Account account =
523                            Account.restoreAccountWithId(EmailActivity.this, accountId);
524                        return (account == null) ? null : account.mDisplayName;
525                    }
526
527                    @Override
528                    protected void onPostExecute(String accountName) {
529                        String message =
530                            MessagingExceptionStrings.getErrorString(EmailActivity.this, result);
531                        if (!TextUtils.isEmpty(accountName)) {
532                            // TODO Use properly designed layout. Don't just concatenate strings;
533                            // which is generally poor for I18N.
534                            message = message + "   (" + accountName + ")";
535                        }
536                        if (mErrorBanner.show(message)) {
537                            mLastErrorAccountId = accountId;
538                        }
539                    }
540                }.executeParallel();
541            }
542        }
543    }
544}
545