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