MessageCompose.java revision 8978aac1977408b05e386ae846c30920c7faa0a6
1
2package com.android.email.activity;
3
4import java.io.Serializable;
5import java.util.ArrayList;
6import java.util.Date;
7
8import android.app.Activity;
9import android.content.ContentResolver;
10import android.content.Context;
11import android.content.Intent;
12import android.content.pm.ActivityInfo;
13import android.database.Cursor;
14import android.net.Uri;
15import android.os.Bundle;
16import android.os.Handler;
17import android.os.Parcelable;
18import android.provider.OpenableColumns;
19import android.text.TextWatcher;
20import android.text.util.Rfc822Tokenizer;
21import android.util.Config;
22import android.util.Log;
23import android.view.Menu;
24import android.view.MenuItem;
25import android.view.View;
26import android.view.Window;
27import android.view.View.OnClickListener;
28import android.view.View.OnFocusChangeListener;
29import android.webkit.WebView;
30import android.widget.Button;
31import android.widget.EditText;
32import android.widget.ImageButton;
33import android.widget.LinearLayout;
34import android.widget.MultiAutoCompleteTextView;
35import android.widget.TextView;
36import android.widget.Toast;
37import android.widget.AutoCompleteTextView.Validator;
38
39import com.android.email.Account;
40import com.android.email.Email;
41import com.android.email.EmailAddressAdapter;
42import com.android.email.EmailAddressValidator;
43import com.android.email.MessagingController;
44import com.android.email.MessagingListener;
45import com.android.email.Preferences;
46import com.android.email.R;
47import com.android.email.Utility;
48import com.android.email.mail.Address;
49import com.android.email.mail.Body;
50import com.android.email.mail.Message;
51import com.android.email.mail.MessagingException;
52import com.android.email.mail.Multipart;
53import com.android.email.mail.Part;
54import com.android.email.mail.Message.RecipientType;
55import com.android.email.mail.internet.MimeBodyPart;
56import com.android.email.mail.internet.MimeHeader;
57import com.android.email.mail.internet.MimeMessage;
58import com.android.email.mail.internet.MimeMultipart;
59import com.android.email.mail.internet.MimeUtility;
60import com.android.email.mail.internet.TextBody;
61import com.android.email.mail.store.LocalStore;
62import com.android.email.mail.store.LocalStore.LocalAttachmentBody;
63
64public class MessageCompose extends Activity implements OnClickListener, OnFocusChangeListener {
65    private static final String ACTION_REPLY = "com.android.email.intent.action.REPLY";
66    private static final String ACTION_REPLY_ALL = "com.android.email.intent.action.REPLY_ALL";
67    private static final String ACTION_FORWARD = "com.android.email.intent.action.FORWARD";
68    private static final String ACTION_EDIT_DRAFT = "com.android.email.intent.action.EDIT_DRAFT";
69
70    private static final String EXTRA_ACCOUNT = "account";
71    private static final String EXTRA_FOLDER = "folder";
72    private static final String EXTRA_MESSAGE = "message";
73
74    private static final String STATE_KEY_ATTACHMENTS =
75        "com.android.email.activity.MessageCompose.attachments";
76    private static final String STATE_KEY_CC_SHOWN =
77        "com.android.email.activity.MessageCompose.ccShown";
78    private static final String STATE_KEY_BCC_SHOWN =
79        "com.android.email.activity.MessageCompose.bccShown";
80    private static final String STATE_KEY_QUOTED_TEXT_SHOWN =
81        "com.android.email.activity.MessageCompose.quotedTextShown";
82    private static final String STATE_KEY_SOURCE_MESSAGE_PROCED =
83        "com.android.email.activity.MessageCompose.stateKeySourceMessageProced";
84    private static final String STATE_KEY_DRAFT_UID =
85        "com.android.email.activity.MessageCompose.draftUid";
86
87    private static final int MSG_PROGRESS_ON = 1;
88    private static final int MSG_PROGRESS_OFF = 2;
89    private static final int MSG_UPDATE_TITLE = 3;
90    private static final int MSG_SKIPPED_ATTACHMENTS = 4;
91    private static final int MSG_SAVED_DRAFT = 5;
92    private static final int MSG_DISCARDED_DRAFT = 6;
93
94    private static final int ACTIVITY_REQUEST_PICK_ATTACHMENT = 1;
95
96    private Account mAccount;
97    private String mFolder;
98    private String mSourceMessageUid;
99    private Message mSourceMessage;
100    /**
101     * Indicates that the source message has been processed at least once and should not
102     * be processed on any subsequent loads. This protects us from adding attachments that
103     * have already been added from the restore of the view state.
104     */
105    private boolean mSourceMessageProcessed = false;
106
107    private MultiAutoCompleteTextView mToView;
108    private MultiAutoCompleteTextView mCcView;
109    private MultiAutoCompleteTextView mBccView;
110    private EditText mSubjectView;
111    private EditText mMessageContentView;
112    private Button mSendButton;
113    private Button mDiscardButton;
114    private Button mSaveButton;
115    private LinearLayout mAttachments;
116    private View mQuotedTextBar;
117    private ImageButton mQuotedTextDelete;
118    private WebView mQuotedText;
119
120    private boolean mDraftNeedsSaving = false;
121
122    /**
123     * The draft uid of this message. This is used when saving drafts so that the same draft is
124     * overwritten instead of being created anew. This property is null until the first save.
125     */
126    private String mDraftUid;
127
128    private Handler mHandler = new Handler() {
129        @Override
130        public void handleMessage(android.os.Message msg) {
131            switch (msg.what) {
132                case MSG_PROGRESS_ON:
133                    setProgressBarIndeterminateVisibility(true);
134                    break;
135                case MSG_PROGRESS_OFF:
136                    setProgressBarIndeterminateVisibility(false);
137                    break;
138                case MSG_UPDATE_TITLE:
139                    updateTitle();
140                    break;
141                case MSG_SKIPPED_ATTACHMENTS:
142                    Toast.makeText(
143                            MessageCompose.this,
144                            getString(R.string.message_compose_attachments_skipped_toast),
145                            Toast.LENGTH_LONG).show();
146                    break;
147                case MSG_SAVED_DRAFT:
148                    Toast.makeText(
149                            MessageCompose.this,
150                            getString(R.string.message_saved_toast),
151                            Toast.LENGTH_LONG).show();
152                    break;
153                case MSG_DISCARDED_DRAFT:
154                    Toast.makeText(
155                            MessageCompose.this,
156                            getString(R.string.message_discarded_toast),
157                            Toast.LENGTH_LONG).show();
158                    break;
159                default:
160                    super.handleMessage(msg);
161                    break;
162            }
163        }
164    };
165
166    private Listener mListener = new Listener();
167    private EmailAddressAdapter mAddressAdapter;
168    private Validator mAddressValidator;
169
170
171    class Attachment implements Serializable {
172        public String name;
173        public String contentType;
174        public long size;
175        public Uri uri;
176    }
177
178    /**
179     * Compose a new message using the given account. If account is null the default account
180     * will be used.
181     * @param context
182     * @param account
183     */
184    public static void actionCompose(Context context, Account account) {
185       Intent i = new Intent(context, MessageCompose.class);
186       i.putExtra(EXTRA_ACCOUNT, account);
187       context.startActivity(i);
188    }
189
190    /**
191     * Compose a new message as a reply to the given message. If replyAll is true the function
192     * is reply all instead of simply reply.
193     * @param context
194     * @param account
195     * @param message
196     * @param replyAll
197     */
198    public static void actionReply(
199            Context context,
200            Account account,
201            Message message,
202            boolean replyAll) {
203        Intent i = new Intent(context, MessageCompose.class);
204        i.putExtra(EXTRA_ACCOUNT, account);
205        i.putExtra(EXTRA_FOLDER, message.getFolder().getName());
206        i.putExtra(EXTRA_MESSAGE, message.getUid());
207        if (replyAll) {
208            i.setAction(ACTION_REPLY_ALL);
209        }
210        else {
211            i.setAction(ACTION_REPLY);
212        }
213        context.startActivity(i);
214    }
215
216    /**
217     * Compose a new message as a forward of the given message.
218     * @param context
219     * @param account
220     * @param message
221     */
222    public static void actionForward(Context context, Account account, Message message) {
223        Intent i = new Intent(context, MessageCompose.class);
224        i.putExtra(EXTRA_ACCOUNT, account);
225        i.putExtra(EXTRA_FOLDER, message.getFolder().getName());
226        i.putExtra(EXTRA_MESSAGE, message.getUid());
227        i.setAction(ACTION_FORWARD);
228        context.startActivity(i);
229    }
230
231    /**
232     * Continue composition of the given message. This action modifies the way this Activity
233     * handles certain actions.
234     * Save will attempt to replace the message in the given folder with the updated version.
235     * Discard will delete the message from the given folder.
236     * @param context
237     * @param account
238     * @param folder
239     * @param message
240     */
241    public static void actionEditDraft(Context context, Account account, Message message) {
242        Intent i = new Intent(context, MessageCompose.class);
243        i.putExtra(EXTRA_ACCOUNT, account);
244        i.putExtra(EXTRA_FOLDER, message.getFolder().getName());
245        i.putExtra(EXTRA_MESSAGE, message.getUid());
246        i.setAction(ACTION_EDIT_DRAFT);
247        context.startActivity(i);
248    }
249
250    public void onCreate(Bundle savedInstanceState) {
251        super.onCreate(savedInstanceState);
252
253        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
254
255        setContentView(R.layout.message_compose);
256
257        mAddressAdapter = new EmailAddressAdapter(this);
258        mAddressValidator = new EmailAddressValidator();
259
260        mToView = (MultiAutoCompleteTextView)findViewById(R.id.to);
261        mCcView = (MultiAutoCompleteTextView)findViewById(R.id.cc);
262        mBccView = (MultiAutoCompleteTextView)findViewById(R.id.bcc);
263        mSubjectView = (EditText)findViewById(R.id.subject);
264        mMessageContentView = (EditText)findViewById(R.id.message_content);
265        mSendButton = (Button)findViewById(R.id.send);
266        mDiscardButton = (Button)findViewById(R.id.discard);
267        mSaveButton = (Button)findViewById(R.id.save);
268        mAttachments = (LinearLayout)findViewById(R.id.attachments);
269        mQuotedTextBar = findViewById(R.id.quoted_text_bar);
270        mQuotedTextDelete = (ImageButton)findViewById(R.id.quoted_text_delete);
271        mQuotedText = (WebView)findViewById(R.id.quoted_text);
272
273        TextWatcher watcher = new TextWatcher() {
274            public void beforeTextChanged(CharSequence s, int start,
275                                          int before, int after) { }
276
277            public void onTextChanged(CharSequence s, int start,
278                                          int before, int count) {
279                mDraftNeedsSaving = true;
280            }
281
282            public void afterTextChanged(android.text.Editable s) { }
283        };
284
285        mToView.addTextChangedListener(watcher);
286        mCcView.addTextChangedListener(watcher);
287        mBccView.addTextChangedListener(watcher);
288        mSubjectView.addTextChangedListener(watcher);
289        mMessageContentView.addTextChangedListener(watcher);
290
291        /*
292         * We set this to invisible by default. Other methods will turn it back on if it's
293         * needed.
294         */
295        mQuotedTextBar.setVisibility(View.GONE);
296        mQuotedText.setVisibility(View.GONE);
297
298        mQuotedTextDelete.setOnClickListener(this);
299
300        mToView.setAdapter(mAddressAdapter);
301        mToView.setTokenizer(new Rfc822Tokenizer());
302        mToView.setValidator(mAddressValidator);
303
304        mCcView.setAdapter(mAddressAdapter);
305        mCcView.setTokenizer(new Rfc822Tokenizer());
306        mCcView.setValidator(mAddressValidator);
307
308        mBccView.setAdapter(mAddressAdapter);
309        mBccView.setTokenizer(new Rfc822Tokenizer());
310        mBccView.setValidator(mAddressValidator);
311
312        mSendButton.setOnClickListener(this);
313        mDiscardButton.setOnClickListener(this);
314        mSaveButton.setOnClickListener(this);
315
316        mSubjectView.setOnFocusChangeListener(this);
317
318        if (savedInstanceState != null) {
319            /*
320             * This data gets used in onCreate, so grab it here instead of onRestoreIntstanceState
321             */
322            mSourceMessageProcessed =
323                savedInstanceState.getBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, false);
324        }
325
326        Intent intent = getIntent();
327
328        String action = intent.getAction();
329
330        if (Intent.ACTION_VIEW.equals(action) || Intent.ACTION_SENDTO.equals(action)) {
331            /*
332             * Someone has clicked a mailto: link. The address is in the URI.
333             */
334            mAccount = Preferences.getPreferences(this).getDefaultAccount();
335            if (mAccount == null) {
336                /*
337                 * There are no accounts set up. This should not have happened. Prompt the
338                 * user to set up an account as an acceptable bailout.
339                 */
340                startActivity(new Intent(this, Accounts.class));
341                mDraftNeedsSaving = false;
342                finish();
343                return;
344            }
345            if (intent.getData() != null) {
346                Uri uri = intent.getData();
347                try {
348                    if (uri.getScheme().equalsIgnoreCase("mailto")) {
349                        Address[] addresses = Address.parse(uri.getSchemeSpecificPart());
350                        addAddresses(mToView, addresses);
351                    }
352                }
353                catch (Exception e) {
354                    /*
355                     * If we can't extract any information from the URI it's okay. They can
356                     * still compose a message.
357                     */
358                }
359            }
360        }
361        else if (Intent.ACTION_SEND.equals(action)) {
362            /*
363             * Someone is trying to compose an email with an attachment, probably Pictures.
364             * The Intent should contain an EXTRA_STREAM with the data to attach.
365             */
366
367            mAccount = Preferences.getPreferences(this).getDefaultAccount();
368            if (mAccount == null) {
369                /*
370                 * There are no accounts set up. This should not have happened. Prompt the
371                 * user to set up an account as an acceptable bailout.
372                 */
373                startActivity(new Intent(this, Accounts.class));
374                mDraftNeedsSaving = false;
375                finish();
376                return;
377            }
378
379            String type = intent.getType();
380            Uri stream = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
381            if (stream != null && type != null) {
382                if (MimeUtility.mimeTypeMatches(type, Email.ACCEPTABLE_ATTACHMENT_SEND_TYPES)) {
383                    addAttachment(stream);
384                }
385            }
386        }
387        else {
388            mAccount = (Account) intent.getSerializableExtra(EXTRA_ACCOUNT);
389            mFolder = (String) intent.getStringExtra(EXTRA_FOLDER);
390            mSourceMessageUid = (String) intent.getStringExtra(EXTRA_MESSAGE);
391        }
392
393        if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action) ||
394                ACTION_FORWARD.equals(action) || ACTION_EDIT_DRAFT.equals(action)) {
395            /*
396             * If we need to load the message we add ourself as a message listener here
397             * so we can kick it off. Normally we add in onResume but we don't
398             * want to reload the message every time the activity is resumed.
399             * There is no harm in adding twice.
400             */
401            MessagingController.getInstance(getApplication()).addListener(mListener);
402            MessagingController.getInstance(getApplication()).loadMessageForView(
403                    mAccount,
404                    mFolder,
405                    mSourceMessageUid,
406                    mListener);
407        }
408
409        updateTitle();
410    }
411
412    public void onResume() {
413        super.onResume();
414        MessagingController.getInstance(getApplication()).addListener(mListener);
415    }
416
417    public void onPause() {
418        super.onPause();
419        saveIfNeeded();
420        MessagingController.getInstance(getApplication()).removeListener(mListener);
421    }
422
423    /**
424     * The framework handles most of the fields, but we need to handle stuff that we
425     * dynamically show and hide:
426     * Attachment list,
427     * Cc field,
428     * Bcc field,
429     * Quoted text,
430     */
431    @Override
432    protected void onSaveInstanceState(Bundle outState) {
433        super.onSaveInstanceState(outState);
434        saveIfNeeded();
435        ArrayList<Uri> attachments = new ArrayList<Uri>();
436        for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
437            View view = mAttachments.getChildAt(i);
438            Attachment attachment = (Attachment) view.getTag();
439            attachments.add(attachment.uri);
440        }
441        outState.putParcelableArrayList(STATE_KEY_ATTACHMENTS, attachments);
442        outState.putBoolean(STATE_KEY_CC_SHOWN, mCcView.getVisibility() == View.VISIBLE);
443        outState.putBoolean(STATE_KEY_BCC_SHOWN, mBccView.getVisibility() == View.VISIBLE);
444        outState.putBoolean(STATE_KEY_QUOTED_TEXT_SHOWN,
445                mQuotedTextBar.getVisibility() == View.VISIBLE);
446        outState.putBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, mSourceMessageProcessed);
447        outState.putString(STATE_KEY_DRAFT_UID, mDraftUid);
448    }
449
450    @Override
451    protected void onRestoreInstanceState(Bundle savedInstanceState) {
452        super.onRestoreInstanceState(savedInstanceState);
453        ArrayList<Parcelable> attachments = (ArrayList<Parcelable>)
454                savedInstanceState.getParcelableArrayList(STATE_KEY_ATTACHMENTS);
455        mAttachments.removeAllViews();
456        for (Parcelable p : attachments) {
457            Uri uri = (Uri) p;
458            addAttachment(uri);
459        }
460
461        mCcView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_CC_SHOWN) ?
462                View.VISIBLE : View.GONE);
463        mBccView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_BCC_SHOWN) ?
464                View.VISIBLE : View.GONE);
465        mQuotedTextBar.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ?
466                View.VISIBLE : View.GONE);
467        mQuotedText.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ?
468                View.VISIBLE : View.GONE);
469        mDraftUid = savedInstanceState.getString(STATE_KEY_DRAFT_UID);
470        mDraftNeedsSaving = false;
471    }
472
473    private void updateTitle() {
474        if (mSubjectView.getText().length() == 0) {
475            setTitle(R.string.compose_title);
476        } else {
477            setTitle(mSubjectView.getText().toString());
478        }
479    }
480
481    public void onFocusChange(View view, boolean focused) {
482        if (!focused) {
483            updateTitle();
484        }
485    }
486
487    private void addAddresses(MultiAutoCompleteTextView view, Address[] addresses) {
488        if (addresses == null) {
489            return;
490        }
491        for (Address address : addresses) {
492            addAddress(view, address);
493        }
494    }
495
496    private void addAddress(MultiAutoCompleteTextView view, Address address) {
497        view.append(address + ", ");
498    }
499
500    private Address[] getAddresses(MultiAutoCompleteTextView view) {
501        Address[] addresses = Address.parse(view.getText().toString().trim());
502        return addresses;
503    }
504
505    private MimeMessage createMessage() throws MessagingException {
506        MimeMessage message = new MimeMessage();
507        message.setSentDate(new Date());
508        Address from = new Address(mAccount.getEmail(), mAccount.getName());
509        message.setFrom(from);
510        message.setRecipients(RecipientType.TO, getAddresses(mToView));
511        message.setRecipients(RecipientType.CC, getAddresses(mCcView));
512        message.setRecipients(RecipientType.BCC, getAddresses(mBccView));
513        message.setSubject(mSubjectView.getText().toString());
514
515        /*
516         * Build the Body that will contain the text of the message. We'll decide where to
517         * include it later.
518         */
519
520        String text = mMessageContentView.getText().toString();
521
522        if (mQuotedTextBar.getVisibility() == View.VISIBLE) {
523            String action = getIntent().getAction();
524            String quotedText = null;
525            Part part = MimeUtility.findFirstPartByMimeType(mSourceMessage,
526                    "text/plain");
527            if (part != null) {
528                quotedText = MimeUtility.getTextFromPart(part);
529            }
530            if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action)) {
531                text += String.format(
532                        getString(R.string.message_compose_reply_header_fmt),
533                        Address.toString(mSourceMessage.getFrom()));
534                if (quotedText != null) {
535                    text += quotedText.replaceAll("(?m)^", ">");
536                }
537            }
538            else if (ACTION_FORWARD.equals(action)) {
539                text += String.format(
540                        getString(R.string.message_compose_fwd_header_fmt),
541                        mSourceMessage.getSubject(),
542                        Address.toString(mSourceMessage.getFrom()),
543                        Address.toString(
544                                mSourceMessage.getRecipients(RecipientType.TO)),
545                        Address.toString(
546                                mSourceMessage.getRecipients(RecipientType.CC)));
547                if (quotedText != null) {
548                    text += quotedText;
549                }
550            }
551        }
552
553        TextBody body = new TextBody(text);
554
555        if (mAttachments.getChildCount() > 0) {
556            /*
557             * The message has attachments that need to be included. First we add the part
558             * containing the text that will be sent and then we include each attachment.
559             */
560
561            MimeMultipart mp;
562
563            mp = new MimeMultipart();
564            mp.addBodyPart(new MimeBodyPart(body, "text/plain"));
565
566            for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
567                Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag();
568                MimeBodyPart bp = new MimeBodyPart(
569                        new LocalStore.LocalAttachmentBody(attachment.uri, getApplication()));
570                bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n name=\"%s\"",
571                        attachment.contentType,
572                        attachment.name));
573                bp.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
574                bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
575                        String.format("attachment;\n filename=\"%s\"",
576                        attachment.name));
577                mp.addBodyPart(bp);
578            }
579
580            message.setBody(mp);
581        }
582        else {
583            /*
584             * No attachments to include, just stick the text body in the message and call
585             * it good.
586             */
587            message.setBody(body);
588        }
589
590        return message;
591    }
592
593    private void sendOrSaveMessage(boolean save) {
594        /*
595         * Create the message from all the data the user has entered.
596         */
597        MimeMessage message;
598        try {
599            message = createMessage();
600        }
601        catch (MessagingException me) {
602            Log.e(Email.LOG_TAG, "Failed to create new message for send or save.", me);
603            throw new RuntimeException("Failed to create a new message for send or save.", me);
604        }
605
606        if (save) {
607            /*
608             * Save a draft
609             */
610            if (mDraftUid != null) {
611                message.setUid(mDraftUid);
612            }
613            else if (ACTION_EDIT_DRAFT.equals(getIntent().getAction())) {
614                /*
615                 * We're saving a previously saved draft, so update the new message's uid
616                 * to the old message's uid.
617                 */
618                message.setUid(mSourceMessageUid);
619            }
620            MessagingController.getInstance(getApplication()).saveDraft(mAccount, message);
621            mDraftUid = message.getUid();
622
623            // Don't display the toast if the user is just changing the orientation
624            if ((getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
625                mHandler.sendEmptyMessage(MSG_SAVED_DRAFT);
626            }
627        }
628        else {
629            /*
630             * Send the message
631             * TODO Is it possible for us to be editing a draft with a null source message? Don't
632             * think so. Could probably remove below check.
633             */
634            if (ACTION_EDIT_DRAFT.equals(getIntent().getAction())
635                    && mSourceMessageUid != null) {
636                /*
637                 * We're sending a previously saved draft, so delete the old draft first.
638                 */
639                MessagingController.getInstance(getApplication()).deleteMessage(
640                        mAccount,
641                        mFolder,
642                        mSourceMessage,
643                        null);
644            }
645            MessagingController.getInstance(getApplication()).sendMessage(mAccount, message, null);
646        }
647    }
648
649    private void saveIfNeeded() {
650        if (!mDraftNeedsSaving) {
651            return;
652        }
653        mDraftNeedsSaving = false;
654        sendOrSaveMessage(true);
655    }
656
657    private void onSend() {
658        if (getAddresses(mToView).length == 0 &&
659                getAddresses(mCcView).length == 0 &&
660                getAddresses(mBccView).length == 0) {
661            mToView.setError(getString(R.string.message_compose_error_no_recipients));
662            Toast.makeText(this, getString(R.string.message_compose_error_no_recipients),
663                    Toast.LENGTH_LONG).show();
664            return;
665        }
666        sendOrSaveMessage(false);
667        mDraftNeedsSaving = false;
668        finish();
669    }
670
671    private void onDiscard() {
672        if (mSourceMessageUid != null) {
673            if (ACTION_EDIT_DRAFT.equals(getIntent().getAction()) && mSourceMessageUid != null) {
674                MessagingController.getInstance(getApplication()).deleteMessage(
675                        mAccount,
676                        mFolder,
677                        mSourceMessage,
678                        null);
679            }
680        }
681        mHandler.sendEmptyMessage(MSG_DISCARDED_DRAFT);
682        mDraftNeedsSaving = false;
683        finish();
684    }
685
686    private void onSave() {
687        saveIfNeeded();
688        finish();
689    }
690
691    private void onAddCcBcc() {
692        mCcView.setVisibility(View.VISIBLE);
693        mBccView.setVisibility(View.VISIBLE);
694    }
695
696    /**
697     * Kick off a picker for whatever kind of MIME types we'll accept and let Android take over.
698     */
699    private void onAddAttachment() {
700        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
701        i.addCategory(Intent.CATEGORY_OPENABLE);
702        i.setType(Email.ACCEPTABLE_ATTACHMENT_SEND_TYPES[0]);
703        startActivityForResult(Intent.createChooser(i, null), ACTIVITY_REQUEST_PICK_ATTACHMENT);
704    }
705
706    private void addAttachment(Uri uri) {
707        addAttachment(uri, -1, null);
708    }
709
710    private void addAttachment(Uri uri, int size, String name) {
711        ContentResolver contentResolver = getContentResolver();
712
713        String contentType = contentResolver.getType(uri);
714
715        if (contentType == null) {
716            contentType = "";
717        }
718
719        Attachment attachment = new Attachment();
720        attachment.name = name;
721        attachment.contentType = contentType;
722        attachment.size = size;
723        attachment.uri = uri;
724
725        if (attachment.size == -1 || attachment.name == null) {
726            Cursor metadataCursor = contentResolver.query(
727                    uri,
728                    new String[]{ OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE },
729                    null,
730                    null,
731                    null);
732            if (metadataCursor != null) {
733                try {
734                    if (metadataCursor.moveToFirst()) {
735                        if (attachment.name == null) {
736                            attachment.name = metadataCursor.getString(0);
737                        }
738                        if (attachment.size == -1) {
739                            attachment.size = metadataCursor.getInt(1);
740                        }
741                    }
742                } finally {
743                    metadataCursor.close();
744                }
745            }
746        }
747
748        if (attachment.name == null) {
749            attachment.name = uri.getLastPathSegment();
750        }
751
752        View view = getLayoutInflater().inflate(
753                R.layout.message_compose_attachment,
754                mAttachments,
755                false);
756        TextView nameView = (TextView)view.findViewById(R.id.attachment_name);
757        ImageButton delete = (ImageButton)view.findViewById(R.id.attachment_delete);
758        nameView.setText(attachment.name);
759        delete.setOnClickListener(this);
760        delete.setTag(view);
761        view.setTag(attachment);
762        mAttachments.addView(view);
763    }
764
765    @Override
766    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
767        if (data == null) {
768            return;
769        }
770        addAttachment(data.getData());
771        mDraftNeedsSaving = true;
772    }
773
774    public void onClick(View view) {
775        switch (view.getId()) {
776            case R.id.send:
777                onSend();
778                break;
779            case R.id.save:
780                onSave();
781                break;
782            case R.id.discard:
783                onDiscard();
784                break;
785            case R.id.attachment_delete:
786                /*
787                 * The view is the delete button, and we have previously set the tag of
788                 * the delete button to the view that owns it. We don't use parent because the
789                 * view is very complex and could change in the future.
790                 */
791                mAttachments.removeView((View) view.getTag());
792                mDraftNeedsSaving = true;
793                break;
794            case R.id.quoted_text_delete:
795                mQuotedTextBar.setVisibility(View.GONE);
796                mQuotedText.setVisibility(View.GONE);
797                mDraftNeedsSaving = true;
798                break;
799        }
800    }
801
802    public boolean onOptionsItemSelected(MenuItem item) {
803        switch (item.getItemId()) {
804            case R.id.send:
805                onSend();
806                break;
807            case R.id.save:
808                onSave();
809                break;
810            case R.id.discard:
811                onDiscard();
812                break;
813            case R.id.add_cc_bcc:
814                onAddCcBcc();
815                break;
816            case R.id.add_attachment:
817                onAddAttachment();
818                break;
819            default:
820                return super.onOptionsItemSelected(item);
821        }
822        return true;
823    }
824
825    public boolean onCreateOptionsMenu(Menu menu) {
826        super.onCreateOptionsMenu(menu);
827        getMenuInflater().inflate(R.menu.message_compose_option, menu);
828        return true;
829    }
830
831    /**
832     * Returns true if all attachments were able to be attached, otherwise returns false.
833     */
834    private boolean loadAttachments(Part part, int depth) throws MessagingException {
835        if (part.getBody() instanceof Multipart) {
836            Multipart mp = (Multipart) part.getBody();
837            boolean ret = true;
838            for (int i = 0, count = mp.getCount(); i < count; i++) {
839                if (!loadAttachments(mp.getBodyPart(i), depth + 1)) {
840                    ret = false;
841                }
842            }
843            return ret;
844        } else {
845            String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
846            String name = MimeUtility.getHeaderParameter(contentType, "name");
847            if (name != null) {
848                Body body = part.getBody();
849                if (body != null && body instanceof LocalAttachmentBody) {
850                    final Uri uri = ((LocalAttachmentBody) body).getContentUri();
851                    mHandler.post(new Runnable() {
852                        public void run() {
853                            addAttachment(uri);
854                        }
855                    });
856                }
857                else {
858                    return false;
859                }
860            }
861            return true;
862        }
863    }
864
865    /**
866     * Pull out the parts of the now loaded source message and apply them to the new message
867     * depending on the type of message being composed.
868     * @param message
869     */
870    private void processSourceMessage(Message message) {
871        String action = getIntent().getAction();
872        if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action)) {
873            try {
874                if (message.getSubject() != null &&
875                        !message.getSubject().toLowerCase().startsWith("re:")) {
876                    mSubjectView.setText("Re: " + message.getSubject());
877                }
878                else {
879                    mSubjectView.setText(message.getSubject());
880                }
881                /*
882                 * If a reply-to was included with the message use that, otherwise use the from
883                 * or sender address.
884                 */
885                Address[] replyToAddresses;
886                if (message.getReplyTo().length > 0) {
887                    addAddresses(mToView, replyToAddresses = message.getReplyTo());
888                }
889                else {
890                    addAddresses(mToView, replyToAddresses = message.getFrom());
891                }
892                if (ACTION_REPLY_ALL.equals(action)) {
893                    for (Address address : message.getRecipients(RecipientType.TO)) {
894                        if (!address.getAddress().equalsIgnoreCase(mAccount.getEmail())) {
895                            addAddress(mToView, address);
896                        }
897                    }
898                    if (message.getRecipients(RecipientType.CC).length > 0) {
899                        for (Address address : message.getRecipients(RecipientType.CC)) {
900                            if (!Utility.arrayContains(replyToAddresses, address)) {
901                                addAddress(mCcView, address);
902                            }
903                        }
904                        mCcView.setVisibility(View.VISIBLE);
905                    }
906                }
907
908                Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain");
909                if (part == null) {
910                    part = MimeUtility.findFirstPartByMimeType(message, "text/html");
911                }
912                if (part != null) {
913                    String text = MimeUtility.getTextFromPart(part);
914                    if (text != null) {
915                        mQuotedTextBar.setVisibility(View.VISIBLE);
916                        mQuotedText.setVisibility(View.VISIBLE);
917                        mQuotedText.loadDataWithBaseURL("email://", text, part.getMimeType(),
918                                "utf-8", null);
919                    }
920                }
921            }
922            catch (MessagingException me) {
923                /*
924                 * This really should not happen at this point but if it does it's okay.
925                 * The user can continue composing their message.
926                 */
927            }
928        }
929        else if (ACTION_FORWARD.equals(action)) {
930            try {
931                if (message.getSubject() != null &&
932                        !message.getSubject().toLowerCase().startsWith("fwd:")) {
933                    mSubjectView.setText("Fwd: " + message.getSubject());
934                }
935                else {
936                    mSubjectView.setText(message.getSubject());
937                }
938
939                Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain");
940                if (part == null) {
941                    part = MimeUtility.findFirstPartByMimeType(message, "text/html");
942                }
943                if (part != null) {
944                    String text = MimeUtility.getTextFromPart(part);
945                    if (text != null) {
946                        mQuotedTextBar.setVisibility(View.VISIBLE);
947                        mQuotedText.setVisibility(View.VISIBLE);
948                        mQuotedText.loadDataWithBaseURL("email://", text, part.getMimeType(),
949                                "utf-8", null);
950                    }
951                }
952                if (!mSourceMessageProcessed) {
953                    if (!loadAttachments(message, 0)) {
954                        mHandler.sendEmptyMessage(MSG_SKIPPED_ATTACHMENTS);
955                    }
956                }
957            }
958            catch (MessagingException me) {
959                /*
960                 * This really should not happen at this point but if it does it's okay.
961                 * The user can continue composing their message.
962                 */
963            }
964        }
965        else if (ACTION_EDIT_DRAFT.equals(action)) {
966            try {
967                mSubjectView.setText(message.getSubject());
968                addAddresses(mToView, message.getRecipients(RecipientType.TO));
969                if (message.getRecipients(RecipientType.CC).length > 0) {
970                    addAddresses(mCcView, message.getRecipients(RecipientType.CC));
971                    mCcView.setVisibility(View.VISIBLE);
972                }
973                if (message.getRecipients(RecipientType.BCC).length > 0) {
974                    addAddresses(mBccView, message.getRecipients(RecipientType.BCC));
975                    mBccView.setVisibility(View.VISIBLE);
976                }
977                Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain");
978                if (part != null) {
979                    String text = MimeUtility.getTextFromPart(part);
980                    mMessageContentView.setText(text);
981                }
982                if (!mSourceMessageProcessed) {
983                    loadAttachments(message, 0);
984                }
985            }
986            catch (MessagingException me) {
987                // TODO
988            }
989        }
990        mSourceMessageProcessed = true;
991        mDraftNeedsSaving = false;
992    }
993
994    class Listener extends MessagingListener {
995        @Override
996        public void loadMessageForViewStarted(Account account, String folder, String uid) {
997            mHandler.sendEmptyMessage(MSG_PROGRESS_ON);
998        }
999
1000        @Override
1001        public void loadMessageForViewFinished(Account account, String folder, String uid,
1002                Message message) {
1003            mHandler.sendEmptyMessage(MSG_PROGRESS_OFF);
1004        }
1005
1006        @Override
1007        public void loadMessageForViewBodyAvailable(Account account, String folder, String uid,
1008                final Message message) {
1009            mSourceMessage = message;
1010            runOnUiThread(new Runnable() {
1011                public void run() {
1012                    processSourceMessage(message);
1013                }
1014            });
1015        }
1016
1017        @Override
1018        public void loadMessageForViewFailed(Account account, String folder, String uid,
1019                final String message) {
1020            mHandler.sendEmptyMessage(MSG_PROGRESS_OFF);
1021            // TODO show network error
1022        }
1023
1024        @Override
1025        public void messageUidChanged(
1026                Account account,
1027                String folder,
1028                String oldUid,
1029                String newUid) {
1030            if (account.equals(mAccount)
1031                    && (folder.equals(mFolder)
1032                            || (mFolder == null
1033                                    && folder.equals(mAccount.getDraftsFolderName())))) {
1034                if (oldUid.equals(mDraftUid)) {
1035                    mDraftUid = newUid;
1036                }
1037                if (oldUid.equals(mSourceMessageUid)) {
1038                    mSourceMessageUid = newUid;
1039                }
1040                if (mSourceMessage != null && (oldUid.equals(mSourceMessage.getUid()))) {
1041                    mSourceMessage.setUid(newUid);
1042                }
1043            }
1044        }
1045    }
1046}
1047