Home | History | Annotate | Download | only in activity
      1 /*
      2  * Copyright (C) 2008 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 com.android.email.Controller;
     20 import com.android.email.Email;
     21 import com.android.email.EmailAddressAdapter;
     22 import com.android.email.EmailAddressValidator;
     23 import com.android.email.R;
     24 import com.android.email.Utility;
     25 import com.android.email.mail.Address;
     26 import com.android.email.mail.MessagingException;
     27 import com.android.email.mail.internet.EmailHtmlUtil;
     28 import com.android.email.mail.internet.MimeUtility;
     29 import com.android.email.provider.EmailContent;
     30 import com.android.email.provider.EmailContent.Account;
     31 import com.android.email.provider.EmailContent.Attachment;
     32 import com.android.email.provider.EmailContent.Body;
     33 import com.android.email.provider.EmailContent.BodyColumns;
     34 import com.android.email.provider.EmailContent.Message;
     35 import com.android.email.provider.EmailContent.MessageColumns;
     36 import com.android.exchange.provider.GalEmailAddressAdapter;
     37 
     38 import android.app.Activity;
     39 import android.content.ActivityNotFoundException;
     40 import android.content.ContentResolver;
     41 import android.content.ContentUris;
     42 import android.content.ContentValues;
     43 import android.content.Context;
     44 import android.content.Intent;
     45 import android.content.pm.ActivityInfo;
     46 import android.database.Cursor;
     47 import android.net.Uri;
     48 import android.os.AsyncTask;
     49 import android.os.Bundle;
     50 import android.os.Handler;
     51 import android.os.Parcelable;
     52 import android.provider.OpenableColumns;
     53 import android.text.InputFilter;
     54 import android.text.SpannableStringBuilder;
     55 import android.text.Spanned;
     56 import android.text.TextUtils;
     57 import android.text.TextWatcher;
     58 import android.text.util.Rfc822Tokenizer;
     59 import android.util.Log;
     60 import android.view.Menu;
     61 import android.view.MenuItem;
     62 import android.view.View;
     63 import android.view.View.OnClickListener;
     64 import android.view.View.OnFocusChangeListener;
     65 import android.view.Window;
     66 import android.webkit.WebView;
     67 import android.widget.Button;
     68 import android.widget.EditText;
     69 import android.widget.ImageButton;
     70 import android.widget.LinearLayout;
     71 import android.widget.MultiAutoCompleteTextView;
     72 import android.widget.TextView;
     73 import android.widget.Toast;
     74 
     75 import java.io.File;
     76 import java.io.UnsupportedEncodingException;
     77 import java.net.URLDecoder;
     78 import java.util.ArrayList;
     79 import java.util.List;
     80 
     81 
     82 public class MessageCompose extends Activity implements OnClickListener, OnFocusChangeListener {
     83     private static final String ACTION_REPLY = "com.android.email.intent.action.REPLY";
     84     private static final String ACTION_REPLY_ALL = "com.android.email.intent.action.REPLY_ALL";
     85     private static final String ACTION_FORWARD = "com.android.email.intent.action.FORWARD";
     86     private static final String ACTION_EDIT_DRAFT = "com.android.email.intent.action.EDIT_DRAFT";
     87 
     88     private static final String EXTRA_ACCOUNT_ID = "account_id";
     89     private static final String EXTRA_MESSAGE_ID = "message_id";
     90     private static final String STATE_KEY_CC_SHOWN =
     91         "com.android.email.activity.MessageCompose.ccShown";
     92     private static final String STATE_KEY_BCC_SHOWN =
     93         "com.android.email.activity.MessageCompose.bccShown";
     94     private static final String STATE_KEY_QUOTED_TEXT_SHOWN =
     95         "com.android.email.activity.MessageCompose.quotedTextShown";
     96     private static final String STATE_KEY_SOURCE_MESSAGE_PROCED =
     97         "com.android.email.activity.MessageCompose.stateKeySourceMessageProced";
     98     private static final String STATE_KEY_DRAFT_ID =
     99         "com.android.email.activity.MessageCompose.draftId";
    100 
    101     private static final int MSG_UPDATE_TITLE = 3;
    102     private static final int MSG_SKIPPED_ATTACHMENTS = 4;
    103     private static final int MSG_DISCARDED_DRAFT = 6;
    104 
    105     private static final int ACTIVITY_REQUEST_PICK_ATTACHMENT = 1;
    106 
    107     private static final String[] ATTACHMENT_META_NAME_PROJECTION = {
    108         OpenableColumns.DISPLAY_NAME
    109     };
    110     private static final int ATTACHMENT_META_NAME_COLUMN_DISPLAY_NAME = 0;
    111 
    112     private static final String[] ATTACHMENT_META_SIZE_PROJECTION = {
    113         OpenableColumns.SIZE
    114     };
    115     private static final int ATTACHMENT_META_SIZE_COLUMN_SIZE = 0;
    116 
    117     // Is set while the draft is saved by a background thread.
    118     // Is static in order to be shared between the two activity instances
    119     // on orientation change.
    120     private static boolean sSaveInProgress = false;
    121     // lock and condition for sSaveInProgress
    122     private static final Object sSaveInProgressCondition = new Object();
    123 
    124     private Account mAccount;
    125 
    126     // mDraft has mId > 0 after the first draft save.
    127     private Message mDraft = new Message();
    128 
    129     // mSource is only set for REPLY, REPLY_ALL and FORWARD, and contains the source message.
    130     private Message mSource;
    131 
    132     // we use mAction instead of Intent.getAction() because sometimes we need to
    133     // re-write the action to EDIT_DRAFT.
    134     private String mAction;
    135 
    136     /**
    137      * Indicates that the source message has been processed at least once and should not
    138      * be processed on any subsequent loads. This protects us from adding attachments that
    139      * have already been added from the restore of the view state.
    140      */
    141     private boolean mSourceMessageProcessed = false;
    142 
    143     private MultiAutoCompleteTextView mToView;
    144     private MultiAutoCompleteTextView mCcView;
    145     private MultiAutoCompleteTextView mBccView;
    146     private EditText mSubjectView;
    147     private EditText mMessageContentView;
    148     private Button mSendButton;
    149     private Button mDiscardButton;
    150     private Button mSaveButton;
    151     private LinearLayout mAttachments;
    152     private View mQuotedTextBar;
    153     private ImageButton mQuotedTextDelete;
    154     private WebView mQuotedText;
    155     private TextView mLeftTitle;
    156     private TextView mRightTitle;
    157 
    158     private Controller mController;
    159     private Listener mListener;
    160     private boolean mDraftNeedsSaving;
    161     private boolean mMessageLoaded;
    162     private AsyncTask mLoadAttachmentsTask;
    163     private AsyncTask mSaveMessageTask;
    164     private AsyncTask mLoadMessageTask;
    165 
    166     private EmailAddressAdapter mAddressAdapterTo;
    167     private EmailAddressAdapter mAddressAdapterCc;
    168     private EmailAddressAdapter mAddressAdapterBcc;
    169 
    170     private Handler mHandler = new Handler() {
    171         @Override
    172         public void handleMessage(android.os.Message msg) {
    173             switch (msg.what) {
    174                 case MSG_UPDATE_TITLE:
    175                     updateTitle();
    176                     break;
    177                 case MSG_SKIPPED_ATTACHMENTS:
    178                     Toast.makeText(
    179                             MessageCompose.this,
    180                             getString(R.string.message_compose_attachments_skipped_toast),
    181                             Toast.LENGTH_LONG).show();
    182                     break;
    183                 default:
    184                     super.handleMessage(msg);
    185                     break;
    186             }
    187         }
    188     };
    189 
    190     /**
    191      * Compose a new message using the given account. If account is -1 the default account
    192      * will be used.
    193      * @param context
    194      * @param accountId
    195      */
    196     public static void actionCompose(Context context, long accountId) {
    197        try {
    198            Intent i = new Intent(context, MessageCompose.class);
    199            i.putExtra(EXTRA_ACCOUNT_ID, accountId);
    200            context.startActivity(i);
    201        } catch (ActivityNotFoundException anfe) {
    202            // Swallow it - this is usually a race condition, especially under automated test.
    203            // (The message composer might have been disabled)
    204            Email.log(anfe.toString());
    205        }
    206     }
    207 
    208     /**
    209      * Compose a new message using a uri (mailto:) and a given account.  If account is -1 the
    210      * default account will be used.
    211      * @param context
    212      * @param uriString
    213      * @param accountId
    214      * @return true if startActivity() succeeded
    215      */
    216     public static boolean actionCompose(Context context, String uriString, long accountId) {
    217         try {
    218             Intent i = new Intent(context, MessageCompose.class);
    219             i.setAction(Intent.ACTION_SEND);
    220             i.setData(Uri.parse(uriString));
    221             i.putExtra(EXTRA_ACCOUNT_ID, accountId);
    222             context.startActivity(i);
    223             return true;
    224         } catch (ActivityNotFoundException anfe) {
    225             // Swallow it - this is usually a race condition, especially under automated test.
    226             // (The message composer might have been disabled)
    227             Email.log(anfe.toString());
    228             return false;
    229         }
    230     }
    231 
    232     /**
    233      * Compose a new message as a reply to the given message. If replyAll is true the function
    234      * is reply all instead of simply reply.
    235      * @param context
    236      * @param messageId
    237      * @param replyAll
    238      */
    239     public static void actionReply(Context context, long messageId, boolean replyAll) {
    240         startActivityWithMessage(context, replyAll ? ACTION_REPLY_ALL : ACTION_REPLY, messageId);
    241     }
    242 
    243     /**
    244      * Compose a new message as a forward of the given message.
    245      * @param context
    246      * @param messageId
    247      */
    248     public static void actionForward(Context context, long messageId) {
    249         startActivityWithMessage(context, ACTION_FORWARD, messageId);
    250     }
    251 
    252     /**
    253      * Continue composition of the given message. This action modifies the way this Activity
    254      * handles certain actions.
    255      * Save will attempt to replace the message in the given folder with the updated version.
    256      * Discard will delete the message from the given folder.
    257      * @param context
    258      * @param messageId the message id.
    259      */
    260     public static void actionEditDraft(Context context, long messageId) {
    261         startActivityWithMessage(context, ACTION_EDIT_DRAFT, messageId);
    262     }
    263 
    264     private static void startActivityWithMessage(Context context, String action, long messageId) {
    265         Intent i = new Intent(context, MessageCompose.class);
    266         i.putExtra(EXTRA_MESSAGE_ID, messageId);
    267         i.setAction(action);
    268         context.startActivity(i);
    269     }
    270 
    271     private void setAccount(Intent intent) {
    272         long accountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1);
    273         if (accountId == -1) {
    274             accountId = Account.getDefaultAccountId(this);
    275         }
    276         if (accountId == -1) {
    277             // There are no accounts set up. This should not have happened. Prompt the
    278             // user to set up an account as an acceptable bailout.
    279             AccountFolderList.actionShowAccounts(this);
    280             finish();
    281         } else {
    282             setAccount(Account.restoreAccountWithId(this, accountId));
    283         }
    284     }
    285 
    286     private void setAccount(Account account) {
    287         mAccount = account;
    288         if (account != null) {
    289             mRightTitle.setText(account.mDisplayName);
    290             mAddressAdapterTo.setAccount(account);
    291             mAddressAdapterCc.setAccount(account);
    292             mAddressAdapterBcc.setAccount(account);
    293         }
    294     }
    295 
    296     @Override
    297     public void onCreate(Bundle savedInstanceState) {
    298         super.onCreate(savedInstanceState);
    299         requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);
    300         setContentView(R.layout.message_compose);
    301         getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.list_title);
    302 
    303         mController = Controller.getInstance(getApplication());
    304         mListener = new Listener();
    305         initViews();
    306         setDraftNeedsSaving(false);
    307 
    308         long draftId = -1;
    309         if (savedInstanceState != null) {
    310             // This data gets used in onCreate, so grab it here instead of onRestoreInstanceState
    311             mSourceMessageProcessed =
    312                 savedInstanceState.getBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, false);
    313             draftId = savedInstanceState.getLong(STATE_KEY_DRAFT_ID, -1);
    314         }
    315 
    316         Intent intent = getIntent();
    317         mAction = intent.getAction();
    318 
    319         if (draftId != -1) {
    320             // this means that we saved the draft earlier,
    321             // so now we need to disregard the intent action and do
    322             // EDIT_DRAFT instead.
    323             mAction = ACTION_EDIT_DRAFT;
    324             mDraft.mId = draftId;
    325         }
    326 
    327         // Handle the various intents that launch the message composer
    328         if (Intent.ACTION_VIEW.equals(mAction)
    329                 || Intent.ACTION_SENDTO.equals(mAction)
    330                 || Intent.ACTION_SEND.equals(mAction)
    331                 || Intent.ACTION_SEND_MULTIPLE.equals(mAction)) {
    332             setAccount(intent);
    333             // Use the fields found in the Intent to prefill as much of the message as possible
    334             initFromIntent(intent);
    335             setDraftNeedsSaving(true);
    336             mMessageLoaded = true;
    337             mSourceMessageProcessed = true;
    338         } else {
    339             // Otherwise, handle the internal cases (Message Composer invoked from within app)
    340             long messageId = draftId != -1 ? draftId : intent.getLongExtra(EXTRA_MESSAGE_ID, -1);
    341             if (messageId != -1) {
    342                 mLoadMessageTask = new LoadMessageTask().execute(messageId);
    343             } else {
    344                 setAccount(intent);
    345                 // Since this is a new message, we don't need to call LoadMessageTask.
    346                 // But we DO need to set mMessageLoaded to indicate the message can be sent
    347                 mMessageLoaded = true;
    348                 mSourceMessageProcessed = true;
    349             }
    350             setInitialComposeText(null, (mAccount != null) ? mAccount.mSignature : null);
    351         }
    352 
    353         if (ACTION_REPLY.equals(mAction) || ACTION_REPLY_ALL.equals(mAction) ||
    354                 ACTION_FORWARD.equals(mAction) || ACTION_EDIT_DRAFT.equals(mAction)) {
    355             /*
    356              * If we need to load the message we add ourself as a message listener here
    357              * so we can kick it off. Normally we add in onResume but we don't
    358              * want to reload the message every time the activity is resumed.
    359              * There is no harm in adding twice.
    360              */
    361             // TODO: signal the controller to load the message
    362         }
    363         updateTitle();
    364     }
    365 
    366     // needed for unit tests
    367     @Override
    368     public void setIntent(Intent intent) {
    369         super.setIntent(intent);
    370         mAction = intent.getAction();
    371     }
    372 
    373     @Override
    374     public void onResume() {
    375         super.onResume();
    376         mController.addResultCallback(mListener);
    377 
    378         // Exit immediately if the accounts list has changed (e.g. externally deleted)
    379         if (Email.getNotifyUiAccountsChanged()) {
    380             Welcome.actionStart(this);
    381             finish();
    382             return;
    383         }
    384     }
    385 
    386     @Override
    387     public void onPause() {
    388         super.onPause();
    389         saveIfNeeded();
    390         mController.removeResultCallback(mListener);
    391     }
    392 
    393     /**
    394      * We override onDestroy to make sure that the WebView gets explicitly destroyed.
    395      * Otherwise it can leak native references.
    396      */
    397     @Override
    398     public void onDestroy() {
    399         super.onDestroy();
    400         mQuotedText.destroy();
    401         mQuotedText = null;
    402 
    403         Utility.cancelTaskInterrupt(mLoadAttachmentsTask);
    404         mLoadAttachmentsTask = null;
    405         Utility.cancelTaskInterrupt(mLoadMessageTask);
    406         mLoadMessageTask = null;
    407         // don't cancel mSaveMessageTask, let it do its job to the end.
    408         mSaveMessageTask = null;
    409 
    410         if (mAddressAdapterTo != null) {
    411             mAddressAdapterTo.changeCursor(null);
    412         }
    413         if (mAddressAdapterCc != null) {
    414             mAddressAdapterCc.changeCursor(null);
    415         }
    416         if (mAddressAdapterBcc != null) {
    417             mAddressAdapterBcc.changeCursor(null);
    418         }
    419     }
    420 
    421     /**
    422      * The framework handles most of the fields, but we need to handle stuff that we
    423      * dynamically show and hide:
    424      * Cc field,
    425      * Bcc field,
    426      * Quoted text,
    427      */
    428     @Override
    429     protected void onSaveInstanceState(Bundle outState) {
    430         super.onSaveInstanceState(outState);
    431         long draftId = getOrCreateDraftId();
    432         if (draftId != -1) {
    433             outState.putLong(STATE_KEY_DRAFT_ID, draftId);
    434         }
    435         outState.putBoolean(STATE_KEY_CC_SHOWN, mCcView.getVisibility() == View.VISIBLE);
    436         outState.putBoolean(STATE_KEY_BCC_SHOWN, mBccView.getVisibility() == View.VISIBLE);
    437         outState.putBoolean(STATE_KEY_QUOTED_TEXT_SHOWN,
    438                 mQuotedTextBar.getVisibility() == View.VISIBLE);
    439         outState.putBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, mSourceMessageProcessed);
    440     }
    441 
    442     @Override
    443     protected void onRestoreInstanceState(Bundle savedInstanceState) {
    444         super.onRestoreInstanceState(savedInstanceState);
    445         mCcView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_CC_SHOWN) ?
    446                 View.VISIBLE : View.GONE);
    447         mBccView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_BCC_SHOWN) ?
    448                 View.VISIBLE : View.GONE);
    449         mQuotedTextBar.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ?
    450                 View.VISIBLE : View.GONE);
    451         mQuotedText.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ?
    452                 View.VISIBLE : View.GONE);
    453         setDraftNeedsSaving(false);
    454     }
    455 
    456     private void setDraftNeedsSaving(boolean needsSaving) {
    457         mDraftNeedsSaving = needsSaving;
    458         mSaveButton.setEnabled(needsSaving);
    459     }
    460 
    461     private void initViews() {
    462         mToView = (MultiAutoCompleteTextView)findViewById(R.id.to);
    463         mCcView = (MultiAutoCompleteTextView)findViewById(R.id.cc);
    464         mBccView = (MultiAutoCompleteTextView)findViewById(R.id.bcc);
    465         mSubjectView = (EditText)findViewById(R.id.subject);
    466         mMessageContentView = (EditText)findViewById(R.id.message_content);
    467         mSendButton = (Button)findViewById(R.id.send);
    468         mDiscardButton = (Button)findViewById(R.id.discard);
    469         mSaveButton = (Button)findViewById(R.id.save);
    470         mAttachments = (LinearLayout)findViewById(R.id.attachments);
    471         mQuotedTextBar = findViewById(R.id.quoted_text_bar);
    472         mQuotedTextDelete = (ImageButton)findViewById(R.id.quoted_text_delete);
    473         mQuotedText = (WebView)findViewById(R.id.quoted_text);
    474         mLeftTitle = (TextView)findViewById(R.id.title_left_text);
    475         mRightTitle = (TextView)findViewById(R.id.title_right_text);
    476 
    477         TextWatcher watcher = new TextWatcher() {
    478             public void beforeTextChanged(CharSequence s, int start,
    479                                           int before, int after) { }
    480 
    481             public void onTextChanged(CharSequence s, int start,
    482                                           int before, int count) {
    483                 setDraftNeedsSaving(true);
    484             }
    485 
    486             public void afterTextChanged(android.text.Editable s) { }
    487         };
    488 
    489         /**
    490          * Implements special address cleanup rules:
    491          * The first space key entry following an "@" symbol that is followed by any combination
    492          * of letters and symbols, including one+ dots and zero commas, should insert an extra
    493          * comma (followed by the space).
    494          */
    495         InputFilter recipientFilter = new InputFilter() {
    496 
    497             public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
    498                     int dstart, int dend) {
    499 
    500                 // quick check - did they enter a single space?
    501                 if (end-start != 1 || source.charAt(start) != ' ') {
    502                     return null;
    503                 }
    504 
    505                 // determine if the characters before the new space fit the pattern
    506                 // follow backwards and see if we find a comma, dot, or @
    507                 int scanBack = dstart;
    508                 boolean dotFound = false;
    509                 while (scanBack > 0) {
    510                     char c = dest.charAt(--scanBack);
    511                     switch (c) {
    512                         case '.':
    513                             dotFound = true;    // one or more dots are req'd
    514                             break;
    515                         case ',':
    516                             return null;
    517                         case '@':
    518                             if (!dotFound) {
    519                                 return null;
    520                             }
    521 
    522                             // we have found a comma-insert case.  now just do it
    523                             // in the least expensive way we can.
    524                             if (source instanceof Spanned) {
    525                                 SpannableStringBuilder sb = new SpannableStringBuilder(",");
    526                                 sb.append(source);
    527                                 return sb;
    528                             } else {
    529                                 return ", ";
    530                             }
    531                         default:
    532                             // just keep going
    533                     }
    534                 }
    535 
    536                 // no termination cases were found, so don't edit the input
    537                 return null;
    538             }
    539         };
    540         InputFilter[] recipientFilters = new InputFilter[] { recipientFilter };
    541 
    542         mToView.addTextChangedListener(watcher);
    543         mCcView.addTextChangedListener(watcher);
    544         mBccView.addTextChangedListener(watcher);
    545         mSubjectView.addTextChangedListener(watcher);
    546         mMessageContentView.addTextChangedListener(watcher);
    547 
    548         // NOTE: assumes no other filters are set
    549         mToView.setFilters(recipientFilters);
    550         mCcView.setFilters(recipientFilters);
    551         mBccView.setFilters(recipientFilters);
    552 
    553         /*
    554          * We set this to invisible by default. Other methods will turn it back on if it's
    555          * needed.
    556          */
    557         mQuotedTextBar.setVisibility(View.GONE);
    558         mQuotedText.setVisibility(View.GONE);
    559 
    560         mQuotedText.setClickable(true);
    561         mQuotedText.setLongClickable(false);    // Conflicts with ScrollView, unfortunately
    562         mQuotedTextDelete.setOnClickListener(this);
    563 
    564         EmailAddressValidator addressValidator = new EmailAddressValidator();
    565 
    566         setupAddressAdapters();
    567         mToView.setAdapter(mAddressAdapterTo);
    568         mToView.setTokenizer(new Rfc822Tokenizer());
    569         mToView.setValidator(addressValidator);
    570 
    571         mCcView.setAdapter(mAddressAdapterCc);
    572         mCcView.setTokenizer(new Rfc822Tokenizer());
    573         mCcView.setValidator(addressValidator);
    574 
    575         mBccView.setAdapter(mAddressAdapterBcc);
    576         mBccView.setTokenizer(new Rfc822Tokenizer());
    577         mBccView.setValidator(addressValidator);
    578 
    579         mSendButton.setOnClickListener(this);
    580         mDiscardButton.setOnClickListener(this);
    581         mSaveButton.setOnClickListener(this);
    582 
    583         mSubjectView.setOnFocusChangeListener(this);
    584         mMessageContentView.setOnFocusChangeListener(this);
    585     }
    586 
    587     /**
    588      * Set up address auto-completion adapters.
    589      */
    590     @SuppressWarnings("all")
    591     private void setupAddressAdapters() {
    592         /* EXCHANGE-REMOVE-SECTION-START */
    593         if (true) {
    594             mAddressAdapterTo = new GalEmailAddressAdapter(this);
    595             mAddressAdapterCc = new GalEmailAddressAdapter(this);
    596             mAddressAdapterBcc = new GalEmailAddressAdapter(this);
    597         } else {
    598             /* EXCHANGE-REMOVE-SECTION-END */
    599             mAddressAdapterTo = new EmailAddressAdapter(this);
    600             mAddressAdapterCc = new EmailAddressAdapter(this);
    601             mAddressAdapterBcc = new EmailAddressAdapter(this);
    602             /* EXCHANGE-REMOVE-SECTION-START */
    603         }
    604         /* EXCHANGE-REMOVE-SECTION-END */
    605     }
    606 
    607     // TODO: is there any way to unify this with MessageView.LoadMessageTask?
    608     private class LoadMessageTask extends AsyncTask<Long, Void, Object[]> {
    609         @Override
    610         protected Object[] doInBackground(Long... messageIds) {
    611             synchronized (sSaveInProgressCondition) {
    612                 while (sSaveInProgress) {
    613                     try {
    614                         sSaveInProgressCondition.wait();
    615                     } catch (InterruptedException e) {
    616                         // ignore & retry loop
    617                     }
    618                 }
    619             }
    620             Message message = Message.restoreMessageWithId(MessageCompose.this, messageIds[0]);
    621             if (message == null) {
    622                 return new Object[] {null, null};
    623             }
    624             long accountId = message.mAccountKey;
    625             Account account = Account.restoreAccountWithId(MessageCompose.this, accountId);
    626             try {
    627                 // Body body = Body.restoreBodyWithMessageId(MessageCompose.this, message.mId);
    628                 message.mHtml = Body.restoreBodyHtmlWithMessageId(MessageCompose.this, message.mId);
    629                 message.mText = Body.restoreBodyTextWithMessageId(MessageCompose.this, message.mId);
    630                 boolean isEditDraft = ACTION_EDIT_DRAFT.equals(mAction);
    631                 // the reply fields are only filled/used for Drafts.
    632                 if (isEditDraft) {
    633                     message.mHtmlReply =
    634                         Body.restoreReplyHtmlWithMessageId(MessageCompose.this, message.mId);
    635                     message.mTextReply =
    636                         Body.restoreReplyTextWithMessageId(MessageCompose.this, message.mId);
    637                     message.mIntroText =
    638                         Body.restoreIntroTextWithMessageId(MessageCompose.this, message.mId);
    639                     message.mSourceKey = Body.restoreBodySourceKey(MessageCompose.this,
    640                                                                    message.mId);
    641                 } else {
    642                     message.mHtmlReply = null;
    643                     message.mTextReply = null;
    644                     message.mIntroText = null;
    645                 }
    646             } catch (RuntimeException e) {
    647                 Log.d(Email.LOG_TAG, "Exception while loading message body: " + e);
    648                 return new Object[] {null, null};
    649             }
    650             return new Object[]{message, account};
    651         }
    652 
    653         @Override
    654         protected void onPostExecute(Object[] messageAndAccount) {
    655             if (messageAndAccount == null) {
    656                 return;
    657             }
    658 
    659             final Message message = (Message) messageAndAccount[0];
    660             final Account account = (Account) messageAndAccount[1];
    661             if (message == null && account == null) {
    662                 // Something unexpected happened:
    663                 // the message or the body couldn't be loaded by SQLite.
    664                 // Bail out.
    665                 Toast.makeText(MessageCompose.this, R.string.error_loading_message_body,
    666                                Toast.LENGTH_LONG).show();
    667                 finish();
    668                 return;
    669             }
    670 
    671             if (ACTION_EDIT_DRAFT.equals(mAction)) {
    672                 mDraft = message;
    673                 mLoadAttachmentsTask = new AsyncTask<Long, Void, Attachment[]>() {
    674                     @Override
    675                     protected Attachment[] doInBackground(Long... messageIds) {
    676                         return Attachment.restoreAttachmentsWithMessageId(MessageCompose.this,
    677                                 messageIds[0]);
    678                     }
    679                     @Override
    680                     protected void onPostExecute(Attachment[] attachments) {
    681                         if (attachments == null) {
    682                             return;
    683                         }
    684                         for (Attachment attachment : attachments) {
    685                             addAttachment(attachment);
    686                         }
    687                     }
    688                 }.execute(message.mId);
    689             } else if (ACTION_REPLY.equals(mAction)
    690                        || ACTION_REPLY_ALL.equals(mAction)
    691                        || ACTION_FORWARD.equals(mAction)) {
    692                 mSource = message;
    693             } else if (Email.LOGD) {
    694                 Email.log("Action " + mAction + " has unexpected EXTRA_MESSAGE_ID");
    695             }
    696 
    697             setAccount(account);
    698             processSourceMessageGuarded(message, mAccount);
    699             mMessageLoaded = true;
    700         }
    701     }
    702 
    703     private void updateTitle() {
    704         if (mSubjectView.getText().length() == 0) {
    705             mLeftTitle.setText(R.string.compose_title);
    706         } else {
    707             mLeftTitle.setText(mSubjectView.getText().toString());
    708         }
    709     }
    710 
    711     public void onFocusChange(View view, boolean focused) {
    712         if (!focused) {
    713             updateTitle();
    714         } else {
    715             switch (view.getId()) {
    716                 case R.id.message_content:
    717                     setMessageContentSelection((mAccount != null) ? mAccount.mSignature : null);
    718             }
    719         }
    720     }
    721 
    722     private void addAddresses(MultiAutoCompleteTextView view, Address[] addresses) {
    723         if (addresses == null) {
    724             return;
    725         }
    726         for (Address address : addresses) {
    727             addAddress(view, address.toString());
    728         }
    729     }
    730 
    731     private void addAddresses(MultiAutoCompleteTextView view, String[] addresses) {
    732         if (addresses == null) {
    733             return;
    734         }
    735         for (String oneAddress : addresses) {
    736             addAddress(view, oneAddress);
    737         }
    738     }
    739 
    740     private void addAddress(MultiAutoCompleteTextView view, String address) {
    741         view.append(address + ", ");
    742     }
    743 
    744     private String getPackedAddresses(TextView view) {
    745         Address[] addresses = Address.parse(view.getText().toString().trim());
    746         return Address.pack(addresses);
    747     }
    748 
    749     private Address[] getAddresses(TextView view) {
    750         Address[] addresses = Address.parse(view.getText().toString().trim());
    751         return addresses;
    752     }
    753 
    754     /*
    755      * Computes a short string indicating the destination of the message based on To, Cc, Bcc.
    756      * If only one address appears, returns the friendly form of that address.
    757      * Otherwise returns the friendly form of the first address appended with "and N others".
    758      */
    759     private String makeDisplayName(String packedTo, String packedCc, String packedBcc) {
    760         Address first = null;
    761         int nRecipients = 0;
    762         for (String packed: new String[] {packedTo, packedCc, packedBcc}) {
    763             Address[] addresses = Address.unpack(packed);
    764             nRecipients += addresses.length;
    765             if (first == null && addresses.length > 0) {
    766                 first = addresses[0];
    767             }
    768         }
    769         if (nRecipients == 0) {
    770             return "";
    771         }
    772         String friendly = first.toFriendly();
    773         if (nRecipients == 1) {
    774             return friendly;
    775         }
    776         return this.getString(R.string.message_compose_display_name, friendly, nRecipients - 1);
    777     }
    778 
    779     private ContentValues getUpdateContentValues(Message message) {
    780         ContentValues values = new ContentValues();
    781         values.put(MessageColumns.TIMESTAMP, message.mTimeStamp);
    782         values.put(MessageColumns.FROM_LIST, message.mFrom);
    783         values.put(MessageColumns.TO_LIST, message.mTo);
    784         values.put(MessageColumns.CC_LIST, message.mCc);
    785         values.put(MessageColumns.BCC_LIST, message.mBcc);
    786         values.put(MessageColumns.SUBJECT, message.mSubject);
    787         values.put(MessageColumns.DISPLAY_NAME, message.mDisplayName);
    788         values.put(MessageColumns.FLAG_READ, message.mFlagRead);
    789         values.put(MessageColumns.FLAG_LOADED, message.mFlagLoaded);
    790         values.put(MessageColumns.FLAG_ATTACHMENT, message.mFlagAttachment);
    791         values.put(MessageColumns.FLAGS, message.mFlags);
    792         return values;
    793     }
    794 
    795     /**
    796      * @param message The message to be updated.
    797      * @param account the account (used to obtain From: address).
    798      * @param bodyText the body text.
    799      */
    800     private void updateMessage(Message message, Account account, boolean hasAttachments) {
    801         if (message.mMessageId == null || message.mMessageId.length() == 0) {
    802             message.mMessageId = Utility.generateMessageId();
    803         }
    804         message.mTimeStamp = System.currentTimeMillis();
    805         message.mFrom = new Address(account.getEmailAddress(), account.getSenderName()).pack();
    806         message.mTo = getPackedAddresses(mToView);
    807         message.mCc = getPackedAddresses(mCcView);
    808         message.mBcc = getPackedAddresses(mBccView);
    809         message.mSubject = mSubjectView.getText().toString();
    810         message.mText = mMessageContentView.getText().toString();
    811         message.mAccountKey = account.mId;
    812         message.mDisplayName = makeDisplayName(message.mTo, message.mCc, message.mBcc);
    813         message.mFlagRead = true;
    814         message.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
    815         message.mFlagAttachment = hasAttachments;
    816         // Use the Intent to set flags saying this message is a reply or a forward and save the
    817         // unique id of the source message
    818         if (mSource != null && mQuotedTextBar.getVisibility() == View.VISIBLE) {
    819             if (ACTION_REPLY.equals(mAction) || ACTION_REPLY_ALL.equals(mAction)
    820                     || ACTION_FORWARD.equals(mAction)) {
    821                 message.mSourceKey = mSource.mId;
    822                 // Get the body of the source message here
    823                 message.mHtmlReply = mSource.mHtml;
    824                 message.mTextReply = mSource.mText;
    825             }
    826 
    827             String fromAsString = Address.unpackToString(mSource.mFrom);
    828             if (ACTION_FORWARD.equals(mAction)) {
    829                 message.mFlags |= Message.FLAG_TYPE_FORWARD;
    830                 String subject = mSource.mSubject;
    831                 String to = Address.unpackToString(mSource.mTo);
    832                 String cc = Address.unpackToString(mSource.mCc);
    833                 message.mIntroText =
    834                     getString(R.string.message_compose_fwd_header_fmt, subject, fromAsString,
    835                             to != null ? to : "", cc != null ? cc : "");
    836             } else {
    837                 message.mFlags |= Message.FLAG_TYPE_REPLY;
    838                 message.mIntroText =
    839                     getString(R.string.message_compose_reply_header_fmt, fromAsString);
    840             }
    841         }
    842     }
    843 
    844     private Attachment[] getAttachmentsFromUI() {
    845         int count = mAttachments.getChildCount();
    846         Attachment[] attachments = new Attachment[count];
    847         for (int i = 0; i < count; ++i) {
    848             attachments[i] = (Attachment) mAttachments.getChildAt(i).getTag();
    849         }
    850         return attachments;
    851     }
    852 
    853     /* This method does DB operations in UI thread because
    854        the draftId is needed by onSaveInstanceState() which can't wait for it
    855        to be saved in the background.
    856        TODO: This will cause ANRs, so we need to find a better solution.
    857     */
    858     private long getOrCreateDraftId() {
    859         synchronized (mDraft) {
    860             if (mDraft.mId > 0) {
    861                 return mDraft.mId;
    862             }
    863             // don't save draft if the source message did not load yet
    864             if (!mMessageLoaded) {
    865                 return -1;
    866             }
    867             final Attachment[] attachments = getAttachmentsFromUI();
    868             updateMessage(mDraft, mAccount, attachments.length > 0);
    869             mController.saveToMailbox(mDraft, EmailContent.Mailbox.TYPE_DRAFTS);
    870             return mDraft.mId;
    871         }
    872     }
    873 
    874     /**
    875      * Send or save a message:
    876      * - out of the UI thread
    877      * - write to Drafts
    878      * - if send, invoke Controller.sendMessage()
    879      * - when operation is complete, display toast
    880      */
    881     private void sendOrSaveMessage(final boolean send) {
    882         final Attachment[] attachments = getAttachmentsFromUI();
    883         if (!mMessageLoaded) {
    884             // early save, before the message was loaded: do nothing
    885             return;
    886         }
    887         updateMessage(mDraft, mAccount, attachments.length > 0);
    888 
    889         synchronized (sSaveInProgressCondition) {
    890             sSaveInProgress = true;
    891         }
    892 
    893         mSaveMessageTask = new AsyncTask<Void, Void, Void>() {
    894             @Override
    895             protected Void doInBackground(Void... params) {
    896                 synchronized (mDraft) {
    897                     if (mDraft.isSaved()) {
    898                         // Update the message
    899                         Uri draftUri =
    900                             ContentUris.withAppendedId(mDraft.SYNCED_CONTENT_URI, mDraft.mId);
    901                         getContentResolver().update(draftUri, getUpdateContentValues(mDraft),
    902                                 null, null);
    903                         // Update the body
    904                         ContentValues values = new ContentValues();
    905                         values.put(BodyColumns.TEXT_CONTENT, mDraft.mText);
    906                         values.put(BodyColumns.TEXT_REPLY, mDraft.mTextReply);
    907                         values.put(BodyColumns.HTML_REPLY, mDraft.mHtmlReply);
    908                         values.put(BodyColumns.INTRO_TEXT, mDraft.mIntroText);
    909                         values.put(BodyColumns.SOURCE_MESSAGE_KEY, mDraft.mSourceKey);
    910                         Body.updateBodyWithMessageId(MessageCompose.this, mDraft.mId, values);
    911                     } else {
    912                         // mDraft.mId is set upon return of saveToMailbox()
    913                         mController.saveToMailbox(mDraft, EmailContent.Mailbox.TYPE_DRAFTS);
    914                     }
    915                     for (Attachment attachment : attachments) {
    916                         if (!attachment.isSaved()) {
    917                             // this attachment is new so save it to DB.
    918                             attachment.mMessageKey = mDraft.mId;
    919                             attachment.save(MessageCompose.this);
    920                         }
    921                     }
    922 
    923                     if (send) {
    924                         mController.sendMessage(mDraft.mId, mDraft.mAccountKey);
    925                     }
    926                     return null;
    927                 }
    928             }
    929 
    930             @Override
    931             protected void onPostExecute(Void dummy) {
    932                 synchronized (sSaveInProgressCondition) {
    933                     sSaveInProgress = false;
    934                     sSaveInProgressCondition.notify();
    935                 }
    936                 if (isCancelled()) {
    937                     return;
    938                 }
    939                 // Don't display the toast if the user is just changing the orientation
    940                 if (!send && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
    941                     Toast.makeText(MessageCompose.this, R.string.message_saved_toast,
    942                             Toast.LENGTH_LONG).show();
    943                 }
    944             }
    945         }.execute();
    946     }
    947 
    948     private void saveIfNeeded() {
    949         if (!mDraftNeedsSaving) {
    950             return;
    951         }
    952         setDraftNeedsSaving(false);
    953         sendOrSaveMessage(false);
    954     }
    955 
    956     /**
    957      * Checks whether all the email addresses listed in TO, CC, BCC are valid.
    958      */
    959     /* package */ boolean isAddressAllValid() {
    960         for (TextView view : new TextView[]{mToView, mCcView, mBccView}) {
    961             String addresses = view.getText().toString().trim();
    962             if (!Address.isAllValid(addresses)) {
    963                 view.setError(getString(R.string.message_compose_error_invalid_email));
    964                 return false;
    965             }
    966         }
    967         return true;
    968     }
    969 
    970     private void onSend() {
    971         if (!isAddressAllValid()) {
    972             Toast.makeText(this, getString(R.string.message_compose_error_invalid_email),
    973                            Toast.LENGTH_LONG).show();
    974         } else if (getAddresses(mToView).length == 0 &&
    975                 getAddresses(mCcView).length == 0 &&
    976                 getAddresses(mBccView).length == 0) {
    977             mToView.setError(getString(R.string.message_compose_error_no_recipients));
    978             Toast.makeText(this, getString(R.string.message_compose_error_no_recipients),
    979                     Toast.LENGTH_LONG).show();
    980         } else {
    981             sendOrSaveMessage(true);
    982             setDraftNeedsSaving(false);
    983             finish();
    984         }
    985     }
    986 
    987     private void onDiscard() {
    988         if (mDraft.mId > 0) {
    989             mController.deleteMessage(mDraft.mId, mDraft.mAccountKey);
    990         }
    991         Toast.makeText(this, getString(R.string.message_discarded_toast), Toast.LENGTH_LONG).show();
    992         setDraftNeedsSaving(false);
    993         finish();
    994     }
    995 
    996     private void onSave() {
    997         saveIfNeeded();
    998         finish();
    999     }
   1000 
   1001     private void onAddCcBcc() {
   1002         mCcView.setVisibility(View.VISIBLE);
   1003         mBccView.setVisibility(View.VISIBLE);
   1004     }
   1005 
   1006     /**
   1007      * Kick off a picker for whatever kind of MIME types we'll accept and let Android take over.
   1008      */
   1009     private void onAddAttachment() {
   1010         Intent i = new Intent(Intent.ACTION_GET_CONTENT);
   1011         i.addCategory(Intent.CATEGORY_OPENABLE);
   1012         i.setType(Email.ACCEPTABLE_ATTACHMENT_SEND_UI_TYPES[0]);
   1013         startActivityForResult(
   1014                 Intent.createChooser(i, getString(R.string.choose_attachment_dialog_title)),
   1015                 ACTIVITY_REQUEST_PICK_ATTACHMENT);
   1016     }
   1017 
   1018     private Attachment loadAttachmentInfo(Uri uri) {
   1019         long size = -1;
   1020         String name = null;
   1021         ContentResolver contentResolver = getContentResolver();
   1022 
   1023         // Load name & size independently, because not all providers support both
   1024         Cursor metadataCursor = contentResolver.query(uri, ATTACHMENT_META_NAME_PROJECTION,
   1025                 null, null, null);
   1026         if (metadataCursor != null) {
   1027             try {
   1028                 if (metadataCursor.moveToFirst()) {
   1029                     name = metadataCursor.getString(ATTACHMENT_META_NAME_COLUMN_DISPLAY_NAME);
   1030                 }
   1031             } finally {
   1032                 metadataCursor.close();
   1033             }
   1034         }
   1035         metadataCursor = contentResolver.query(uri, ATTACHMENT_META_SIZE_PROJECTION,
   1036                 null, null, null);
   1037         if (metadataCursor != null) {
   1038             try {
   1039                 if (metadataCursor.moveToFirst()) {
   1040                     size = metadataCursor.getLong(ATTACHMENT_META_SIZE_COLUMN_SIZE);
   1041                 }
   1042             } finally {
   1043                 metadataCursor.close();
   1044             }
   1045         }
   1046 
   1047         // When the name or size are not provided, we need to generate them locally.
   1048         if (name == null) {
   1049             name = uri.getLastPathSegment();
   1050         }
   1051         if (size < 0) {
   1052             // if the URI is a file: URI, ask file system for its size
   1053             if ("file".equalsIgnoreCase(uri.getScheme())) {
   1054                 String path = uri.getPath();
   1055                 if (path != null) {
   1056                     File file = new File(path);
   1057                     size = file.length();  // Returns 0 for file not found
   1058                 }
   1059             }
   1060 
   1061             if (size <= 0) {
   1062                 // The size was not measurable;  This attachment is not safe to use.
   1063                 // Quick hack to force a relevant error into the UI
   1064                 // TODO: A proper announcement of the problem
   1065                 size = Email.MAX_ATTACHMENT_UPLOAD_SIZE + 1;
   1066             }
   1067         }
   1068 
   1069         String contentType = contentResolver.getType(uri);
   1070         if (contentType == null) {
   1071             contentType = "";
   1072         }
   1073 
   1074         Attachment attachment = new Attachment();
   1075         attachment.mFileName = name;
   1076         attachment.mContentUri = uri.toString();
   1077         attachment.mSize = size;
   1078         attachment.mMimeType = contentType;
   1079         return attachment;
   1080     }
   1081 
   1082     private void addAttachment(Attachment attachment) {
   1083         // Before attaching the attachment, make sure it meets any other pre-attach criteria
   1084         if (attachment.mSize > Email.MAX_ATTACHMENT_UPLOAD_SIZE) {
   1085             Toast.makeText(this, R.string.message_compose_attachment_size, Toast.LENGTH_LONG)
   1086                     .show();
   1087             return;
   1088         }
   1089 
   1090         View view = getLayoutInflater().inflate(R.layout.message_compose_attachment,
   1091                 mAttachments, false);
   1092         TextView nameView = (TextView)view.findViewById(R.id.attachment_name);
   1093         ImageButton delete = (ImageButton)view.findViewById(R.id.attachment_delete);
   1094         nameView.setText(attachment.mFileName);
   1095         delete.setOnClickListener(this);
   1096         delete.setTag(view);
   1097         view.setTag(attachment);
   1098         mAttachments.addView(view);
   1099     }
   1100 
   1101     private void addAttachment(Uri uri) {
   1102         addAttachment(loadAttachmentInfo(uri));
   1103     }
   1104 
   1105     @Override
   1106     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
   1107         if (data == null) {
   1108             return;
   1109         }
   1110         addAttachment(data.getData());
   1111         setDraftNeedsSaving(true);
   1112     }
   1113 
   1114     public void onClick(View view) {
   1115         switch (view.getId()) {
   1116             case R.id.send:
   1117                 onSend();
   1118                 break;
   1119             case R.id.save:
   1120                 onSave();
   1121                 break;
   1122             case R.id.discard:
   1123                 onDiscard();
   1124                 break;
   1125             case R.id.attachment_delete:
   1126                 onDeleteAttachment(view);
   1127                 break;
   1128             case R.id.quoted_text_delete:
   1129                 mQuotedTextBar.setVisibility(View.GONE);
   1130                 mQuotedText.setVisibility(View.GONE);
   1131                 mDraft.mIntroText = null;
   1132                 mDraft.mTextReply = null;
   1133                 mDraft.mHtmlReply = null;
   1134                 mDraft.mSourceKey = 0;
   1135                 setDraftNeedsSaving(true);
   1136                 break;
   1137         }
   1138     }
   1139 
   1140     private void onDeleteAttachment(View delButtonView) {
   1141         /*
   1142          * The view is the delete button, and we have previously set the tag of
   1143          * the delete button to the view that owns it. We don't use parent because the
   1144          * view is very complex and could change in the future.
   1145          */
   1146         View attachmentView = (View) delButtonView.getTag();
   1147         Attachment attachment = (Attachment) attachmentView.getTag();
   1148         mAttachments.removeView(attachmentView);
   1149         if (attachment.isSaved()) {
   1150             // The following async task for deleting attachments:
   1151             // - can be started multiple times in parallel (to delete multiple attachments).
   1152             // - need not be interrupted on activity exit, instead should run to completion.
   1153             new AsyncTask<Long, Void, Void>() {
   1154                 @Override
   1155                 protected Void doInBackground(Long... attachmentIds) {
   1156                     mController.deleteAttachment(attachmentIds[0]);
   1157                     return null;
   1158                 }
   1159             }.execute(attachment.mId);
   1160         }
   1161         setDraftNeedsSaving(true);
   1162     }
   1163 
   1164     @Override
   1165     public boolean onOptionsItemSelected(MenuItem item) {
   1166         switch (item.getItemId()) {
   1167             case R.id.send:
   1168                 onSend();
   1169                 break;
   1170             case R.id.save:
   1171                 onSave();
   1172                 break;
   1173             case R.id.discard:
   1174                 onDiscard();
   1175                 break;
   1176             case R.id.add_cc_bcc:
   1177                 onAddCcBcc();
   1178                 break;
   1179             case R.id.add_attachment:
   1180                 onAddAttachment();
   1181                 break;
   1182             default:
   1183                 return super.onOptionsItemSelected(item);
   1184         }
   1185         return true;
   1186     }
   1187 
   1188     @Override
   1189     public boolean onCreateOptionsMenu(Menu menu) {
   1190         super.onCreateOptionsMenu(menu);
   1191         getMenuInflater().inflate(R.menu.message_compose_option, menu);
   1192         return true;
   1193     }
   1194 
   1195     /**
   1196      * Returns true if all attachments were able to be attached, otherwise returns false.
   1197      */
   1198 //     private boolean loadAttachments(Part part, int depth) throws MessagingException {
   1199 //         if (part.getBody() instanceof Multipart) {
   1200 //             Multipart mp = (Multipart) part.getBody();
   1201 //             boolean ret = true;
   1202 //             for (int i = 0, count = mp.getCount(); i < count; i++) {
   1203 //                 if (!loadAttachments(mp.getBodyPart(i), depth + 1)) {
   1204 //                     ret = false;
   1205 //                 }
   1206 //             }
   1207 //             return ret;
   1208 //         } else {
   1209 //             String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
   1210 //             String name = MimeUtility.getHeaderParameter(contentType, "name");
   1211 //             if (name != null) {
   1212 //                 Body body = part.getBody();
   1213 //                 if (body != null && body instanceof LocalAttachmentBody) {
   1214 //                     final Uri uri = ((LocalAttachmentBody) body).getContentUri();
   1215 //                     mHandler.post(new Runnable() {
   1216 //                         public void run() {
   1217 //                             addAttachment(uri);
   1218 //                         }
   1219 //                     });
   1220 //                 }
   1221 //                 else {
   1222 //                     return false;
   1223 //                 }
   1224 //             }
   1225 //             return true;
   1226 //         }
   1227 //     }
   1228 
   1229     /**
   1230      * Set a message body and a signature when the Activity is launched.
   1231      *
   1232      * @param text the message body
   1233      */
   1234     /* package */ void setInitialComposeText(CharSequence text, String signature) {
   1235         int textLength = 0;
   1236         if (text != null) {
   1237             mMessageContentView.append(text);
   1238             textLength = text.length();
   1239         }
   1240         if (!TextUtils.isEmpty(signature)) {
   1241             if (textLength == 0 || text.charAt(textLength - 1) != '\n') {
   1242                 mMessageContentView.append("\n");
   1243             }
   1244             mMessageContentView.append(signature);
   1245         }
   1246     }
   1247 
   1248     /**
   1249      * Fill all the widgets with the content found in the Intent Extra, if any.
   1250      *
   1251      * Note that we don't actually check the intent action  (typically VIEW, SENDTO, or SEND).
   1252      * There is enough overlap in the definitions that it makes more sense to simply check for
   1253      * all available data and use as much of it as possible.
   1254      *
   1255      * With one exception:  EXTRA_STREAM is defined as only valid for ACTION_SEND.
   1256      *
   1257      * @param intent the launch intent
   1258      */
   1259     /* package */ void initFromIntent(Intent intent) {
   1260 
   1261         // First, add values stored in top-level extras
   1262 
   1263         String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
   1264         if (extraStrings != null) {
   1265             addAddresses(mToView, extraStrings);
   1266         }
   1267         extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
   1268         if (extraStrings != null) {
   1269             addAddresses(mCcView, extraStrings);
   1270         }
   1271         extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
   1272         if (extraStrings != null) {
   1273             addAddresses(mBccView, extraStrings);
   1274         }
   1275         String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
   1276         if (extraString != null) {
   1277             mSubjectView.setText(extraString);
   1278         }
   1279 
   1280         // Next, if we were invoked with a URI, try to interpret it
   1281         // We'll take two courses here.  If it's mailto:, there is a specific set of rules
   1282         // that define various optional fields.  However, for any other scheme, we'll simply
   1283         // take the entire scheme-specific part and interpret it as a possible list of addresses.
   1284 
   1285         final Uri dataUri = intent.getData();
   1286         if (dataUri != null) {
   1287             if ("mailto".equals(dataUri.getScheme())) {
   1288                 initializeFromMailTo(dataUri.toString());
   1289             } else {
   1290                 String toText = dataUri.getSchemeSpecificPart();
   1291                 if (toText != null) {
   1292                     addAddresses(mToView, toText.split(","));
   1293                 }
   1294             }
   1295         }
   1296 
   1297         // Next, fill in the plaintext (note, this will override mailto:?body=)
   1298 
   1299         CharSequence text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT);
   1300         if (text != null) {
   1301             setInitialComposeText(text, null);
   1302         }
   1303 
   1304         // Next, convert EXTRA_STREAM into an attachment
   1305 
   1306         if (Intent.ACTION_SEND.equals(mAction) && intent.hasExtra(Intent.EXTRA_STREAM)) {
   1307             String type = intent.getType();
   1308             Uri stream = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
   1309             if (stream != null && type != null) {
   1310                 if (MimeUtility.mimeTypeMatches(type,
   1311                         Email.ACCEPTABLE_ATTACHMENT_SEND_INTENT_TYPES)) {
   1312                     addAttachment(stream);
   1313                 }
   1314             }
   1315         }
   1316 
   1317         if (Intent.ACTION_SEND_MULTIPLE.equals(mAction)
   1318                 && intent.hasExtra(Intent.EXTRA_STREAM)) {
   1319             ArrayList<Parcelable> list = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
   1320             if (list != null) {
   1321                 for (Parcelable parcelable : list) {
   1322                     Uri uri = (Uri) parcelable;
   1323                     if (uri != null) {
   1324                         Attachment attachment = loadAttachmentInfo(uri);
   1325                         if (MimeUtility.mimeTypeMatches(attachment.mMimeType,
   1326                                 Email.ACCEPTABLE_ATTACHMENT_SEND_INTENT_TYPES)) {
   1327                             addAttachment(attachment);
   1328                         }
   1329                     }
   1330                 }
   1331             }
   1332         }
   1333 
   1334         // Finally - expose fields that were filled in but are normally hidden, and set focus
   1335 
   1336         if (mCcView.length() > 0) {
   1337             mCcView.setVisibility(View.VISIBLE);
   1338         }
   1339         if (mBccView.length() > 0) {
   1340             mBccView.setVisibility(View.VISIBLE);
   1341         }
   1342         setNewMessageFocus();
   1343         setDraftNeedsSaving(false);
   1344     }
   1345 
   1346     /**
   1347      * When we are launched with an intent that includes a mailto: URI, we can actually
   1348      * gather quite a few of our message fields from it.
   1349      *
   1350      * @mailToString the href (which must start with "mailto:").
   1351      */
   1352     private void initializeFromMailTo(String mailToString) {
   1353 
   1354         // Chop up everything between mailto: and ? to find recipients
   1355         int index = mailToString.indexOf("?");
   1356         int length = "mailto".length() + 1;
   1357         String to;
   1358         try {
   1359             // Extract the recipient after mailto:
   1360             if (index == -1) {
   1361                 to = decode(mailToString.substring(length));
   1362             } else {
   1363                 to = decode(mailToString.substring(length, index));
   1364             }
   1365             addAddresses(mToView, to.split(" ,"));
   1366         } catch (UnsupportedEncodingException e) {
   1367             Log.e(Email.LOG_TAG, e.getMessage() + " while decoding '" + mailToString + "'");
   1368         }
   1369 
   1370         // Extract the other parameters
   1371 
   1372         // We need to disguise this string as a URI in order to parse it
   1373         Uri uri = Uri.parse("foo://" + mailToString);
   1374 
   1375         List<String> cc = uri.getQueryParameters("cc");
   1376         addAddresses(mCcView, cc.toArray(new String[cc.size()]));
   1377 
   1378         List<String> otherTo = uri.getQueryParameters("to");
   1379         addAddresses(mCcView, otherTo.toArray(new String[otherTo.size()]));
   1380 
   1381         List<String> bcc = uri.getQueryParameters("bcc");
   1382         addAddresses(mBccView, bcc.toArray(new String[bcc.size()]));
   1383 
   1384         List<String> subject = uri.getQueryParameters("subject");
   1385         if (subject.size() > 0) {
   1386             mSubjectView.setText(subject.get(0));
   1387         }
   1388 
   1389         List<String> body = uri.getQueryParameters("body");
   1390         if (body.size() > 0) {
   1391             setInitialComposeText(body.get(0), (mAccount != null) ? mAccount.mSignature : null);
   1392         }
   1393     }
   1394 
   1395     private String decode(String s) throws UnsupportedEncodingException {
   1396         return URLDecoder.decode(s, "UTF-8");
   1397     }
   1398 
   1399     // used by processSourceMessage()
   1400     private void displayQuotedText(String textBody, String htmlBody) {
   1401         /* Use plain-text body if available, otherwise use HTML body.
   1402          * This matches the desired behavior for IMAP/POP where we only send plain-text,
   1403          * and for EAS which sends HTML and has no plain-text body.
   1404          */
   1405         boolean plainTextFlag = textBody != null;
   1406         String text = plainTextFlag ? textBody : htmlBody;
   1407         if (text != null) {
   1408             text = plainTextFlag ? EmailHtmlUtil.escapeCharacterToDisplay(text) : text;
   1409             // TODO: re-enable EmailHtmlUtil.resolveInlineImage() for HTML
   1410             //    EmailHtmlUtil.resolveInlineImage(getContentResolver(), mAccount,
   1411             //                                     text, message, 0);
   1412             mQuotedTextBar.setVisibility(View.VISIBLE);
   1413             if (mQuotedText != null) {
   1414                 mQuotedText.setVisibility(View.VISIBLE);
   1415                 mQuotedText.loadDataWithBaseURL("email://", text, "text/html", "utf-8", null);
   1416             }
   1417         }
   1418     }
   1419 
   1420     /**
   1421      * Given a packed address String, the address of our sending account, a view, and a list of
   1422      * addressees already added to other addressing views, adds unique addressees that don't
   1423      * match our address to the passed in view
   1424      */
   1425     private boolean safeAddAddresses(String addrs, String ourAddress,
   1426             MultiAutoCompleteTextView view, ArrayList<Address> addrList) {
   1427         boolean added = false;
   1428         for (Address address : Address.unpack(addrs)) {
   1429             // Don't send to ourselves or already-included addresses
   1430             if (!address.getAddress().equalsIgnoreCase(ourAddress) && !addrList.contains(address)) {
   1431                 addrList.add(address);
   1432                 addAddress(view, address.toString());
   1433                 added = true;
   1434             }
   1435         }
   1436         return added;
   1437     }
   1438 
   1439     /**
   1440      * Set up the to and cc views properly for the "reply" and "replyAll" cases.  What's important
   1441      * is that we not 1) send to ourselves, and 2) duplicate addressees.
   1442      * @param message the message we're replying to
   1443      * @param account the account we're sending from
   1444      * @param toView the "To" view
   1445      * @param ccView the "Cc" view
   1446      * @param replyAll whether this is a replyAll (vs a reply)
   1447      */
   1448     /*package*/ void setupAddressViews(Message message, Account account,
   1449             MultiAutoCompleteTextView toView, MultiAutoCompleteTextView ccView, boolean replyAll) {
   1450         /*
   1451          * If a reply-to was included with the message use that, otherwise use the from
   1452          * or sender address.
   1453          */
   1454         Address[] replyToAddresses = Address.unpack(message.mReplyTo);
   1455         if (replyToAddresses.length == 0) {
   1456             replyToAddresses = Address.unpack(message.mFrom);
   1457         }
   1458         addAddresses(mToView, replyToAddresses);
   1459 
   1460         if (replyAll) {
   1461             // Keep a running list of addresses we're sending to
   1462             ArrayList<Address> allAddresses = new ArrayList<Address>();
   1463             String ourAddress = account.mEmailAddress;
   1464 
   1465             for (Address address: replyToAddresses) {
   1466                 allAddresses.add(address);
   1467             }
   1468 
   1469             safeAddAddresses(message.mTo, ourAddress, mToView, allAddresses);
   1470             if (safeAddAddresses(message.mCc, ourAddress, mCcView, allAddresses)) {
   1471                 mCcView.setVisibility(View.VISIBLE);
   1472             }
   1473         }
   1474     }
   1475 
   1476     void processSourceMessageGuarded(Message message, Account account) {
   1477         // Make sure we only do this once (otherwise we'll duplicate addresses!)
   1478         if (!mSourceMessageProcessed) {
   1479             processSourceMessage(message, account);
   1480             mSourceMessageProcessed = true;
   1481         }
   1482 
   1483         /* The quoted text is displayed in a WebView whose content is not automatically
   1484          * saved/restored by onRestoreInstanceState(), so we need to *always* restore it here,
   1485          * regardless of the value of mSourceMessageProcessed.
   1486          * This only concerns EDIT_DRAFT because after a configuration change we're always
   1487          * in EDIT_DRAFT.
   1488          */
   1489         if (ACTION_EDIT_DRAFT.equals(mAction)) {
   1490             displayQuotedText(message.mTextReply, message.mHtmlReply);
   1491         }
   1492     }
   1493 
   1494     /**
   1495      * Pull out the parts of the now loaded source message and apply them to the new message
   1496      * depending on the type of message being composed.
   1497      * @param message
   1498      */
   1499     /* package */
   1500     void processSourceMessage(Message message, Account account) {
   1501         setDraftNeedsSaving(true);
   1502         final String subject = message.mSubject;
   1503         if (ACTION_REPLY.equals(mAction) || ACTION_REPLY_ALL.equals(mAction)) {
   1504             setupAddressViews(message, account, mToView, mCcView,
   1505                 ACTION_REPLY_ALL.equals(mAction));
   1506             if (subject != null && !subject.toLowerCase().startsWith("re:")) {
   1507                 mSubjectView.setText("Re: " + subject);
   1508             } else {
   1509                 mSubjectView.setText(subject);
   1510             }
   1511             displayQuotedText(message.mText, message.mHtml);
   1512             setInitialComposeText(null, (account != null) ? account.mSignature : null);
   1513         } else if (ACTION_FORWARD.equals(mAction)) {
   1514             mSubjectView.setText(subject != null && !subject.toLowerCase().startsWith("fwd:") ?
   1515                     "Fwd: " + subject : subject);
   1516             displayQuotedText(message.mText, message.mHtml);
   1517             setInitialComposeText(null, (account != null) ? account.mSignature : null);
   1518                 // TODO: re-enable loadAttachments below
   1519 //                 if (!loadAttachments(message, 0)) {
   1520 //                     mHandler.sendEmptyMessage(MSG_SKIPPED_ATTACHMENTS);
   1521 //                 }
   1522         } else if (ACTION_EDIT_DRAFT.equals(mAction)) {
   1523             mSubjectView.setText(subject);
   1524             addAddresses(mToView, Address.unpack(message.mTo));
   1525             Address[] cc = Address.unpack(message.mCc);
   1526             if (cc.length > 0) {
   1527                 addAddresses(mCcView, cc);
   1528                 mCcView.setVisibility(View.VISIBLE);
   1529             }
   1530             Address[] bcc = Address.unpack(message.mBcc);
   1531             if (bcc.length > 0) {
   1532                 addAddresses(mBccView, bcc);
   1533                 mBccView.setVisibility(View.VISIBLE);
   1534             }
   1535 
   1536             mMessageContentView.setText(message.mText);
   1537             // TODO: re-enable loadAttachments
   1538             // loadAttachments(message, 0);
   1539             setDraftNeedsSaving(false);
   1540         }
   1541         setNewMessageFocus();
   1542     }
   1543 
   1544     /**
   1545      * Set a cursor to the end of a body except a signature
   1546      */
   1547     /* package */ void setMessageContentSelection(String signature) {
   1548         // when selecting the message content, explicitly move IP to the end of the message,
   1549         // so you can quickly resume typing into a draft
   1550         int selection = mMessageContentView.length();
   1551         if (!TextUtils.isEmpty(signature)) {
   1552             int signatureLength = signature.length();
   1553             int estimatedSelection = selection - signatureLength;
   1554             if (estimatedSelection >= 0) {
   1555                 CharSequence text = mMessageContentView.getText();
   1556                 int i = 0;
   1557                 while (i < signatureLength
   1558                        && text.charAt(estimatedSelection + i) == signature.charAt(i)) {
   1559                     ++i;
   1560                 }
   1561                 if (i == signatureLength) {
   1562                     selection = estimatedSelection;
   1563                     while (selection > 0 && text.charAt(selection - 1) == '\n') {
   1564                         --selection;
   1565                     }
   1566                 }
   1567             }
   1568         }
   1569         mMessageContentView.setSelection(selection, selection);
   1570     }
   1571 
   1572     /**
   1573      * In order to accelerate typing, position the cursor in the first empty field,
   1574      * or at the end of the body composition field if none are empty.  Typically, this will
   1575      * play out as follows:
   1576      *   Reply / Reply All - put cursor in the empty message body
   1577      *   Forward - put cursor in the empty To field
   1578      *   Edit Draft - put cursor in whatever field still needs entry
   1579      */
   1580     private void setNewMessageFocus() {
   1581         if (mToView.length() == 0) {
   1582             mToView.requestFocus();
   1583         } else if (mSubjectView.length() == 0) {
   1584             mSubjectView.requestFocus();
   1585         } else {
   1586             mMessageContentView.requestFocus();
   1587             setMessageContentSelection((mAccount != null) ? mAccount.mSignature : null);
   1588         }
   1589     }
   1590 
   1591     private class Listener implements Controller.Result {
   1592         public void updateMailboxListCallback(MessagingException result, long accountId,
   1593                 int progress) {
   1594         }
   1595 
   1596         public void updateMailboxCallback(MessagingException result, long accountId,
   1597                 long mailboxId, int progress, int numNewMessages) {
   1598             if (result != null || progress == 100) {
   1599                 Email.updateMailboxRefreshTime(mailboxId);
   1600             }
   1601         }
   1602 
   1603         public void loadMessageForViewCallback(MessagingException result, long messageId,
   1604                 int progress) {
   1605         }
   1606 
   1607         public void loadAttachmentCallback(MessagingException result, long messageId,
   1608                 long attachmentId, int progress) {
   1609         }
   1610 
   1611         public void serviceCheckMailCallback(MessagingException result, long accountId,
   1612                 long mailboxId, int progress, long tag) {
   1613         }
   1614 
   1615         public void sendMailCallback(MessagingException result, long accountId, long messageId,
   1616                 int progress) {
   1617         }
   1618     }
   1619 }
   1620