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