Home | History | Annotate | Download | only in activity
      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      */
     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      */
    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      */
    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      */
    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      */
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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      */
    321     private void dismissErrorMessage() {
    322         mErrorBanner.dismiss();
    323     }
    324 
    325     @Override
    326     public boolean onCreateOptionsMenu(Menu menu) {
    327         return mUIController.onCreateOptionsMenu(getMenuInflater(), menu);
    328     }
    329 
    330     @Override
    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
    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")
    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
    364         public void sendMailCallback(
    365                 MessagingException result, long accountId, long messageId, int progress) {
    366             handleError(result, accountId, progress);
    367         }
    368 
    369         @Override
    370         public void serviceCheckMailCallback(
    371                 MessagingException result, long accountId, long mailboxId, int progress, long tag) {
    372             handleError(result, accountId, progress);
    373         }
    374 
    375         @Override
    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
    382         public void updateMailboxListCallback(
    383                 MessagingException result, long accountId, int progress) {
    384             handleError(result, accountId, progress);
    385         }
    386 
    387         @Override
    388         public void loadAttachmentCallback(MessagingException result, long accountId,
    389                 long messageId, long attachmentId, int progress) {
    390             handleError(result, accountId, progress);
    391         }
    392 
    393         @Override
    394         public void loadMessageForViewCallback(MessagingException result, long accountId,
    395                 long messageId, int progress) {
    396             handleError(result, accountId, progress);
    397         }
    398 
    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      */
    431     private void onFontScaleChangeDetected() {
    432         MessageListItem.resetDrawingCaches();
    433     }
    434 }
    435