EmailActivity.java revision 075feb45562429cb8c7e19b43dc91e9778afeb24
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.Mailbox;
28import com.android.emailcommon.provider.EmailContent.MailboxColumns;
29import com.android.emailcommon.provider.EmailContent.Message;
30import com.android.emailcommon.utility.EmailAsyncTask;
31import com.android.emailcommon.utility.Utility;
32
33import android.app.Activity;
34import android.app.AlertDialog;
35import android.app.Dialog;
36import android.app.Fragment;
37import android.app.SearchManager;
38import android.content.ContentResolver;
39import android.content.ContentUris;
40import android.content.ContentValues;
41import android.content.Context;
42import android.content.DialogInterface;
43import android.content.Intent;
44import android.os.Bundle;
45import android.os.Handler;
46import android.text.TextUtils;
47import android.util.Log;
48import android.view.Menu;
49import android.view.MenuItem;
50import android.view.View;
51import android.widget.TextView;
52
53import java.security.InvalidParameterException;
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 {
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    private static final int MAILBOX_SYNC_FREQUENCY_DIALOG = 1;
70    private static final int MAILBOX_SYNC_LOOKBACK_DIALOG = 2;
71
72    private Context mContext;
73    private Controller mController;
74    private Controller.Result mControllerResult;
75
76    private final UIControllerTwoPane mUIController = new UIControllerTwoPane(this);
77
78    private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
79
80    /** Banner to display errors */
81    private BannerController mErrorBanner;
82    /** Id of the account that had a messaging exception most recently. */
83    private long mLastErrorAccountId;
84
85    // STOPSHIP Temporary mailbox settings UI
86    private int mDialogSelection = -1;
87
88    /**
89     * Launch and open account's inbox.
90     *
91     * @param accountId If -1, default account will be used.
92     */
93    public static void actionOpenAccount(Activity fromActivity, long accountId) {
94        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
95        if (accountId != -1) {
96            i.putExtra(EXTRA_ACCOUNT_ID, accountId);
97        }
98        fromActivity.startActivity(i);
99    }
100
101    /**
102     * Launch and open a mailbox.
103     *
104     * @param accountId must not be -1.
105     * @param mailboxId must not be -1.  Magic mailboxes IDs (such as
106     * {@link Mailbox#QUERY_ALL_INBOXES}) don't work.
107     */
108    public static void actionOpenMailbox(Activity fromActivity, long accountId, long mailboxId) {
109        if (accountId == -1 || mailboxId == -1) {
110            throw new InvalidParameterException();
111        }
112        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
113        i.putExtra(EXTRA_ACCOUNT_ID, accountId);
114        i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
115        fromActivity.startActivity(i);
116    }
117
118    /**
119     * Launch and open a message.
120     *
121     * @param accountId must not be -1.
122     * @param mailboxId must not be -1.  Magic mailboxes IDs (such as
123     * {@link Mailbox#QUERY_ALL_INBOXES}) don't work.
124     * @param messageId must not be -1.
125     */
126    public static void actionOpenMessage(Activity fromActivity, long accountId, long mailboxId,
127            long messageId) {
128        if (accountId == -1 || mailboxId == -1 || messageId == -1) {
129            throw new InvalidParameterException();
130        }
131        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
132        i.putExtra(EXTRA_ACCOUNT_ID, accountId);
133        i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
134        i.putExtra(EXTRA_MESSAGE_ID, messageId);
135        fromActivity.startActivity(i);
136    }
137
138    @Override
139    protected void onCreate(Bundle savedInstanceState) {
140        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "" + this + " onCreate");
141        super.onCreate(savedInstanceState);
142        ActivityHelper.debugSetWindowFlags(this);
143        setContentView(R.layout.message_list_xl);
144
145        mUIController.onActivityViewReady();
146
147        mContext = getApplicationContext();
148        mController = Controller.getInstance(this);
149        mControllerResult = new ControllerResultUiThreadWrapper<ControllerResult>(new Handler(),
150                new ControllerResult());
151        mController.addResultCallback(mControllerResult);
152
153        // Set up views
154        // TODO Probably better to extract mErrorMessageView related code into a separate class,
155        // so that it'll be easy to reuse for the phone activities.
156        TextView errorMessage = (TextView) findViewById(R.id.error_message);
157        errorMessage.setOnClickListener(this);
158        int errorBannerHeight = getResources().getDimensionPixelSize(R.dimen.error_message_height);
159        mErrorBanner = new BannerController(this, errorMessage, errorBannerHeight);
160
161        // Install restored fragments.
162        mUIController.installRestoredFragments();
163
164        if (savedInstanceState != null) {
165            mUIController.restoreInstanceState(savedInstanceState);
166        } else {
167            // This needs to be done after installRestoredFragments.
168            // See UIControllerTwoPane.preFragmentTransactionCheck()
169            initFromIntent();
170        }
171        mUIController.onActivityCreated();
172    }
173
174    private void initFromIntent() {
175        final Intent i = getIntent();
176        final long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1);
177        final long mailboxId = i.getLongExtra(EXTRA_MAILBOX_ID, -1);
178        final long messageId = i.getLongExtra(EXTRA_MESSAGE_ID, -1);
179        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
180            Log.d(Logging.LOG_TAG, String.format("initFromIntent: %d %d", accountId, mailboxId));
181        }
182
183        if (accountId != -1) {
184            mUIController.open(accountId, mailboxId, messageId);
185        }
186    }
187
188    @Override
189    protected void onSaveInstanceState(Bundle outState) {
190        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
191            Log.d(Logging.LOG_TAG, "" + this + " onSaveInstanceState");
192        }
193        super.onSaveInstanceState(outState);
194        mUIController.onSaveInstanceState(outState);
195    }
196
197    @Override
198    public void onAttachFragment(Fragment fragment) {
199        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
200            Log.d(Logging.LOG_TAG, "" + this + " onAttachFragment fragment=" + fragment);
201        }
202        super.onAttachFragment(fragment);
203        mUIController.onAttachFragment(fragment);
204    }
205
206    @Override
207    protected void onStart() {
208        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "" + this + " onStart");
209        super.onStart();
210        mUIController.onStart();
211
212        // STOPSHIP Temporary search UI
213        Intent intent = getIntent();
214        if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
215            // TODO Very temporary (e.g. no database access in UI thread)
216            Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA);
217            if (appData == null) return; // ??
218            final long accountId = appData.getLong(EXTRA_ACCOUNT_ID);
219            final long mailboxId = appData.getLong(EXTRA_MAILBOX_ID);
220            final String queryString = intent.getStringExtra(SearchManager.QUERY);
221            Log.d(Logging.LOG_TAG, queryString);
222            // Switch to search mailbox
223            // TODO How to handle search from within the search mailbox??
224            final Controller controller = Controller.getInstance(mContext);
225            final Mailbox searchMailbox = controller.getSearchMailbox(accountId);
226            if (searchMailbox == null) return;
227
228            // Delete contents, add a placeholder
229            ContentResolver resolver = mContext.getContentResolver();
230            resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailbox.mId,
231                    null);
232            ContentValues cv = new ContentValues();
233            cv.put(Mailbox.DISPLAY_NAME, queryString);
234            resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailbox.mId), cv,
235                    null, null);
236            Message msg = new Message();
237            msg.mMailboxKey = searchMailbox.mId;
238            msg.mAccountKey = accountId;
239            msg.mDisplayName = "Searching for " + queryString;
240            msg.mTimeStamp = Long.MAX_VALUE; // Sort on top
241            msg.save(mContext);
242
243            actionOpenMessage(EmailActivity.this, accountId, searchMailbox.mId, msg.mId);
244            Utility.runAsync(new Runnable() {
245                @Override
246                public void run() {
247                    controller.searchMessages(accountId, mailboxId, true, queryString, 10, 0,
248                            searchMailbox.mId);
249                }});
250            return;
251        }
252    }
253
254    @Override
255    protected void onResume() {
256        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "" + this + " onResume");
257        super.onResume();
258        mUIController.onResume();
259        /**
260         * In {@link MessageList#onResume()}, we go back to {@link Welcome} if an account
261         * has been added/removed. We don't need to do that here, because we fetch the most
262         * up-to-date account list. Additionally, we detect and do the right thing if all
263         * of the accounts have been removed.
264         */
265    }
266
267    @Override
268    protected void onPause() {
269        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "" + this + " onPause");
270        super.onPause();
271        mUIController.onPause();
272    }
273
274    @Override
275    protected void onStop() {
276        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "" + this + " onStop");
277        super.onStop();
278        mUIController.onStop();
279    }
280
281    @Override
282    protected void onDestroy() {
283        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "" + this + " onDestroy");
284        mController.removeResultCallback(mControllerResult);
285        mTaskTracker.cancellAllInterrupt();
286        mUIController.onDestroy();
287        super.onDestroy();
288    }
289
290    @Override
291    public void onBackPressed() {
292        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
293            Log.d(Logging.LOG_TAG, "" + this + " onBackPressed");
294        }
295        if (!mUIController.onBackPressed(true)) {
296            // Not handled by UIController -- perform the default. i.e. close the app.
297            super.onBackPressed();
298        }
299    }
300
301    @Override
302    public void onClick(View v) {
303        switch (v.getId()) {
304            case R.id.error_message:
305                dismissErrorMessage();
306                break;
307        }
308    }
309
310    /**
311     * Force dismiss the error banner.
312     */
313    private void dismissErrorMessage() {
314        mErrorBanner.dismiss();
315    }
316
317    @Override
318    public boolean onCreateOptionsMenu(Menu menu) {
319        return mUIController.onCreateOptionsMenu(getMenuInflater(), menu);
320    }
321
322    @Override
323    public boolean onPrepareOptionsMenu(Menu menu) {
324        // STOPSHIP Temporary search/sync options UI
325        // Only show search/sync options for EAS
326        boolean isEas = false;
327        long accountId = mUIController.getActualAccountId();
328        if (accountId > 0) {
329            if ("eas".equals(Account.getProtocol(mContext, accountId))) {
330                isEas = true;
331            }
332        }
333        // Should use an isSearchable call to prevent search on inappropriate accounts/boxes
334        menu.findItem(R.id.search).setVisible(isEas);
335        // Should use an isSyncable call to prevent drafts/outbox from allowing this
336        menu.findItem(R.id.sync_lookback).setVisible(isEas);
337        menu.findItem(R.id.sync_frequency).setVisible(isEas);
338
339        return mUIController.onPrepareOptionsMenu(getMenuInflater(), menu);
340    }
341
342    @Override
343    public boolean onSearchRequested() {
344        Bundle bundle = new Bundle();
345        bundle.putLong(EXTRA_ACCOUNT_ID, mUIController.getActualAccountId());
346        bundle.putLong(EXTRA_MAILBOX_ID, mUIController.getMessageListMailboxId());
347        startSearch(null, false, bundle, false);
348        return true;
349    }
350
351    // STOPSHIP Set column from user options
352    private void setMailboxColumn(String column, String value) {
353        final long mailboxId = mUIController.getMessageListMailboxId();
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        Mailbox mailbox
396                = Mailbox.restoreMailboxWithId(this, mUIController.getMessageListMailboxId());
397        if (mailbox == null) return null;
398        switch (id) {
399            case MAILBOX_SYNC_FREQUENCY_DIALOG:
400                String freq = Integer.toString(mailbox.mSyncInterval);
401                final String[] freqValues = getResources().getStringArray(
402                        R.array.account_settings_check_frequency_values_push);
403                int selection = findInStringArray(freqValues, freq);
404                // If not found, this is a push mailbox; trust me on this
405                if (selection == -1) selection = 0;
406                return new AlertDialog.Builder(this)
407                    .setIconAttribute(android.R.attr.dialogIcon)
408                    .setTitle(R.string.mailbox_options_check_frequency_label)
409                    .setSingleChoiceItems(R.array.account_settings_check_frequency_entries_push,
410                            selection,
411                            mSelectionListener)
412                    .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
413                        public void onClick(DialogInterface dialog, int which) {
414                            setMailboxColumn(MailboxColumns.SYNC_INTERVAL,
415                                    freqValues[mDialogSelection]);
416                        }})
417                    .setNegativeButton(R.string.cancel_action, mCancelListener)
418                   .create();
419
420            case MAILBOX_SYNC_LOOKBACK_DIALOG:
421                freq = Integer.toString(mailbox.mSyncLookback);
422                final String[] windowValues = getResources().getStringArray(
423                        R.array.account_settings_mail_window_values);
424                selection = findInStringArray(windowValues, freq);
425                return new AlertDialog.Builder(this)
426                    .setIconAttribute(android.R.attr.dialogIcon)
427                    .setTitle(R.string.mailbox_options_lookback_label)
428                    .setSingleChoiceItems(R.array.account_settings_mail_window_entries,
429                            selection,
430                            mSelectionListener)
431                    .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
432                        public void onClick(DialogInterface dialog, int which) {
433                            setMailboxColumn(MailboxColumns.SYNC_LOOKBACK,
434                                    windowValues[mDialogSelection]);
435                        }})
436                    .setNegativeButton(R.string.cancel_action, mCancelListener)
437                   .create();
438        }
439        return null;
440    }
441
442    @Override
443    @SuppressWarnings("deprecation")
444    public boolean onOptionsItemSelected(MenuItem item) {
445        if (mUIController.onOptionsItemSelected(item)) {
446            return true;
447        }
448        switch (item.getItemId()) {
449            // STOPSHIP Temporary mailbox settings UI
450            case R.id.sync_lookback:
451                showDialog(MAILBOX_SYNC_LOOKBACK_DIALOG);
452                return true;
453            // STOPSHIP Temporary mailbox settings UI
454            case R.id.sync_frequency:
455                showDialog(MAILBOX_SYNC_FREQUENCY_DIALOG);
456                return true;
457            case R.id.search:
458                onSearchRequested();
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) {
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