• 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 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      */
actionCompose(Context context, long accountId)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      */
actionCompose(Context context, String uriString, long accountId)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      */
actionReply(Context context, long messageId, boolean replyAll)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      */
actionForward(Context context, long messageId)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      */
actionEditDraft(Context context, long messageId)260     public static void actionEditDraft(Context context, long messageId) {
261         startActivityWithMessage(context, ACTION_EDIT_DRAFT, messageId);
262     }
263 
startActivityWithMessage(Context context, String action, long messageId)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 
setAccount(Intent intent)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 
setAccount(Account account)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
onCreate(Bundle savedInstanceState)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
setIntent(Intent intent)368     public void setIntent(Intent intent) {
369         super.setIntent(intent);
370         mAction = intent.getAction();
371     }
372 
373     @Override
onResume()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
onPause()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
onDestroy()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
onSaveInstanceState(Bundle outState)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
onRestoreInstanceState(Bundle savedInstanceState)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 
setDraftNeedsSaving(boolean needsSaving)456     private void setDraftNeedsSaving(boolean needsSaving) {
457         mDraftNeedsSaving = needsSaving;
458         mSaveButton.setEnabled(needsSaving);
459     }
460 
initViews()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")
setupAddressAdapters()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
doInBackground(Long... messageIds)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
onPostExecute(Object[] messageAndAccount)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 
updateTitle()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 
onFocusChange(View view, boolean focused)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 
addAddresses(MultiAutoCompleteTextView view, Address[] addresses)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 
addAddresses(MultiAutoCompleteTextView view, String[] addresses)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 
addAddress(MultiAutoCompleteTextView view, String address)740     private void addAddress(MultiAutoCompleteTextView view, String address) {
741         view.append(address + ", ");
742     }
743 
getPackedAddresses(TextView view)744     private String getPackedAddresses(TextView view) {
745         Address[] addresses = Address.parse(view.getText().toString().trim());
746         return Address.pack(addresses);
747     }
748 
getAddresses(TextView view)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      */
makeDisplayName(String packedTo, String packedCc, String packedBcc)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 
getUpdateContentValues(Message message)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      */
updateMessage(Message message, Account account, boolean hasAttachments)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 
getAttachmentsFromUI()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     */
getOrCreateDraftId()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      */
sendOrSaveMessage(final boolean send)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 
saveIfNeeded()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      */
isAddressAllValid()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 
onSend()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 
onDiscard()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 
onSave()996     private void onSave() {
997         saveIfNeeded();
998         finish();
999     }
1000 
onAddCcBcc()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      */
onAddAttachment()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 
loadAttachmentInfo(Uri uri)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 
addAttachment(Attachment attachment)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 
addAttachment(Uri uri)1101     private void addAttachment(Uri uri) {
1102         addAttachment(loadAttachmentInfo(uri));
1103     }
1104 
1105     @Override
onActivityResult(int requestCode, int resultCode, Intent data)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 
onClick(View view)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 
onDeleteAttachment(View delButtonView)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
onOptionsItemSelected(MenuItem item)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
onCreateOptionsMenu(Menu menu)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      */
setInitialComposeText(CharSequence text, String signature)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      */
initFromIntent(Intent intent)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      */
initializeFromMailTo(String mailToString)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 
decode(String s)1395     private String decode(String s) throws UnsupportedEncodingException {
1396         return URLDecoder.decode(s, "UTF-8");
1397     }
1398 
1399     // used by processSourceMessage()
displayQuotedText(String textBody, String htmlBody)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      */
safeAddAddresses(String addrs, String ourAddress, MultiAutoCompleteTextView view, ArrayList<Address> addrList)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      */
setupAddressViews(Message message, Account account, MultiAutoCompleteTextView toView, MultiAutoCompleteTextView ccView, boolean replyAll)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 
processSourceMessageGuarded(Message message, Account account)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 */
processSourceMessage(Message message, Account account)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      */
setMessageContentSelection(String signature)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      */
setNewMessageFocus()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 {
updateMailboxListCallback(MessagingException result, long accountId, int progress)1592         public void updateMailboxListCallback(MessagingException result, long accountId,
1593                 int progress) {
1594         }
1595 
updateMailboxCallback(MessagingException result, long accountId, long mailboxId, int progress, int numNewMessages)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 
loadMessageForViewCallback(MessagingException result, long messageId, int progress)1603         public void loadMessageForViewCallback(MessagingException result, long messageId,
1604                 int progress) {
1605         }
1606 
loadAttachmentCallback(MessagingException result, long messageId, long attachmentId, int progress)1607         public void loadAttachmentCallback(MessagingException result, long messageId,
1608                 long attachmentId, int progress) {
1609         }
1610 
serviceCheckMailCallback(MessagingException result, long accountId, long mailboxId, int progress, long tag)1611         public void serviceCheckMailCallback(MessagingException result, long accountId,
1612                 long mailboxId, int progress, long tag) {
1613         }
1614 
sendMailCallback(MessagingException result, long accountId, long messageId, int progress)1615         public void sendMailCallback(MessagingException result, long accountId, long messageId,
1616                 int progress) {
1617         }
1618     }
1619 }
1620