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