package com.fsck.k9.activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.provider.OpenableColumns; import android.text.TextWatcher; import android.text.util.Rfc822Tokenizer; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; import android.view.Window; import android.widget.AutoCompleteTextView.Validator; import android.widget.*; import com.fsck.k9.*; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.*; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.internet.*; import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBody; import java.io.File; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.StringTokenizer; import org.apache.james.mime4j.codec.EncoderUtil; public class MessageCompose extends K9Activity implements OnClickListener, OnFocusChangeListener { private static final int DIALOG_SAVE_OR_DISCARD_DRAFT_MESSAGE = 1; private static final String ACTION_REPLY = "com.fsck.k9.intent.action.REPLY"; private static final String ACTION_REPLY_ALL = "com.fsck.k9.intent.action.REPLY_ALL"; private static final String ACTION_FORWARD = "com.fsck.k9.intent.action.FORWARD"; private static final String ACTION_EDIT_DRAFT = "com.fsck.k9.intent.action.EDIT_DRAFT"; private static final String EXTRA_ACCOUNT = "account"; private static final String EXTRA_FOLDER = "folder"; private static final String EXTRA_MESSAGE = "message"; private static final String STATE_KEY_ATTACHMENTS = "com.fsck.k9.activity.MessageCompose.attachments"; private static final String STATE_KEY_CC_SHOWN = "com.fsck.k9.activity.MessageCompose.ccShown"; private static final String STATE_KEY_BCC_SHOWN = "com.fsck.k9.activity.MessageCompose.bccShown"; private static final String STATE_KEY_QUOTED_TEXT_SHOWN = "com.fsck.k9.activity.MessageCompose.quotedTextShown"; private static final String STATE_KEY_SOURCE_MESSAGE_PROCED = "com.fsck.k9.activity.MessageCompose.stateKeySourceMessageProced"; private static final String STATE_KEY_DRAFT_UID = "com.fsck.k9.activity.MessageCompose.draftUid"; private static final String STATE_IDENTITY_CHANGED = "com.fsck.k9.activity.MessageCompose.identityChanged"; private static final String STATE_IDENTITY = "com.fsck.k9.activity.MessageCompose.identity"; private static final String STATE_IN_REPLY_TO = "com.fsck.k9.activity.MessageCompose.inReplyTo"; private static final String STATE_REFERENCES = "com.fsck.k9.activity.MessageCompose.references"; private static final int MSG_PROGRESS_ON = 1; private static final int MSG_PROGRESS_OFF = 2; private static final int MSG_UPDATE_TITLE = 3; private static final int MSG_SKIPPED_ATTACHMENTS = 4; private static final int MSG_SAVED_DRAFT = 5; private static final int MSG_DISCARDED_DRAFT = 6; private static final int ACTIVITY_REQUEST_PICK_ATTACHMENT = 1; private static final int ACTIVITY_CHOOSE_IDENTITY = 2; private Account mAccount; private Identity mIdentity; private boolean mIdentityChanged = false; private boolean mSignatureChanged = false; private String mFolder; private String mSourceMessageUid; private Message mSourceMessage; /** * Indicates that the source message has been processed at least once and should not * be processed on any subsequent loads. This protects us from adding attachments that * have already been added from the restore of the view state. */ private boolean mSourceMessageProcessed = false; private TextView mFromView; private MultiAutoCompleteTextView mToView; private MultiAutoCompleteTextView mCcView; private MultiAutoCompleteTextView mBccView; private EditText mSubjectView; private EditText mSignatureView; private EditText mMessageContentView; private LinearLayout mAttachments; private View mQuotedTextBar; private ImageButton mQuotedTextDelete; private EditText mQuotedText; private String mReferences; private String mInReplyTo; private boolean mDraftNeedsSaving = false; /** * The draft uid of this message. This is used when saving drafts so that the same draft is * overwritten instead of being created anew. This property is null until the first save. */ private String mDraftUid; private Handler mHandler = new Handler() { @Override public void handleMessage(android.os.Message msg) { switch (msg.what) { case MSG_PROGRESS_ON: setProgressBarIndeterminateVisibility(true); break; case MSG_PROGRESS_OFF: setProgressBarIndeterminateVisibility(false); break; case MSG_UPDATE_TITLE: updateTitle(); break; case MSG_SKIPPED_ATTACHMENTS: Toast.makeText( MessageCompose.this, getString(R.string.message_compose_attachments_skipped_toast), Toast.LENGTH_LONG).show(); break; case MSG_SAVED_DRAFT: Toast.makeText( MessageCompose.this, getString(R.string.message_saved_toast), Toast.LENGTH_LONG).show(); break; case MSG_DISCARDED_DRAFT: Toast.makeText( MessageCompose.this, getString(R.string.message_discarded_toast), Toast.LENGTH_LONG).show(); break; default: super.handleMessage(msg); break; } } }; private Listener mListener = new Listener(); private EmailAddressAdapter mAddressAdapter; private Validator mAddressValidator; class Attachment implements Serializable { public String name; public String contentType; public long size; public Uri uri; } /** * Compose a new message using the given account. If account is null the default account * will be used. * @param context * @param account */ public static void actionCompose(Context context, Account account) { if (account == null) { account = Preferences.getPreferences(context).getDefaultAccount(); } Intent i = new Intent(context, MessageCompose.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); context.startActivity(i); } /** * Compose a new message as a reply to the given message. If replyAll is true the function * is reply all instead of simply reply. * @param context * @param account * @param message * @param replyAll */ public static void actionReply( Context context, Account account, Message message, boolean replyAll) { Intent i = new Intent(context, MessageCompose.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_FOLDER, message.getFolder().getName()); i.putExtra(EXTRA_MESSAGE, message.getUid()); if (replyAll) { i.setAction(ACTION_REPLY_ALL); } else { i.setAction(ACTION_REPLY); } context.startActivity(i); } /** * Compose a new message as a forward of the given message. * @param context * @param account * @param message */ public static void actionForward(Context context, Account account, Message message) { Intent i = new Intent(context, MessageCompose.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_FOLDER, message.getFolder().getName()); i.putExtra(EXTRA_MESSAGE, message.getUid()); i.setAction(ACTION_FORWARD); context.startActivity(i); } /** * Continue composition of the given message. This action modifies the way this Activity * handles certain actions. * Save will attempt to replace the message in the given folder with the updated version. * Discard will delete the message from the given folder. * @param context * @param account * @param folder * @param message */ public static void actionEditDraft(Context context, Account account, Message message) { Intent i = new Intent(context, MessageCompose.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_FOLDER, message.getFolder().getName()); i.putExtra(EXTRA_MESSAGE, message.getUid()); i.setAction(ACTION_EDIT_DRAFT); context.startActivity(i); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); setContentView(R.layout.message_compose); Intent intent = getIntent(); String accountUuid = intent.getStringExtra(EXTRA_ACCOUNT); mAccount = Preferences.getPreferences(this).getAccount(accountUuid); if (mAccount == null) { mAccount = Preferences.getPreferences(this).getDefaultAccount(); } if (mAccount == null) { /* * There are no accounts set up. This should not have happened. Prompt the * user to set up an account as an acceptable bailout. */ startActivity(new Intent(this, Accounts.class)); mDraftNeedsSaving = false; finish(); return; } mAddressAdapter = EmailAddressAdapter.getInstance(this); mAddressValidator = new EmailAddressValidator(); mFromView = (TextView)findViewById(R.id.from); mToView = (MultiAutoCompleteTextView)findViewById(R.id.to); mCcView = (MultiAutoCompleteTextView)findViewById(R.id.cc); mBccView = (MultiAutoCompleteTextView)findViewById(R.id.bcc); mSubjectView = (EditText)findViewById(R.id.subject); EditText upperSignature = (EditText)findViewById(R.id.upper_signature); EditText lowerSignature = (EditText)findViewById(R.id.lower_signature); mMessageContentView = (EditText)findViewById(R.id.message_content); mAttachments = (LinearLayout)findViewById(R.id.attachments); mQuotedTextBar = findViewById(R.id.quoted_text_bar); mQuotedTextDelete = (ImageButton)findViewById(R.id.quoted_text_delete); mQuotedText = (EditText)findViewById(R.id.quoted_text); TextWatcher watcher = new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int before, int after) { } public void onTextChanged(CharSequence s, int start, int before, int count) { mDraftNeedsSaving = true; } public void afterTextChanged(android.text.Editable s) { } }; TextWatcher sigwatcher = new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int before, int after) { } public void onTextChanged(CharSequence s, int start, int before, int count) { mDraftNeedsSaving = true; mSignatureChanged = true; } public void afterTextChanged(android.text.Editable s) { } }; mToView.addTextChangedListener(watcher); mCcView.addTextChangedListener(watcher); mBccView.addTextChangedListener(watcher); mSubjectView.addTextChangedListener(watcher); mMessageContentView.addTextChangedListener(watcher); mQuotedText.addTextChangedListener(watcher); /* * We set this to invisible by default. Other methods will turn it back on if it's * needed. */ mQuotedTextBar.setVisibility(View.GONE); mQuotedText.setVisibility(View.GONE); mQuotedTextDelete.setOnClickListener(this); mFromView.setVisibility(View.GONE); mToView.setAdapter(mAddressAdapter); mToView.setTokenizer(new Rfc822Tokenizer()); mToView.setValidator(mAddressValidator); mCcView.setAdapter(mAddressAdapter); mCcView.setTokenizer(new Rfc822Tokenizer()); mCcView.setValidator(mAddressValidator); mBccView.setAdapter(mAddressAdapter); mBccView.setTokenizer(new Rfc822Tokenizer()); mBccView.setValidator(mAddressValidator); mSubjectView.setOnFocusChangeListener(this); if (savedInstanceState != null) { /* * This data gets used in onCreate, so grab it here instead of onRestoreIntstanceState */ mSourceMessageProcessed = savedInstanceState.getBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, false); } String action = intent.getAction(); if (Intent.ACTION_VIEW.equals(action) || Intent.ACTION_SENDTO.equals(action)) { /* * Someone has clicked a mailto: link. The address is in the URI. */ if (intent.getData() != null) { Uri uri = intent.getData(); if ("mailto".equals(uri.getScheme())) { initializeFromMailTo(uri.toString()); } else { String toText = uri.getSchemeSpecificPart(); if (toText != null) { mToView.setText(toText); } } } } //TODO: Use constant Intent.ACTION_SEND_MULTIPLE once we drop Android 1.5 support else if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SENDTO.equals(action) || "android.intent.action.SEND_MULTIPLE".equals(action)) { /* * Someone is trying to compose an email with an attachment, probably Pictures. * The Intent should contain an EXTRA_STREAM with the data to attach. */ String text = intent.getStringExtra(Intent.EXTRA_TEXT); if (text != null) { mMessageContentView.setText(text); } String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT); if (subject != null) { mSubjectView.setText(subject); } String type = intent.getType(); //TODO: Use constant Intent.ACTION_SEND_MULTIPLE once we drop Android 1.5 support if ("android.intent.action.SEND_MULTIPLE".equals(action)) { ArrayList list = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); if (list != null) { for (Parcelable parcelable : list) { Uri stream = (Uri) parcelable; if (stream != null && type != null) { if (MimeUtility.mimeTypeMatches(type, K9.ACCEPTABLE_ATTACHMENT_SEND_TYPES)) { addAttachment(stream); } } } } } else { Uri stream = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); if (stream != null && type != null) { if (MimeUtility.mimeTypeMatches(type, K9.ACCEPTABLE_ATTACHMENT_SEND_TYPES)) { addAttachment(stream); } } } /* * There might be an EXTRA_SUBJECT, EXTRA_TEXT, EXTRA_EMAIL, EXTRA_BCC or EXTRA_CC */ String extraSubject = intent.getStringExtra(Intent.EXTRA_SUBJECT); String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); mSubjectView.setText(extraSubject); mMessageContentView.setText(extraText); String[] extraEmail = intent.getStringArrayExtra(Intent.EXTRA_EMAIL); String[] extraCc = intent.getStringArrayExtra(Intent.EXTRA_CC); String[] extraBcc = intent.getStringArrayExtra(Intent.EXTRA_BCC); String addressList; // Cache array size, as per Google's recommendations. int arraySize; int i; addressList = ""; if (extraEmail != null) { arraySize = extraEmail.length; for (i=0; i < arraySize; i++) { addressList += extraEmail[i]+", "; } } mToView.setText(addressList); addressList = ""; if (extraCc != null) { arraySize = extraCc.length; for (i=0; i < arraySize; i++) { addressList += extraCc[i]+", "; } } mCcView.setText(addressList); addressList = ""; if (extraBcc != null) { arraySize = extraBcc.length; for (i=0; i < arraySize; i++) { addressList += extraBcc[i]+", "; } } mBccView.setText(addressList); } else { mFolder = (String) intent.getStringExtra(EXTRA_FOLDER); mSourceMessageUid = (String) intent.getStringExtra(EXTRA_MESSAGE); } if (mIdentity == null) { mIdentity = mAccount.getIdentity(0); } if (mAccount.isSignatureBeforeQuotedText()) { mSignatureView = upperSignature; lowerSignature.setVisibility(View.GONE); } else { mSignatureView = lowerSignature; upperSignature.setVisibility(View.GONE); } mSignatureView.addTextChangedListener(sigwatcher); if (!mIdentity.getSignatureUse()) { mSignatureView.setVisibility(View.GONE); } if (!mSourceMessageProcessed) { updateFrom(); updateSignature(); if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action) || ACTION_FORWARD.equals(action) || ACTION_EDIT_DRAFT.equals(action)) { /* * If we need to load the message we add ourself as a message listener here * so we can kick it off. Normally we add in onResume but we don't * want to reload the message every time the activity is resumed. * There is no harm in adding twice. */ MessagingController.getInstance(getApplication()).addListener(mListener); MessagingController.getInstance(getApplication()).loadMessageForView(mAccount, mFolder, mSourceMessageUid, null); } if (!ACTION_EDIT_DRAFT.equals(action)) { String bccAddress = mAccount.getAlwaysBcc(); if (bccAddress!=null && !"".equals(bccAddress)) { addAddress(mBccView, new Address(mAccount.getAlwaysBcc(), "")); } } if (K9.DEBUG) Log.d(K9.LOG_TAG, "action = " + action + ", mAccount = " + mAccount + ", mFolder = " + mFolder + ", mSourceMessageUid = " + mSourceMessageUid); if ((ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action)) && mAccount != null && mFolder != null && mSourceMessageUid != null) { if (K9.DEBUG) Log.d(K9.LOG_TAG, "Setting message ANSWERED flag to true"); // TODO: Really, we should wait until we send the message, but that would require saving the original // message info along with a Draft copy, in case it is left in Drafts for a while before being sent MessagingController.getInstance(getApplication()).setFlag(mAccount, mFolder, new String[] { mSourceMessageUid }, Flag.ANSWERED, true); } updateTitle(); } if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action) || ACTION_EDIT_DRAFT.equals(action)) { //change focus to message body. mMessageContentView.requestFocus(); } mDraftNeedsSaving = false; } @Override public void onResume() { super.onResume(); MessagingController.getInstance(getApplication()).addListener(mListener); } @Override public void onPause() { super.onPause(); MessagingController.getInstance(getApplication()).removeListener(mListener); } /** * The framework handles most of the fields, but we need to handle stuff that we * dynamically show and hide: * Attachment list, * Cc field, * Bcc field, * Quoted text, */ @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); ArrayList attachments = new ArrayList(); for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { View view = mAttachments.getChildAt(i); Attachment attachment = (Attachment) view.getTag(); attachments.add(attachment.uri); } outState.putParcelableArrayList(STATE_KEY_ATTACHMENTS, attachments); outState.putBoolean(STATE_KEY_CC_SHOWN, mCcView.getVisibility() == View.VISIBLE); outState.putBoolean(STATE_KEY_BCC_SHOWN, mBccView.getVisibility() == View.VISIBLE); outState.putBoolean(STATE_KEY_QUOTED_TEXT_SHOWN, mQuotedTextBar.getVisibility() == View.VISIBLE); outState.putBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, mSourceMessageProcessed); outState.putString(STATE_KEY_DRAFT_UID, mDraftUid); outState.putSerializable(STATE_IDENTITY, mIdentity); outState.putBoolean(STATE_IDENTITY_CHANGED, mIdentityChanged); outState.putString(STATE_IN_REPLY_TO, mInReplyTo); outState.putString(STATE_REFERENCES, mReferences); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); ArrayList attachments = (ArrayList) savedInstanceState.getParcelableArrayList(STATE_KEY_ATTACHMENTS); mAttachments.removeAllViews(); for (Parcelable p : attachments) { Uri uri = (Uri) p; addAttachment(uri); } mCcView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_CC_SHOWN) ? View.VISIBLE : View.GONE); mBccView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_BCC_SHOWN) ? View.VISIBLE : View.GONE); mQuotedTextBar.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ? View.VISIBLE : View.GONE); mQuotedText.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ? View.VISIBLE : View.GONE); mDraftUid = savedInstanceState.getString(STATE_KEY_DRAFT_UID); mIdentity = (Identity)savedInstanceState.getSerializable(STATE_IDENTITY); mIdentityChanged = savedInstanceState.getBoolean(STATE_IDENTITY_CHANGED); mInReplyTo = savedInstanceState.getString(STATE_IN_REPLY_TO); mReferences = savedInstanceState.getString(STATE_REFERENCES); updateFrom(); updateSignature(); mDraftNeedsSaving = false; } private void updateTitle() { if (mSubjectView.getText().length() == 0) { setTitle(R.string.compose_title); } else { setTitle(mSubjectView.getText().toString()); } } public void onFocusChange(View view, boolean focused) { if (!focused) { updateTitle(); } } private void addAddresses(MultiAutoCompleteTextView view, Address[] addresses) { if (addresses == null) { return; } for (Address address : addresses) { addAddress(view, address); } } private void addAddress(MultiAutoCompleteTextView view, Address address) { view.append(address + ", "); } private Address[] getAddresses(MultiAutoCompleteTextView view) { Address[] addresses = Address.parseUnencoded(view.getText().toString().trim()); return addresses; } private MimeMessage createMessage(boolean appendSig) throws MessagingException { MimeMessage message = new MimeMessage(); message.addSentDate(new Date()); Address from = new Address(mIdentity.getEmail(), mIdentity.getName()); message.setFrom(from); message.setRecipients(RecipientType.TO, getAddresses(mToView)); message.setRecipients(RecipientType.CC, getAddresses(mCcView)); message.setRecipients(RecipientType.BCC, getAddresses(mBccView)); message.setSubject(mSubjectView.getText().toString()); message.setHeader("X-User-Agent", getString(R.string.message_header_mua)); final String replyTo = mIdentity.getReplyTo(); if (replyTo != null) { message.setReplyTo(new Address[] { new Address(replyTo) }); } if (mInReplyTo != null) { message.setInReplyTo(mInReplyTo); } if (mReferences != null) { message.setReferences(mReferences); } /* * Build the Body that will contain the text of the message. We'll decide where to * include it later. */ String text = mMessageContentView.getText().toString(); if (appendSig && mAccount.isSignatureBeforeQuotedText()) { text = appendSignature(text); } if (mQuotedTextBar.getVisibility() == View.VISIBLE) { text += "\n" + mQuotedText.getText().toString(); } if (appendSig && mAccount.isSignatureBeforeQuotedText() == false) { text = appendSignature(text); } TextBody body = new TextBody(text); if (mAttachments.getChildCount() > 0) { /* * The message has attachments that need to be included. First we add the part * containing the text that will be sent and then we include each attachment. */ MimeMultipart mp; mp = new MimeMultipart(); mp.addBodyPart(new MimeBodyPart(body, "text/plain")); for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag(); MimeBodyPart bp = new MimeBodyPart( new LocalStore.LocalAttachmentBody(attachment.uri, getApplication())); /* * Correctly encode the filename here. Otherwise the whole * header value (all parameters at once) will be encoded by * MimeHeader.writeTo(). */ bp.addHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n name=\"%s\"", attachment.contentType, EncoderUtil.encodeIfNecessary(attachment.name, EncoderUtil.Usage.WORD_ENTITY, 7))); bp.addHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); /* * TODO: Oh the joys of MIME... * * From RFC 2183 (The Content-Disposition Header Field): * "Parameter values longer than 78 characters, or which * contain non-ASCII characters, MUST be encoded as specified * in [RFC 2184]." * * Example: * * Content-Type: application/x-stuff * title*1*=us-ascii'en'This%20is%20even%20more%20 * title*2*=%2A%2A%2Afun%2A%2A%2A%20 * title*3="isn't it!" */ bp.addHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, String.format( "attachment;\n filename=\"%s\";\n size=%d", attachment.name, attachment.size)); mp.addBodyPart(bp); } message.setBody(mp); } else { /* * No attachments to include, just stick the text body in the message and call * it good. */ message.setBody(body); } return message; } private String appendSignature(String text) { if (mIdentity.getSignatureUse()) { String signature = mSignatureView.getText().toString(); if (signature != null && !signature.contentEquals("")) { text += "\n" + signature; } } return text; } private void sendOrSaveMessage(boolean save) { /* * Create the message from all the data the user has entered. */ MimeMessage message; try { message = createMessage(!save); // Only append sig on save } catch (MessagingException me) { Log.e(K9.LOG_TAG, "Failed to create new message for send or save.", me); throw new RuntimeException("Failed to create a new message for send or save.", me); } if (save) { /* * Save a draft */ if (mDraftUid != null) { message.setUid(mDraftUid); } else if (ACTION_EDIT_DRAFT.equals(getIntent().getAction())) { /* * We're saving a previously saved draft, so update the new message's uid * to the old message's uid. */ message.setUid(mSourceMessageUid); } String k9identity = Utility.base64Encode("" + mMessageContentView.getText().toString().length()); if (mIdentityChanged || mSignatureChanged) { String signature = mSignatureView.getText().toString(); k9identity += ":" + Utility.base64Encode(signature); if (mIdentityChanged) { String name = mIdentity.getName(); String email = mIdentity.getEmail(); k9identity += ":" + Utility.base64Encode(name) + ":" + Utility.base64Encode(email); } } if (K9.DEBUG) Log.d(K9.LOG_TAG, "Saving identity: " + k9identity); message.addHeader(K9.K9MAIL_IDENTITY, k9identity); Message draftMessage = MessagingController.getInstance(getApplication()).saveDraft(mAccount, message); mDraftUid = draftMessage.getUid(); // Don't display the toast if the user is just changing the orientation if ((getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) { mHandler.sendEmptyMessage(MSG_SAVED_DRAFT); } } else { MessagingController.getInstance(getApplication()).sendMessage(mAccount, message, null); if (mDraftUid != null) { MessagingController.getInstance(getApplication()).deleteDraft(mAccount, mDraftUid); mDraftUid = null; } } } private void saveIfNeeded() { if (!mDraftNeedsSaving) { return; } mDraftNeedsSaving = false; sendOrSaveMessage(true); } private void onSend() { if (getAddresses(mToView).length == 0 && getAddresses(mCcView).length == 0 && getAddresses(mBccView).length == 0) { mToView.setError(getString(R.string.message_compose_error_no_recipients)); Toast.makeText(this, getString(R.string.message_compose_error_no_recipients), Toast.LENGTH_LONG).show(); return; } sendOrSaveMessage(false); mDraftNeedsSaving = false; finish(); } private void onDiscard() { if (mDraftUid != null) { MessagingController.getInstance(getApplication()).deleteDraft(mAccount, mDraftUid); mDraftUid = null; } mHandler.sendEmptyMessage(MSG_DISCARDED_DRAFT); mDraftNeedsSaving = false; finish(); } private void onSave() { mDraftNeedsSaving = true; saveIfNeeded(); finish(); } private void onAddCcBcc() { mCcView.setVisibility(View.VISIBLE); mBccView.setVisibility(View.VISIBLE); } /** * Kick off a picker for whatever kind of MIME types we'll accept and let Android take over. */ private void onAddAttachment() { if (K9.isGalleryBuggy()) { if (K9.useGalleryBugWorkaround()) { Toast.makeText(MessageCompose.this, getString(R.string.message_compose_use_workaround), Toast.LENGTH_LONG).show(); } else { Toast.makeText(MessageCompose.this, getString(R.string.message_compose_buggy_gallery), Toast.LENGTH_LONG).show(); } } onAddAttachment2(K9.ACCEPTABLE_ATTACHMENT_SEND_TYPES[0]); } /** * Kick off a picker for the specified MIME type and let Android take over. */ private void onAddAttachment2(final String mime_type) { Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); i.setType(mime_type); startActivityForResult(Intent.createChooser(i, null), ACTIVITY_REQUEST_PICK_ATTACHMENT); } private void addAttachment(Uri uri) { addAttachment(uri, -1, null); } private void addAttachment(Uri uri, int size, String name) { ContentResolver contentResolver = getContentResolver(); Attachment attachment = new Attachment(); attachment.name = name; attachment.size = size; attachment.uri = uri; if (attachment.size == -1 || attachment.name == null) { Cursor metadataCursor = contentResolver.query(uri, new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }, null, null, null); if (metadataCursor != null) { try { if (metadataCursor.moveToFirst()) { if (attachment.name == null) { attachment.name = metadataCursor.getString(0); } if (attachment.size == -1) { attachment.size = metadataCursor.getInt(1); Log.v(K9.LOG_TAG, "size: " + attachment.size); } } } finally { metadataCursor.close(); } } } if (attachment.name == null) { attachment.name = uri.getLastPathSegment(); } String contentType = contentResolver.getType(uri); if (contentType == null) { contentType = MimeUtility.getMimeTypeByExtension(attachment.name); } attachment.contentType = contentType; if (attachment.size<=0) { String uriString = uri.toString(); if (uriString.startsWith("file://")) { Log.v(K9.LOG_TAG, uriString.substring("file://".length())); File f = new File(uriString.substring("file://".length())); attachment.size = f.length(); } else { Log.v(K9.LOG_TAG, "Not a file: " + uriString); } } else { Log.v(K9.LOG_TAG, "old attachment.size: " + attachment.size); } Log.v(K9.LOG_TAG, "new attachment.size: " + attachment.size); View view = getLayoutInflater().inflate(R.layout.message_compose_attachment, mAttachments, false); TextView nameView = (TextView)view.findViewById(R.id.attachment_name); ImageButton delete = (ImageButton)view.findViewById(R.id.attachment_delete); nameView.setText(attachment.name); delete.setOnClickListener(this); delete.setTag(view); view.setTag(attachment); mAttachments.addView(view); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode != RESULT_OK) return; if (data == null) { return; } switch (requestCode) { case ACTIVITY_REQUEST_PICK_ATTACHMENT: addAttachment(data.getData()); mDraftNeedsSaving = true; break; case ACTIVITY_CHOOSE_IDENTITY: onIdentityChosen(data); break; } } private void onIdentityChosen(Intent intent) { Bundle bundle = intent.getExtras();; switchToIdentity((Identity)bundle.getSerializable(ChooseIdentity.EXTRA_IDENTITY)); } private void switchToIdentity(Identity identity) { mIdentity = identity; mIdentityChanged = true; mDraftNeedsSaving = true; updateFrom(); updateSignature(); } private void updateFrom() { if (mIdentityChanged) { mFromView.setVisibility(View.VISIBLE); } mFromView.setText(getString(R.string.message_view_from_format, mIdentity.getName(), mIdentity.getEmail())); } private void updateSignature() { if (mIdentity.getSignatureUse()) { mSignatureView.setText(mIdentity.getSignature()); mSignatureView.setVisibility(View.VISIBLE); } else { mSignatureView.setVisibility(View.GONE); } } public void onClick(View view) { switch (view.getId()) { case R.id.attachment_delete: /* * The view is the delete button, and we have previously set the tag of * the delete button to the view that owns it. We don't use parent because the * view is very complex and could change in the future. */ mAttachments.removeView((View) view.getTag()); mDraftNeedsSaving = true; break; case R.id.quoted_text_delete: mQuotedTextBar.setVisibility(View.GONE); mQuotedText.setVisibility(View.GONE); mDraftNeedsSaving = true; break; } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.send: onSend(); break; case R.id.save: onSave(); break; case R.id.discard: onDiscard(); break; case R.id.add_cc_bcc: onAddCcBcc(); break; case R.id.add_attachment: onAddAttachment(); break; case R.id.add_attachment_image: onAddAttachment2("image/*"); break; case R.id.add_attachment_video: onAddAttachment2("video/*"); break; case R.id.choose_identity: onChooseIdentity(); break; default: return super.onOptionsItemSelected(item); } return true; } private void onChooseIdentity() { if (mAccount.getIdentities().size() > 1) { Intent intent = new Intent(this, ChooseIdentity.class); intent.putExtra(ChooseIdentity.EXTRA_ACCOUNT, mAccount.getUuid()); startActivityForResult(intent, ACTIVITY_CHOOSE_IDENTITY); } else { Toast.makeText(this, getString(R.string.no_identities), Toast.LENGTH_LONG).show(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.message_compose_option, menu); /* * Show the menu items "Add attachment (Image)" and "Add attachment (Video)" * if the work-around for the Gallery bug is enabled (see Issue 1186). */ int found = 0; for (int i = menu.size() - 1; i >= 0; i--) { MenuItem item = menu.getItem(i); int id = item.getItemId(); if ((id == R.id.add_attachment_image) || (id == R.id.add_attachment_video)) { item.setVisible(K9.useGalleryBugWorkaround()); found++; } // We found all the menu items we were looking for. So stop here. if (found == 2) break; } return true; } public void onBackPressed() { // This will be called either automatically for you on 2.0 // or later, or by the code above on earlier versions of the // platform. showDialog(DIALOG_SAVE_OR_DISCARD_DRAFT_MESSAGE); } @Override public Dialog onCreateDialog(int id) { switch (id) { case DIALOG_SAVE_OR_DISCARD_DRAFT_MESSAGE: return new AlertDialog.Builder(this) .setTitle(R.string.save_or_discard_draft_message_dlg_title) .setMessage(R.string.save_or_discard_draft_message_instructions_fmt) .setPositiveButton(R.string.save_draft_action, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { dismissDialog(1); onSave(); } }) .setNegativeButton(R.string.discard_action, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { dismissDialog(1); onDiscard(); } }) .create(); } return super.onCreateDialog(id); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if ( // TODO - when we move to android 2.0, uncomment this. // android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.ECLAIR && keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0 && K9.manageBack()) { // Take care of calling this method on earlier versions of // the platform where it doesn't exist. onBackPressed(); return true; } return super.onKeyDown(keyCode, event); } /** * Returns true if all attachments were able to be attached, otherwise returns false. */ private boolean loadAttachments(Part part, int depth) throws MessagingException { if (part.getBody() instanceof Multipart) { Multipart mp = (Multipart) part.getBody(); boolean ret = true; for (int i = 0, count = mp.getCount(); i < count; i++) { if (!loadAttachments(mp.getBodyPart(i), depth + 1)) { ret = false; } } return ret; } else { String contentType = MimeUtility.unfoldAndDecode(part.getContentType()); String name = MimeUtility.getHeaderParameter(contentType, "name"); if (name != null) { Body body = part.getBody(); if (body != null && body instanceof LocalAttachmentBody) { final Uri uri = ((LocalAttachmentBody) body).getContentUri(); mHandler.post(new Runnable() { public void run() { addAttachment(uri); } }); } else { return false; } } return true; } } /** * Pull out the parts of the now loaded source message and apply them to the new message * depending on the type of message being composed. * @param message */ private void processSourceMessage(Message message) { String action = getIntent().getAction(); if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action)) { try { if (message.getSubject() != null && !message.getSubject().toLowerCase().startsWith("re:")) { mSubjectView.setText("Re: " + message.getSubject()); } else { mSubjectView.setText(message.getSubject()); } /* * If a reply-to was included with the message use that, otherwise use the from * or sender address. */ Address[] replyToAddresses; if (message.getReplyTo().length > 0) { addAddresses(mToView, replyToAddresses = message.getReplyTo()); } else { addAddresses(mToView, replyToAddresses = message.getFrom()); } if (message.getMessageId() != null && message.getMessageId().length() > 0) { String messageId = message.getMessageId(); mInReplyTo = messageId; if (message.getReferences() != null && message.getReferences().length > 0) { StringBuffer buffy = new StringBuffer(); for (int i=0; i < message.getReferences().length; i++) buffy.append(message.getReferences()[i]); mReferences = buffy.toString() + " " + mInReplyTo; } else { mReferences = mInReplyTo; } } else { if (K9.DEBUG) Log.d(K9.LOG_TAG, "could not get Message-ID."); } Part part = MimeUtility.findFirstPartByMimeType(mSourceMessage, "text/plain"); if (part != null) { String quotedText = String.format( getString(R.string.message_compose_reply_header_fmt), Address.toString(mSourceMessage.getFrom())); final String prefix = mAccount.getQuotePrefix(); // "$" and "\" in the quote prefix have to be escaped for // the replaceAll() invocation. final String escapedPrefix = prefix.replaceAll("(\\\\|\\$)", "\\\\$1"); quotedText += MimeUtility.getTextFromPart(part).replaceAll( "(?m)^", escapedPrefix); quotedText = quotedText.replaceAll("\\\r", ""); mQuotedText.setText(quotedText); mQuotedTextBar.setVisibility(View.VISIBLE); mQuotedText.setVisibility(View.VISIBLE); } if (ACTION_REPLY_ALL.equals(action) || ACTION_REPLY.equals(action)) { Identity useIdentity = null; for (Address address : message.getRecipients(RecipientType.TO)) { Identity identity = mAccount.findIdentity(address); if (identity != null) { useIdentity = identity; break; } } if (useIdentity == null) { if (message.getRecipients(RecipientType.CC).length > 0) { for (Address address : message.getRecipients(RecipientType.CC)) { Identity identity = mAccount.findIdentity(address); if (identity != null) { useIdentity = identity; break; } } } } if (useIdentity != null) { Identity defaultIdentity = mAccount.getIdentity(0); if (useIdentity != defaultIdentity) { switchToIdentity(useIdentity); } } } if (ACTION_REPLY_ALL.equals(action)) { for (Address address : message.getRecipients(RecipientType.TO)) { if (!mAccount.isAnIdentity(address)) { addAddress(mToView, address); } } if (message.getRecipients(RecipientType.CC).length > 0) { for (Address address : message.getRecipients(RecipientType.CC)) { if (!mAccount.isAnIdentity(address) && !Utility.arrayContains(replyToAddresses, address)) { addAddress(mCcView, address); } } mCcView.setVisibility(View.VISIBLE); } } } catch (MessagingException me) { /* * This really should not happen at this point but if it does it's okay. * The user can continue composing their message. */ } } else if (ACTION_FORWARD.equals(action)) { try { if (message.getSubject() != null && !message.getSubject().toLowerCase().startsWith("fwd:")) { mSubjectView.setText("Fwd: " + message.getSubject()); } else { mSubjectView.setText(message.getSubject()); } Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); if (part == null) { part = MimeUtility.findFirstPartByMimeType(message, "text/html"); } if (part != null) { String quotedText = MimeUtility.getTextFromPart(part); if (quotedText != null) { String text = String.format( getString(R.string.message_compose_fwd_header_fmt), mSourceMessage.getSubject(), Address.toString(mSourceMessage.getFrom()), Address.toString( mSourceMessage.getRecipients(RecipientType.TO)), Address.toString( mSourceMessage.getRecipients(RecipientType.CC))); quotedText = quotedText.replaceAll("\\\r", ""); text += quotedText; mQuotedText.setText(text); mQuotedTextBar.setVisibility(View.VISIBLE); mQuotedText.setVisibility(View.VISIBLE); } } if (!mSourceMessageProcessed) { if (!loadAttachments(message, 0)) { mHandler.sendEmptyMessage(MSG_SKIPPED_ATTACHMENTS); } } } catch (MessagingException me) { /* * This really should not happen at this point but if it does it's okay. * The user can continue composing their message. */ } } else if (ACTION_EDIT_DRAFT.equals(action)) { try { mDraftUid = message.getUid(); mSubjectView.setText(message.getSubject()); addAddresses(mToView, message.getRecipients(RecipientType.TO)); if (message.getRecipients(RecipientType.CC).length > 0) { addAddresses(mCcView, message.getRecipients(RecipientType.CC)); mCcView.setVisibility(View.VISIBLE); } if (message.getRecipients(RecipientType.BCC).length > 0) { addAddresses(mBccView, message.getRecipients(RecipientType.BCC)); mBccView.setVisibility(View.VISIBLE); } // Read In-Reply-To header from draft final String[] inReplyTo = message.getHeader("In-Reply-To"); if ((inReplyTo != null) && (inReplyTo.length >= 1)) { mInReplyTo = inReplyTo[0]; } // Read References header from draft final String[] references = message.getHeader("References"); if ((references != null) && (references.length >= 1)) { mReferences = references[0]; } if (!mSourceMessageProcessed) { loadAttachments(message, 0); } Integer bodyLength = null; String[] k9identities = message.getHeader(K9.K9MAIL_IDENTITY); if (k9identities != null && k9identities.length > 0) { String k9identity = k9identities[0]; if (k9identity != null) { if (K9.DEBUG) Log.d(K9.LOG_TAG, "Got a saved identity: " + k9identity); StringTokenizer tokens = new StringTokenizer(k9identity, ":", false); String bodyLengthS = null; String name = null; String email = null; String signature = null; boolean signatureUse = message.getFolder().getAccount().getSignatureUse(); if (tokens.hasMoreTokens()) { bodyLengthS = Utility.base64Decode(tokens.nextToken()); try { bodyLength = Integer.parseInt(bodyLengthS); } catch (Exception e) { Log.e(K9.LOG_TAG, "Unable to parse bodyLength '" + bodyLengthS + "'"); } } if (tokens.hasMoreTokens()) { signatureUse = true; signature = Utility.base64Decode(tokens.nextToken()); } if (tokens.hasMoreTokens()) { name = Utility.base64Decode(tokens.nextToken()); } if (tokens.hasMoreTokens()) { email = Utility.base64Decode(tokens.nextToken()); } Identity newIdentity = new Identity(); newIdentity.setSignatureUse(signatureUse); if (signature != null) { newIdentity.setSignature(signature); mSignatureChanged = true; } else { newIdentity.setSignature(mIdentity.getSignature()); } if (name != null) { newIdentity.setName(name); mIdentityChanged = true; } else { newIdentity.setName(mIdentity.getName()); } if (email != null) { newIdentity.setEmail(email); mIdentityChanged = true; } else { newIdentity.setEmail(mIdentity.getEmail()); } mIdentity = newIdentity; updateSignature(); updateFrom(); } } Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); if (part != null) { String text = MimeUtility.getTextFromPart(part); if (bodyLength != null && bodyLength + 1 < text.length()) // + 1 to get rid of the newline we added when saving the draft { String bodyText = text.substring(0, bodyLength); String quotedText = text.substring(bodyLength + 1, text.length()); mMessageContentView.setText(bodyText); mQuotedText.setText(quotedText); mQuotedTextBar.setVisibility(View.VISIBLE); mQuotedText.setVisibility(View.VISIBLE); } else { mMessageContentView.setText(text); } } } catch (MessagingException me) { // TODO } } mSourceMessageProcessed = true; mDraftNeedsSaving = false; } class Listener extends MessagingListener { @Override public void loadMessageForViewStarted(Account account, String folder, String uid) { if (mSourceMessageUid==null || !mSourceMessageUid.equals(uid)) { return; } mHandler.sendEmptyMessage(MSG_PROGRESS_ON); } @Override public void loadMessageForViewFinished(Account account, String folder, String uid, Message message) { if (mSourceMessageUid==null || !mSourceMessageUid.equals(uid)) { return; } mHandler.sendEmptyMessage(MSG_PROGRESS_OFF); } @Override public void loadMessageForViewBodyAvailable(Account account, String folder, String uid, final Message message) { if (mSourceMessageUid==null || !mSourceMessageUid.equals(uid)) { return; } mSourceMessage = message; runOnUiThread(new Runnable() { public void run() { processSourceMessage(message); } }); } @Override public void loadMessageForViewFailed(Account account, String folder, String uid, Throwable t) { if (mSourceMessageUid==null || !mSourceMessageUid.equals(uid)) { return; } mHandler.sendEmptyMessage(MSG_PROGRESS_OFF); // TODO show network error } @Override public void messageUidChanged(Account account, String folder, String oldUid, String newUid) { if (account.equals(mAccount) && folder.equals(mAccount.getDraftsFolderName())) { if (oldUid.equals(mDraftUid)) { mDraftUid = newUid; } } if (account.equals(mAccount) && (folder.equals(mFolder))) { if (oldUid.equals(mSourceMessageUid)) { mSourceMessageUid = newUid; } if (mSourceMessage != null && (oldUid.equals(mSourceMessage.getUid()))) { mSourceMessage.setUid(newUid); } } } } private String decode(String s) throws UnsupportedEncodingException { return URLDecoder.decode(s, "UTF-8"); } /** * When we are launched with an intent that includes a mailto: URI, we can actually * gather quite a few of our message fields from it. * * @mailToString the href (which must start with "mailto:"). */ private void initializeFromMailTo(String mailToString) { // Chop up everything between mailto: and ? to find recipients int index = mailToString.indexOf("?"); int length = "mailto".length() + 1; String to; try { // Extract the recipient after mailto: if (index == -1) { to = decode(mailToString.substring(length)); } else { to = decode(mailToString.substring(length, index)); } mToView.setText(to); } catch (UnsupportedEncodingException e) { Log.e(K9.LOG_TAG, e.getMessage() + " while decoding '" + mailToString + "'"); } // Extract the other parameters // We need to disguise this string as a URI in order to parse it Uri uri = Uri.parse("foo://" + mailToString); String addressList; addressList = ""; List cc = uri.getQueryParameters("cc"); for (String address : cc) { addressList += address + ","; } mCcView.setText(addressList); addressList = ""; List bcc = uri.getQueryParameters("bcc"); for (String address : bcc) { addressList += address + ","; } mBccView.setText(addressList); List subject = uri.getQueryParameters("subject"); if (subject.size() > 0) { mSubjectView.setText(subject.get(0)); } List body = uri.getQueryParameters("body"); if (body.size() > 0) { mMessageContentView.setText(body.get(0)); } } }