EmailActivity.java revision 458816b2c8c3bc684119adba8126584c45b468e8
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.Fragment;
21import android.content.Intent;
22import android.content.res.Configuration;
23import android.os.Bundle;
24import android.os.Handler;
25import android.text.TextUtils;
26import android.util.Log;
27import android.view.Menu;
28import android.view.MenuItem;
29import android.view.View;
30import android.view.WindowManager;
31import android.widget.TextView;
32
33import com.android.email.Controller;
34import com.android.email.ControllerResultUiThreadWrapper;
35import com.android.email.Email;
36import com.android.email.MessageListContext;
37import com.android.email.MessagingExceptionStrings;
38import com.android.email.R;
39import com.android.emailcommon.Logging;
40import com.android.emailcommon.mail.MessagingException;
41import com.android.emailcommon.provider.Account;
42import com.android.emailcommon.provider.EmailContent.Message;
43import com.android.emailcommon.provider.Mailbox;
44import com.android.emailcommon.utility.EmailAsyncTask;
45import com.android.emailcommon.utility.IntentUtilities;
46import com.google.common.base.Preconditions;
47
48import java.util.ArrayList;
49
50/**
51 * The main Email activity, which is used on both the tablet and the phone.
52 *
53 * Because this activity is device agnostic, so most of the UI aren't owned by this, but by
54 * the UIController.
55 */
56public class EmailActivity extends Activity implements View.OnClickListener, FragmentInstallable {
57    public static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID";
58    public static final String EXTRA_MAILBOX_ID = "MAILBOX_ID";
59    public static final String EXTRA_MESSAGE_ID = "MESSAGE_ID";
60    public static final String EXTRA_FROM_KEYGUARD = "FROM_KEYGUARD";
61    public static final String EXTRA_QUERY_STRING = "QUERY_STRING";
62
63    /** Loader IDs starting with this is safe to use from UIControllers. */
64    static final int UI_CONTROLLER_LOADER_ID_BASE = 100;
65
66    /** Loader IDs starting with this is safe to use from ActionBarController. */
67    static final int ACTION_BAR_CONTROLLER_LOADER_ID_BASE = 200;
68
69    private static float sLastFontScale = -1;
70
71    private Controller mController;
72    private Controller.Result mControllerResult;
73
74    private UIControllerBase mUIController;
75
76    private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
77
78    /** Banner to display errors */
79    private BannerController mErrorBanner;
80    /** Id of the account that had a messaging exception most recently. */
81    private long mLastErrorAccountId;
82
83    /**
84     * Create an intent to launch and open account's inbox.
85     *
86     * @param accountId If -1, default account will be used.
87     */
88    public static Intent createOpenAccountIntent(Activity fromActivity, long accountId,
89            boolean fromKeyguard) {
90        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
91        if (accountId != -1) {
92            i.putExtra(EXTRA_ACCOUNT_ID, accountId);
93            i.putExtra(EXTRA_FROM_KEYGUARD, fromKeyguard);
94        }
95        return i;
96    }
97
98    /**
99     * Create an intent to launch and open a mailbox.
100     *
101     * @param accountId must not be -1.
102     * @param mailboxId must not be -1.  Magic mailboxes IDs (such as
103     * {@link Mailbox#QUERY_ALL_INBOXES}) don't work.
104     */
105    public static Intent createOpenMailboxIntent(Activity fromActivity, long accountId,
106            long mailboxId, boolean fromKeyguard) {
107        if (accountId == -1 || mailboxId == -1) {
108            throw new IllegalArgumentException();
109        }
110        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
111        i.putExtra(EXTRA_ACCOUNT_ID, accountId);
112        i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
113        i.putExtra(EXTRA_FROM_KEYGUARD, fromKeyguard);
114        return i;
115    }
116
117    /**
118     * Create an intent to launch and open a message.
119     *
120     * @param accountId must not be -1.
121     * @param mailboxId must not be -1.  Magic mailboxes IDs (such as
122     * {@link Mailbox#QUERY_ALL_INBOXES}) don't work.
123     * @param messageId must not be -1.
124     */
125    public static Intent createOpenMessageIntent(Activity fromActivity, long accountId,
126            long mailboxId, long messageId, boolean fromKeyguard) {
127        if (accountId == -1 || mailboxId == -1 || messageId == -1) {
128            throw new IllegalArgumentException();
129        }
130        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class);
131        i.putExtra(EXTRA_ACCOUNT_ID, accountId);
132        i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
133        i.putExtra(EXTRA_MESSAGE_ID, messageId);
134        i.putExtra(EXTRA_FROM_KEYGUARD, fromKeyguard);
135        return i;
136    }
137
138    /**
139     * Create an intent to launch search activity.
140     *
141     * @param accountId ID of the account for the mailbox.  Must not be {@link Account#NO_ACCOUNT}.
142     * @param mailboxId ID of the mailbox to search, or {@link Mailbox#NO_MAILBOX} to perform
143     *     global search.
144     * @param query query string.
145     */
146    public static Intent createSearchIntent(Activity fromActivity, long accountId,
147            long mailboxId, String query) {
148        Preconditions.checkArgument(Account.isNormalAccount(accountId),
149                "Can only search in normal accounts");
150
151        // Note that a search doesn't use a restart intent, as we want another instance of
152        // the activity to sit on the stack for search.
153        Intent i = new Intent(fromActivity, EmailActivity.class);
154        i.putExtra(EXTRA_ACCOUNT_ID, accountId);
155        i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
156        i.putExtra(EXTRA_QUERY_STRING, query);
157        i.setAction(Intent.ACTION_SEARCH);
158        return i;
159    }
160
161    /**
162     * Initialize {@link #mUIController}.
163     */
164    private void initUIController() {
165        if (UiUtilities.useTwoPane(this)) {
166            if (getIntent().getAction() != null
167                    && Intent.ACTION_SEARCH.equals(getIntent().getAction())
168                    && !UiUtilities.showTwoPaneSearchResults(this)) {
169                mUIController = new UIControllerSearchTwoPane(this);
170            } else {
171                mUIController = new UIControllerTwoPane(this);
172            }
173        } else {
174            mUIController = new UIControllerOnePane(this);
175        }
176    }
177
178    @Override
179    protected void onCreate(Bundle savedInstanceState) {
180        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onCreate");
181
182        float fontScale = getResources().getConfiguration().fontScale;
183        if (sLastFontScale != -1 && sLastFontScale != fontScale) {
184            // If the font scale has been initialized, and has been detected to be different than
185            // the last time the Activity ran, it means the user changed the font while no
186            // Email Activity was running - we still need to purge static information though.
187            onFontScaleChangeDetected();
188        }
189        sLastFontScale = fontScale;
190
191        // UIController is used in onPrepareOptionsMenu(), which can be called from within
192        // super.onCreate(), so we need to initialize it here.
193        initUIController();
194
195        super.onCreate(savedInstanceState);
196        ActivityHelper.debugSetWindowFlags(this);
197
198        final Intent intent = getIntent();
199        boolean fromKeyguard = intent.getBooleanExtra(EXTRA_FROM_KEYGUARD, false);
200        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
201            Log.d(Logging.LOG_TAG, "FLAG_DISMISS_KEYGUARD " + fromKeyguard);
202        }
203        if (fromKeyguard) {
204            getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
205        }
206
207        setContentView(mUIController.getLayoutId());
208
209        mUIController.onActivityViewReady();
210
211        mController = Controller.getInstance(this);
212        mControllerResult = new ControllerResultUiThreadWrapper<ControllerResult>(new Handler(),
213                new ControllerResult());
214        mController.addResultCallback(mControllerResult);
215
216        // Set up views
217        // TODO Probably better to extract mErrorMessageView related code into a separate class,
218        // so that it'll be easy to reuse for the phone activities.
219        TextView errorMessage = (TextView) findViewById(R.id.error_message);
220        errorMessage.setOnClickListener(this);
221        int errorBannerHeight = getResources().getDimensionPixelSize(R.dimen.error_message_height);
222        mErrorBanner = new BannerController(this, errorMessage, errorBannerHeight);
223
224        if (savedInstanceState != null) {
225            mUIController.onRestoreInstanceState(savedInstanceState);
226        } else {
227            final MessageListContext viewContext = MessageListContext.forIntent(this, intent);
228            if (viewContext == null) {
229                // This might happen if accounts were deleted on another thread, and there aren't
230                // any remaining
231                Welcome.actionStart(this);
232                finish();
233                return;
234            } else {
235                final long messageId = intent.getLongExtra(EXTRA_MESSAGE_ID, Message.NO_MESSAGE);
236                mUIController.open(viewContext, messageId);
237            }
238        }
239        mUIController.onActivityCreated();
240    }
241
242    @Override
243    protected void onSaveInstanceState(Bundle outState) {
244        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
245            Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
246        }
247        super.onSaveInstanceState(outState);
248        mUIController.onSaveInstanceState(outState);
249    }
250
251    // FragmentInstallable
252    @Override
253    public void onInstallFragment(Fragment fragment) {
254        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
255            Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment);
256        }
257        mUIController.onInstallFragment(fragment);
258    }
259
260    // FragmentInstallable
261    @Override
262    public void onUninstallFragment(Fragment fragment) {
263        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
264            Log.d(Logging.LOG_TAG, this + " onUninstallFragment fragment=" + fragment);
265        }
266        mUIController.onUninstallFragment(fragment);
267    }
268
269    @Override
270    protected void onStart() {
271        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStart");
272        super.onStart();
273        mUIController.onActivityStart();
274    }
275
276    @Override
277    protected void onResume() {
278        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onResume");
279        super.onResume();
280        mUIController.onActivityResume();
281        /**
282         * In {@link MessageList#onResume()}, we go back to {@link Welcome} if an account
283         * has been added/removed. We don't need to do that here, because we fetch the most
284         * up-to-date account list. Additionally, we detect and do the right thing if all
285         * of the accounts have been removed.
286         */
287    }
288
289    @Override
290    protected void onPause() {
291        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onPause");
292        super.onPause();
293        mUIController.onActivityPause();
294    }
295
296    @Override
297    protected void onStop() {
298        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStop");
299        super.onStop();
300        mUIController.onActivityStop();
301    }
302
303    @Override
304    protected void onDestroy() {
305        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onDestroy");
306        mController.removeResultCallback(mControllerResult);
307        mTaskTracker.cancellAllInterrupt();
308        mUIController.onActivityDestroy();
309        super.onDestroy();
310    }
311
312    @Override
313    public void onBackPressed() {
314        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
315            Log.d(Logging.LOG_TAG, this + " onBackPressed");
316        }
317        if (!mUIController.onBackPressed(true)) {
318            // Not handled by UIController -- perform the default. i.e. close the app.
319            super.onBackPressed();
320        }
321    }
322
323    @Override
324    public void onClick(View v) {
325        switch (v.getId()) {
326            case R.id.error_message:
327                dismissErrorMessage();
328                break;
329        }
330    }
331
332    @Override
333    public void onWindowFocusChanged(boolean hasFocus) {
334        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
335            Log.d(Logging.LOG_TAG, "FLAG_DISMISS_KEYGUARD onWindowFocusChanged " + hasFocus);
336        }
337        if (hasFocus) {
338            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
339        }
340    }
341
342    /**
343     * Force dismiss the error banner.
344     */
345    private void dismissErrorMessage() {
346        mErrorBanner.dismiss();
347    }
348
349    @Override
350    public boolean onCreateOptionsMenu(Menu menu) {
351        return mUIController.onCreateOptionsMenu(getMenuInflater(), menu);
352    }
353
354    @Override
355    public boolean onPrepareOptionsMenu(Menu menu) {
356        return mUIController.onPrepareOptionsMenu(getMenuInflater(), menu);
357    }
358
359    /**
360     * Called when the search key is pressd.
361     *
362     * Use the below command to emulate the key press on devices without the search key.
363     * adb shell input keyevent 84
364     */
365    @Override
366    public boolean onSearchRequested() {
367        if (Email.DEBUG) {
368            Log.d(Logging.LOG_TAG, this + " onSearchRequested");
369        }
370        mUIController.onSearchRequested();
371        return true; // Event handled.
372    }
373
374    @Override
375    @SuppressWarnings("deprecation")
376    public boolean onOptionsItemSelected(MenuItem item) {
377        if (mUIController.onOptionsItemSelected(item)) {
378            return true;
379        }
380        return super.onOptionsItemSelected(item);
381    }
382
383    /**
384     * A {@link Controller.Result} to detect connection status.
385     */
386    private class ControllerResult extends Controller.Result {
387        @Override
388        public void sendMailCallback(
389                MessagingException result, long accountId, long messageId, int progress) {
390            handleError(result, accountId, progress);
391        }
392
393        @Override
394        public void serviceCheckMailCallback(
395                MessagingException result, long accountId, long mailboxId, int progress, long tag) {
396            handleError(result, accountId, progress);
397        }
398
399        @Override
400        public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId,
401                int progress, int numNewMessages, ArrayList<Long> addedMessages) {
402            handleError(result, accountId, progress);
403        }
404
405        @Override
406        public void updateMailboxListCallback(
407                MessagingException result, long accountId, int progress) {
408            handleError(result, accountId, progress);
409        }
410
411        @Override
412        public void loadAttachmentCallback(MessagingException result, long accountId,
413                long messageId, long attachmentId, int progress) {
414            handleError(result, accountId, progress);
415        }
416
417        @Override
418        public void loadMessageForViewCallback(MessagingException result, long accountId,
419                long messageId, int progress) {
420            handleError(result, accountId, progress);
421        }
422
423        private void handleError(final MessagingException result, final long accountId,
424                int progress) {
425            if (accountId == -1) {
426                return;
427            }
428            if (result == null) {
429                if (progress > 0) {
430                    // Connection now working; clear the error message banner
431                    if (mLastErrorAccountId == accountId) {
432                        dismissErrorMessage();
433                    }
434                }
435            } else {
436                Account account = Account.restoreAccountWithId(EmailActivity.this, accountId);
437                if (account == null) return;
438                String message =
439                    MessagingExceptionStrings.getErrorString(EmailActivity.this, result);
440                if (!TextUtils.isEmpty(account.mDisplayName)) {
441                    // TODO Use properly designed layout. Don't just concatenate strings;
442                    // which is generally poor for I18N.
443                    message = message + "   (" + account.mDisplayName + ")";
444                }
445                if (mErrorBanner.show(message)) {
446                    mLastErrorAccountId = accountId;
447                }
448             }
449        }
450    }
451
452    /**
453     * Handle a change to the system font size. This invalidates some static caches we have.
454     */
455    private void onFontScaleChangeDetected() {
456        MessageListItem.resetDrawingCaches();
457    }
458}
459