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