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