package com.fsck.k9.activity; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; import android.annotation.SuppressLint; import android.app.ActionBar; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.Dialog; import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import android.content.pm.ActivityInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.text.TextUtils; import android.text.TextWatcher; import timber.log.Timber; import android.util.TypedValue; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; 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.EditText; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.fsck.k9.Account; import com.fsck.k9.Account.MessageFormat; import com.fsck.k9.Identity; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.R; import com.fsck.k9.activity.MessageLoaderHelper.MessageLoaderCallbacks; import com.fsck.k9.activity.compose.AttachmentPresenter; import com.fsck.k9.activity.compose.AttachmentPresenter.AttachmentMvpView; import com.fsck.k9.activity.compose.AttachmentPresenter.WaitingAction; import com.fsck.k9.activity.compose.ComposeCryptoStatus; import com.fsck.k9.activity.compose.ComposeCryptoStatus.SendErrorState; import com.fsck.k9.activity.compose.CryptoSettingsDialog.OnCryptoModeChangedListener; import com.fsck.k9.activity.compose.IdentityAdapter; import com.fsck.k9.activity.compose.IdentityAdapter.IdentityContainer; import com.fsck.k9.activity.compose.PgpInlineDialog.OnOpenPgpInlineChangeListener; import com.fsck.k9.activity.compose.PgpSignOnlyDialog.OnOpenPgpSignOnlyChangeListener; import com.fsck.k9.activity.compose.RecipientMvpView; import com.fsck.k9.activity.compose.RecipientPresenter; import com.fsck.k9.activity.compose.RecipientPresenter.CryptoMode; import com.fsck.k9.activity.compose.SaveMessageTask; import com.fsck.k9.activity.misc.Attachment; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.controller.SimpleMessagingListener; import com.fsck.k9.fragment.AttachmentDownloadDialogFragment; import com.fsck.k9.fragment.ProgressDialogFragment; import com.fsck.k9.fragment.ProgressDialogFragment.CancelListener; import com.fsck.k9.fragment.AttachmentDownloadDialogFragment; import com.fsck.k9.fragment.AttachmentDownloadDialogFragment.AttachmentDownloadCancelListener; import com.fsck.k9.helper.Contacts; import com.fsck.k9.helper.IdentityHelper; import com.fsck.k9.helper.MailTo; import com.fsck.k9.helper.ReplyToParser; import com.fsck.k9.helper.SimpleTextWatcher; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.MessageViewInfo; import com.fsck.k9.message.ComposePgpInlineDecider; import com.fsck.k9.message.IdentityField; import com.fsck.k9.message.IdentityHeaderParser; import com.fsck.k9.message.MessageBuilder; import com.fsck.k9.message.PgpMessageBuilder; import com.fsck.k9.message.QuotedTextMode; import com.fsck.k9.message.SimpleMessageBuilder; import com.fsck.k9.message.SimpleMessageFormat; import com.fsck.k9.search.LocalSearch; import com.fsck.k9.ui.EolConvertingEditText; import com.fsck.k9.ui.compose.QuotedMessageMvpView; import com.fsck.k9.ui.compose.QuotedMessagePresenter; @SuppressWarnings("deprecation") // TODO get rid of activity dialogs and indeterminate progress bars public class MessageCompose extends K9Activity implements OnClickListener, CancelListener, AttachmentDownloadCancelListener, OnFocusChangeListener, OnCryptoModeChangedListener, OnOpenPgpInlineChangeListener, OnOpenPgpSignOnlyChangeListener, MessageBuilder.Callback, AttachmentPresenter.AttachmentsChangedListener, RecipientPresenter.RecipientsChangedListener { private static final int DIALOG_SAVE_OR_DISCARD_DRAFT_MESSAGE = 1; private static final int DIALOG_CONFIRM_DISCARD_ON_BACK = 2; private static final int DIALOG_CHOOSE_IDENTITY = 3; private static final int DIALOG_CONFIRM_DISCARD = 4; private static final long INVALID_DRAFT_ID = MessagingController.INVALID_MESSAGE_ID; public static final String ACTION_COMPOSE = "com.fsck.k9.intent.action.COMPOSE"; public static final String ACTION_REPLY = "com.fsck.k9.intent.action.REPLY"; public static final String ACTION_REPLY_ALL = "com.fsck.k9.intent.action.REPLY_ALL"; public static final String ACTION_FORWARD = "com.fsck.k9.intent.action.FORWARD"; public static final String ACTION_EDIT_DRAFT = "com.fsck.k9.intent.action.EDIT_DRAFT"; public static final String EXTRA_ACCOUNT = "account"; public static final String EXTRA_MESSAGE_REFERENCE = "message_reference"; public static final String EXTRA_MESSAGE_DECRYPTION_RESULT = "message_decryption_result"; private static final String STATE_KEY_SOURCE_MESSAGE_PROCED = "com.fsck.k9.activity.MessageCompose.stateKeySourceMessageProced"; private static final String STATE_KEY_DRAFT_ID = "com.fsck.k9.activity.MessageCompose.draftId"; 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 String STATE_KEY_READ_RECEIPT = "com.fsck.k9.activity.MessageCompose.messageReadReceipt"; private static final String STATE_KEY_CHANGES_MADE_SINCE_LAST_SAVE = "com.fsck.k9.activity.MessageCompose.changesMadeSinceLastSave"; private static final String STATE_ALREADY_NOTIFIED_USER_OF_EMPTY_SUBJECT = "alreadyNotifiedUserOfEmptySubject"; private static final String FRAGMENT_WAITING_FOR_ATTACHMENT = "waitingForAttachment"; private static final int MSG_PROGRESS_ON = 1; private static final int MSG_PROGRESS_OFF = 2; public static final int MSG_SAVED_DRAFT = 4; private static final int MSG_DISCARDED_DRAFT = 5; private static final int REQUEST_MASK_RECIPIENT_PRESENTER = (1 << 8); private static final int REQUEST_MASK_LOADER_HELPER = (1 << 9); private static final int REQUEST_MASK_ATTACHMENT_PRESENTER = (1 << 10); private static final int REQUEST_MASK_MESSAGE_BUILDER = (1 << 11); /** * Regular expression to remove the first localized "Re:" prefix in subjects. * * Currently: * - "Aw:" (german: abbreviation for "Antwort") */ private static final Pattern PREFIX = Pattern.compile("^AW[:\\s]\\s*", Pattern.CASE_INSENSITIVE); private QuotedMessagePresenter quotedMessagePresenter; private MessageLoaderHelper messageLoaderHelper; private AttachmentPresenter attachmentPresenter; private Contacts contacts; /** * The account used for message composition. */ private Account account; private Identity identity; private boolean identityChanged = false; private boolean signatureChanged = false; // relates to the message being replied to, forwarded, or edited TODO split up? private MessageReference relatedMessageReference; /** * 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 relatedMessageProcessed = false; private RecipientPresenter recipientPresenter; private MessageBuilder currentMessageBuilder; private boolean finishAfterDraftSaved; private boolean alreadyNotifiedUserOfEmptySubject = false; private boolean changesMadeSinceLastSave = false; /** * The database ID of this message's draft. This is used when saving drafts so the message in * the database is updated instead of being created anew. This property is INVALID_DRAFT_ID * until the first save. */ private long draftId = INVALID_DRAFT_ID; private Action action; private boolean requestReadReceipt = false; private TextView chooseIdentityButton; private EditText subjectView; private EolConvertingEditText signatureView; private EolConvertingEditText messageContentView; private LinearLayout attachmentsView; private String referencedMessageIds; private String repliedToMessageId; // The currently used message format. private SimpleMessageFormat currentMessageFormat; private boolean isInSubActivity = false; private boolean navigateUp; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (UpgradeDatabases.actionUpgradeDatabases(this, getIntent())) { finish(); return; } requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); if (K9.getK9ComposerThemeSetting() != K9.Theme.USE_GLOBAL) { // theme the whole content according to the theme (except the action bar) ContextThemeWrapper themeContext = new ContextThemeWrapper(this, K9.getK9ThemeResourceId(K9.getK9ComposerTheme())); @SuppressLint("InflateParams") // this is the top level activity element, it has no root View v = LayoutInflater.from(themeContext).inflate(R.layout.message_compose, null); TypedValue outValue = new TypedValue(); // background color needs to be forced themeContext.getTheme().resolveAttribute(R.attr.messageViewBackgroundColor, outValue, true); v.setBackgroundColor(outValue.data); setContentView(v); } else { setContentView(R.layout.message_compose); } initializeActionBar(); // on api level 15, setContentView() shows the progress bar for some reason... setProgressBarIndeterminateVisibility(false); final Intent intent = getIntent(); String messageReferenceString = intent.getStringExtra(EXTRA_MESSAGE_REFERENCE); relatedMessageReference = MessageReference.parse(messageReferenceString); final String accountUuid = (relatedMessageReference != null) ? relatedMessageReference.getAccountUuid() : intent.getStringExtra(EXTRA_ACCOUNT); account = Preferences.getPreferences(this).getAccount(accountUuid); if (account == null) { account = Preferences.getPreferences(this).getDefaultAccount(); } if (account == 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)); changesMadeSinceLastSave = false; finish(); return; } contacts = Contacts.getInstance(MessageCompose.this); chooseIdentityButton = (TextView) findViewById(R.id.identity); chooseIdentityButton.setOnClickListener(this); RecipientMvpView recipientMvpView = new RecipientMvpView(this); ComposePgpInlineDecider composePgpInlineDecider = new ComposePgpInlineDecider(); recipientPresenter = new RecipientPresenter(getApplicationContext(), getLoaderManager(), recipientMvpView, account, composePgpInlineDecider, new ReplyToParser(), this); recipientPresenter.updateCryptoStatus(); subjectView = (EditText) findViewById(R.id.subject); subjectView.getInputExtras(true).putBoolean("allowEmoji", true); EolConvertingEditText upperSignature = (EolConvertingEditText) findViewById(R.id.upper_signature); EolConvertingEditText lowerSignature = (EolConvertingEditText) findViewById(R.id.lower_signature); QuotedMessageMvpView quotedMessageMvpView = new QuotedMessageMvpView(this); quotedMessagePresenter = new QuotedMessagePresenter(this, quotedMessageMvpView, account); attachmentPresenter = new AttachmentPresenter(getApplicationContext(), attachmentMvpView, getLoaderManager(), this); messageContentView = (EolConvertingEditText) findViewById(R.id.message_content); messageContentView.getInputExtras(true).putBoolean("allowEmoji", true); attachmentsView = (LinearLayout) findViewById(R.id.attachments); TextWatcher draftNeedsChangingTextWatcher = new SimpleTextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { changesMadeSinceLastSave = true; } }; TextWatcher signTextWatcher = new SimpleTextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { changesMadeSinceLastSave = true; signatureChanged = true; } }; recipientMvpView.addTextChangedListener(draftNeedsChangingTextWatcher); quotedMessageMvpView.addTextChangedListener(draftNeedsChangingTextWatcher); subjectView.addTextChangedListener(draftNeedsChangingTextWatcher); messageContentView.addTextChangedListener(draftNeedsChangingTextWatcher); /* * We set this to invisible by default. Other methods will turn it back on if it's * needed. */ quotedMessagePresenter.showOrHideQuotedText(QuotedTextMode.NONE); subjectView.setOnFocusChangeListener(this); messageContentView.setOnFocusChangeListener(this); if (savedInstanceState != null) { /* * This data gets used in onCreate, so grab it here instead of onRestoreInstanceState */ relatedMessageProcessed = savedInstanceState.getBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, false); } if (initFromIntent(intent)) { action = Action.COMPOSE; changesMadeSinceLastSave = true; } else { String action = intent.getAction(); if (ACTION_COMPOSE.equals(action)) { this.action = Action.COMPOSE; } else if (ACTION_REPLY.equals(action)) { this.action = Action.REPLY; } else if (ACTION_REPLY_ALL.equals(action)) { this.action = Action.REPLY_ALL; } else if (ACTION_FORWARD.equals(action)) { this.action = Action.FORWARD; } else if (ACTION_EDIT_DRAFT.equals(action)) { this.action = Action.EDIT_DRAFT; } else { // This shouldn't happen Timber.w("MessageCompose was started with an unsupported action"); this.action = Action.COMPOSE; } } if (identity == null) { identity = account.getIdentity(0); } if (account.isSignatureBeforeQuotedText()) { signatureView = upperSignature; lowerSignature.setVisibility(View.GONE); } else { signatureView = lowerSignature; upperSignature.setVisibility(View.GONE); } updateSignature(); signatureView.addTextChangedListener(signTextWatcher); if (!identity.getSignatureUse()) { signatureView.setVisibility(View.GONE); } requestReadReceipt = account.isMessageReadReceiptAlways(); updateFrom(); if (!relatedMessageProcessed) { if (action == Action.REPLY || action == Action.REPLY_ALL || action == Action.FORWARD || action == Action.EDIT_DRAFT) { messageLoaderHelper = new MessageLoaderHelper(this, getLoaderManager(), getFragmentManager(), messageLoaderCallbacks); internalMessageHandler.sendEmptyMessage(MSG_PROGRESS_ON); Parcelable cachedDecryptionResult = intent.getParcelableExtra(EXTRA_MESSAGE_DECRYPTION_RESULT); messageLoaderHelper.asyncStartOrResumeLoadingMessage(relatedMessageReference, cachedDecryptionResult); } if (action != Action.EDIT_DRAFT) { String alwaysBccString = account.getAlwaysBcc(); if (!TextUtils.isEmpty(alwaysBccString)) { recipientPresenter.addBccAddresses(Address.parse(alwaysBccString)); } } } if (action == Action.REPLY || action == Action.REPLY_ALL) { relatedMessageReference = relatedMessageReference.withModifiedFlag(Flag.ANSWERED); } if (action == Action.REPLY || action == Action.REPLY_ALL || action == Action.EDIT_DRAFT) { //change focus to message body. messageContentView.requestFocus(); } else { // Explicitly set focus to "To:" input field (see issue 2998) recipientMvpView.requestFocusOnToField(); } if (action == Action.FORWARD) { relatedMessageReference = relatedMessageReference.withModifiedFlag(Flag.FORWARDED); } updateMessageFormat(); // Set font size of input controls int fontSize = K9.getFontSizes().getMessageComposeInput(); recipientMvpView.setFontSizes(K9.getFontSizes(), fontSize); quotedMessageMvpView.setFontSizes(K9.getFontSizes(), fontSize); K9.getFontSizes().setViewTextSize(subjectView, fontSize); K9.getFontSizes().setViewTextSize(messageContentView, fontSize); K9.getFontSizes().setViewTextSize(signatureView, fontSize); updateMessageFormat(); setTitle(); currentMessageBuilder = (MessageBuilder) getLastNonConfigurationInstance(); if (currentMessageBuilder != null) { setProgressBarIndeterminateVisibility(true); currentMessageBuilder.reattachCallback(this); } } @Override public void onDestroy() { super.onDestroy(); if (recipientPresenter != null) { recipientPresenter.onActivityDestroy(); } } /** * Handle external intents that trigger the message compose activity. * * <p> * Supported external intents: * <ul> * <li>{@link Intent#ACTION_VIEW}</li> * <li>{@link Intent#ACTION_SENDTO}</li> * <li>{@link Intent#ACTION_SEND}</li> * <li>{@link Intent#ACTION_SEND_MULTIPLE}</li> * </ul> * </p> * * @param intent * The (external) intent that started the activity. * * @return {@code true}, if this activity was started by an external intent. {@code false}, * otherwise. */ private boolean initFromIntent(final Intent intent) { boolean startedByExternalIntent = false; final 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.isMailTo(uri)) { MailTo mailTo = MailTo.parse(uri); initializeFromMailto(mailTo); } } /* * Note: According to the documentation ACTION_VIEW and ACTION_SENDTO don't accept * EXTRA_* parameters. * And previously we didn't process these EXTRAs. But it looks like nobody bothers to * read the official documentation and just copies wrong sample code that happens to * work with the AOSP Email application. And because even big players get this wrong, * we're now finally giving in and read the EXTRAs for those actions (below). */ } if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action) || Intent.ACTION_SENDTO.equals(action) || Intent.ACTION_VIEW.equals(action)) { startedByExternalIntent = true; /* * Note: Here we allow a slight deviation from the documented behavior. * EXTRA_TEXT is used as message body (if available) regardless of the MIME * type of the intent. In addition one or multiple attachments can be added * using EXTRA_STREAM. */ CharSequence text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT); // Only use EXTRA_TEXT if the body hasn't already been set by the mailto URI if (text != null && messageContentView.getText().length() == 0) { messageContentView.setCharacters(text); } String type = intent.getType(); if (Intent.ACTION_SEND.equals(action)) { Uri stream = intent.getParcelableExtra(Intent.EXTRA_STREAM); if (stream != null) { attachmentPresenter.addAttachment(stream, type); } } else { List<Parcelable> list = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); if (list != null) { for (Parcelable parcelable : list) { Uri stream = (Uri) parcelable; if (stream != null) { attachmentPresenter.addAttachment(stream, type); } } } } String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT); // Only use EXTRA_SUBJECT if the subject hasn't already been set by the mailto URI if (subject != null && subjectView.getText().length() == 0) { subjectView.setText(subject); } recipientPresenter.initFromSendOrViewIntent(intent); } return startedByExternalIntent; } @Override protected void onResume() { super.onResume(); MessagingController.getInstance(this).addListener(messagingListener); } @Override public void onPause() { super.onPause(); MessagingController.getInstance(this).removeListener(messagingListener); boolean isPausingOnConfigurationChange = (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == ActivityInfo.CONFIG_ORIENTATION; boolean isCurrentlyBuildingMessage = currentMessageBuilder != null; if (isPausingOnConfigurationChange || isCurrentlyBuildingMessage || isInSubActivity) { return; } checkToSaveDraftImplicitly(); } /** * 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); outState.putBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, relatedMessageProcessed); outState.putLong(STATE_KEY_DRAFT_ID, draftId); outState.putSerializable(STATE_IDENTITY, identity); outState.putBoolean(STATE_IDENTITY_CHANGED, identityChanged); outState.putString(STATE_IN_REPLY_TO, repliedToMessageId); outState.putString(STATE_REFERENCES, referencedMessageIds); outState.putBoolean(STATE_KEY_READ_RECEIPT, requestReadReceipt); outState.putBoolean(STATE_KEY_CHANGES_MADE_SINCE_LAST_SAVE, changesMadeSinceLastSave); outState.putBoolean(STATE_ALREADY_NOTIFIED_USER_OF_EMPTY_SUBJECT, alreadyNotifiedUserOfEmptySubject); recipientPresenter.onSaveInstanceState(outState); quotedMessagePresenter.onSaveInstanceState(outState); attachmentPresenter.onSaveInstanceState(outState); } @Override public Object onRetainNonConfigurationInstance() { if (currentMessageBuilder != null) { currentMessageBuilder.detachCallback(); } return currentMessageBuilder; } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); attachmentsView.removeAllViews(); requestReadReceipt = savedInstanceState.getBoolean(STATE_KEY_READ_RECEIPT); recipientPresenter.onRestoreInstanceState(savedInstanceState); quotedMessagePresenter.onRestoreInstanceState(savedInstanceState); attachmentPresenter.onRestoreInstanceState(savedInstanceState); draftId = savedInstanceState.getLong(STATE_KEY_DRAFT_ID); identity = (Identity) savedInstanceState.getSerializable(STATE_IDENTITY); identityChanged = savedInstanceState.getBoolean(STATE_IDENTITY_CHANGED); repliedToMessageId = savedInstanceState.getString(STATE_IN_REPLY_TO); referencedMessageIds = savedInstanceState.getString(STATE_REFERENCES); changesMadeSinceLastSave = savedInstanceState.getBoolean(STATE_KEY_CHANGES_MADE_SINCE_LAST_SAVE); alreadyNotifiedUserOfEmptySubject = savedInstanceState.getBoolean(STATE_ALREADY_NOTIFIED_USER_OF_EMPTY_SUBJECT); updateFrom(); updateMessageFormat(); } private void setTitle() { setTitle(action.getTitleResource()); } @Nullable private MessageBuilder createMessageBuilder(boolean isDraft) { MessageBuilder builder; recipientPresenter.updateCryptoStatus(); ComposeCryptoStatus cryptoStatus = recipientPresenter.getCurrentCryptoStatus(); // TODO encrypt drafts for storage if (!isDraft && cryptoStatus.shouldUsePgpMessageBuilder()) { SendErrorState maybeSendErrorState = cryptoStatus.getSendErrorStateOrNull(); if (maybeSendErrorState != null) { recipientPresenter.showPgpSendError(maybeSendErrorState); return null; } PgpMessageBuilder pgpBuilder = PgpMessageBuilder.newInstance(); recipientPresenter.builderSetProperties(pgpBuilder); builder = pgpBuilder; } else { builder = SimpleMessageBuilder.newInstance(); } builder.setSubject(Utility.stripNewLines(subjectView.getText().toString())) .setSentDate(new Date()) .setHideTimeZone(K9.hideTimeZone()) .setTo(recipientPresenter.getToAddresses()) .setCc(recipientPresenter.getCcAddresses()) .setBcc(recipientPresenter.getBccAddresses()) .setInReplyTo(repliedToMessageId) .setReferences(referencedMessageIds) .setRequestReadReceipt(requestReadReceipt) .setIdentity(identity) .setMessageFormat(currentMessageFormat) .setText(messageContentView.getCharacters()) .setAttachments(attachmentPresenter.createAttachmentList()) .setSignature(signatureView.getCharacters()) .setSignatureBeforeQuotedText(account.isSignatureBeforeQuotedText()) .setIdentityChanged(identityChanged) .setSignatureChanged(signatureChanged) .setCursorPosition(messageContentView.getSelectionStart()) .setMessageReference(relatedMessageReference) .setDraft(isDraft) .setIsPgpInlineEnabled(cryptoStatus.isPgpInlineModeEnabled()); quotedMessagePresenter.builderSetProperties(builder); return builder; } private void checkToSendMessage() { if (subjectView.getText().length() == 0 && !alreadyNotifiedUserOfEmptySubject) { Toast.makeText(this, R.string.empty_subject, Toast.LENGTH_LONG).show(); alreadyNotifiedUserOfEmptySubject = true; return; } if (recipientPresenter.checkRecipientsOkForSending()) { return; } if (attachmentPresenter.checkOkForSendingOrDraftSaving()) { return; } performSendAfterChecks(); } private void checkToSaveDraftAndSave() { if (!account.hasDraftsFolder()) { Toast.makeText(this, R.string.compose_error_no_draft_folder, Toast.LENGTH_SHORT).show(); return; } if (attachmentPresenter.checkOkForSendingOrDraftSaving()) { return; } finishAfterDraftSaved = true; performSaveAfterChecks(); } private void checkToSaveDraftImplicitly() { if (!account.hasDraftsFolder()) { return; } if (!changesMadeSinceLastSave) { return; } finishAfterDraftSaved = false; performSaveAfterChecks(); } private void performSaveAfterChecks() { currentMessageBuilder = createMessageBuilder(true); if (currentMessageBuilder != null) { setProgressBarIndeterminateVisibility(true); currentMessageBuilder.buildAsync(this); } } public void performSendAfterChecks() { currentMessageBuilder = createMessageBuilder(false); if (currentMessageBuilder != null) { changesMadeSinceLastSave = false; setProgressBarIndeterminateVisibility(true); currentMessageBuilder.buildAsync(this); } } private void onDiscard() { if (draftId != INVALID_DRAFT_ID) { MessagingController.getInstance(getApplication()).deleteDraft(account, draftId); draftId = INVALID_DRAFT_ID; } internalMessageHandler.sendEmptyMessage(MSG_DISCARDED_DRAFT); changesMadeSinceLastSave = false; if (navigateUp) { openAutoExpandFolder(); } else { finish(); } } private void onReadReceipt() { CharSequence txt; if (!requestReadReceipt) { txt = getString(R.string.read_receipt_enabled); requestReadReceipt = true; } else { txt = getString(R.string.read_receipt_disabled); requestReadReceipt = false; } Context context = getApplicationContext(); Toast toast = Toast.makeText(context, txt, Toast.LENGTH_SHORT); toast.show(); } public void showContactPicker(int requestCode) { requestCode |= REQUEST_MASK_RECIPIENT_PRESENTER; isInSubActivity = true; startActivityForResult(contacts.contactPickerIntent(), requestCode); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { isInSubActivity = false; if ((requestCode & REQUEST_MASK_MESSAGE_BUILDER) == REQUEST_MASK_MESSAGE_BUILDER) { requestCode ^= REQUEST_MASK_MESSAGE_BUILDER; if (currentMessageBuilder == null) { Timber.e("Got a message builder activity result for no message builder, " + "this is an illegal state!"); return; } currentMessageBuilder.onActivityResult(requestCode, resultCode, data, this); return; } if ((requestCode & REQUEST_MASK_RECIPIENT_PRESENTER) == REQUEST_MASK_RECIPIENT_PRESENTER) { requestCode ^= REQUEST_MASK_RECIPIENT_PRESENTER; recipientPresenter.onActivityResult(requestCode, resultCode, data); return; } if ((requestCode & REQUEST_MASK_LOADER_HELPER) == REQUEST_MASK_LOADER_HELPER) { requestCode ^= REQUEST_MASK_LOADER_HELPER; messageLoaderHelper.onActivityResult(requestCode, resultCode, data); return; } if ((requestCode & REQUEST_MASK_ATTACHMENT_PRESENTER) == REQUEST_MASK_ATTACHMENT_PRESENTER) { requestCode ^= REQUEST_MASK_ATTACHMENT_PRESENTER; attachmentPresenter.onActivityResult(resultCode, requestCode, data); } } private void onAccountChosen(Account account, Identity identity) { if (!this.account.equals(account)) { Timber.v("Switching account from %s to %s", this.account, account); // on draft edit, make sure we don't keep previous message UID if (action == Action.EDIT_DRAFT) { relatedMessageReference = null; } // test whether there is something to save if (changesMadeSinceLastSave || (draftId != INVALID_DRAFT_ID)) { final long previousDraftId = draftId; final Account previousAccount = this.account; // make current message appear as new draftId = INVALID_DRAFT_ID; // actual account switch this.account = account; Timber.v("Account switch, saving new draft in new account"); checkToSaveDraftImplicitly(); if (previousDraftId != INVALID_DRAFT_ID) { Timber.v("Account switch, deleting draft from previous account: %d", previousDraftId); MessagingController.getInstance(getApplication()).deleteDraft(previousAccount, previousDraftId); } } else { this.account = account; } // Show CC/BCC text input field when switching to an account that always wants them // displayed. // Please note that we're not hiding the fields if the user switches back to an account // that doesn't have this setting checked. recipientPresenter.onSwitchAccount(this.account); quotedMessagePresenter.onSwitchAccount(this.account); // not sure how to handle mFolder, mSourceMessage? } switchToIdentity(identity); } private void switchToIdentity(Identity identity) { this.identity = identity; identityChanged = true; changesMadeSinceLastSave = true; updateFrom(); updateSignature(); updateMessageFormat(); recipientPresenter.onSwitchIdentity(identity); } private void updateFrom() { chooseIdentityButton.setText(identity.getEmail()); } private void updateSignature() { if (identity.getSignatureUse()) { signatureView.setCharacters(identity.getSignature()); signatureView.setVisibility(View.VISIBLE); } else { signatureView.setVisibility(View.GONE); } } @Override public void onFocusChange(View v, boolean hasFocus) { switch (v.getId()) { case R.id.message_content: case R.id.subject: if (hasFocus) { recipientPresenter.onNonRecipientFieldFocused(); } break; } } @Override public void onCryptoModeChanged(CryptoMode cryptoMode) { recipientPresenter.onCryptoModeChanged(cryptoMode); } @Override public void onOpenPgpInlineChange(boolean enabled) { recipientPresenter.onCryptoPgpInlineChanged(enabled); } @Override public void onOpenPgpSignOnlyChange(boolean enabled) { recipientPresenter.onCryptoPgpSignOnlyDisabled(); } @Override public void onAttachmentAdded() { changesMadeSinceLastSave = true; } @Override public void onAttachmentRemoved() { changesMadeSinceLastSave = true; } @Override public void onRecipientsChanged() { changesMadeSinceLastSave = true; } @Override public void onClick(View view) { switch (view.getId()) { case R.id.identity: showDialog(DIALOG_CHOOSE_IDENTITY); break; } } private void askBeforeDiscard() { if (K9.confirmDiscardMessage()) { showDialog(DIALOG_CONFIRM_DISCARD); } else { onDiscard(); } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: prepareToFinish(true); break; case R.id.send: checkToSendMessage(); break; case R.id.save: checkToSaveDraftAndSave(); break; case R.id.discard: askBeforeDiscard(); break; case R.id.add_from_contacts: recipientPresenter.onMenuAddFromContacts(); break; case R.id.openpgp_inline_enable: recipientPresenter.onMenuSetPgpInline(true); updateMessageFormat(); break; case R.id.openpgp_inline_disable: recipientPresenter.onMenuSetPgpInline(false); updateMessageFormat(); break; case R.id.openpgp_sign_only: recipientPresenter.onMenuSetSignOnly(true); break; case R.id.openpgp_sign_only_disable: recipientPresenter.onMenuSetSignOnly(false); break; case R.id.add_attachment: attachmentPresenter.onClickAddAttachment(recipientPresenter); break; case R.id.read_receipt: onReadReceipt(); break; default: return super.onOptionsItemSelected(item); } return true; } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); if (isFinishing()) { return false; } getMenuInflater().inflate(R.menu.message_compose_option, menu); // Disable the 'Save' menu option if Drafts folder is set to -NONE- if (!account.hasDraftsFolder()) { menu.findItem(R.id.save).setEnabled(false); } return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); recipientPresenter.onPrepareOptionsMenu(menu); return true; } @Override public void onBackPressed() { prepareToFinish(false); } private void prepareToFinish(boolean shouldNavigateUp) { navigateUp = shouldNavigateUp; if (changesMadeSinceLastSave && draftIsNotEmpty()) { if (!account.hasDraftsFolder()) { showDialog(DIALOG_CONFIRM_DISCARD_ON_BACK); } else { showDialog(DIALOG_SAVE_OR_DISCARD_DRAFT_MESSAGE); } } else { // Check if editing an existing draft. if (draftId == INVALID_DRAFT_ID) { onDiscard(); } else { if (navigateUp) { openAutoExpandFolder(); } else { super.onBackPressed(); } } } } private void openAutoExpandFolder() { String folder = account.getAutoExpandFolderName(); LocalSearch search = new LocalSearch(folder); search.addAccountUuid(account.getUuid()); search.addAllowedFolder(folder); MessageList.actionDisplaySearch(this, search, false, true); finish(); } private boolean draftIsNotEmpty() { if (messageContentView.getText().length() != 0) { return true; } if (!attachmentPresenter.createAttachmentList().isEmpty()) { return true; } if (subjectView.getText().length() != 0) { return true; } if (!recipientPresenter.getToAddresses().isEmpty() || !recipientPresenter.getCcAddresses().isEmpty() || !recipientPresenter.getBccAddresses().isEmpty()) { return true; } return false; } @Override public void onProgressCancel(AttachmentDownloadDialogFragment fragment) { attachmentPresenter.attachmentProgressDialogCancelled(); } public void onProgressCancel(ProgressDialogFragment fragment) { attachmentPresenter.attachmentProgressDialogCancelled(); } @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() { @Override public void onClick(DialogInterface dialog, int whichButton) { dismissDialog(DIALOG_SAVE_OR_DISCARD_DRAFT_MESSAGE); checkToSaveDraftAndSave(); } }) .setNegativeButton(R.string.discard_action, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int whichButton) { dismissDialog(DIALOG_SAVE_OR_DISCARD_DRAFT_MESSAGE); onDiscard(); } }) .create(); case DIALOG_CONFIRM_DISCARD_ON_BACK: return new AlertDialog.Builder(this) .setTitle(R.string.confirm_discard_draft_message_title) .setMessage(R.string.confirm_discard_draft_message) .setPositiveButton(R.string.cancel_action, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int whichButton) { dismissDialog(DIALOG_CONFIRM_DISCARD_ON_BACK); } }) .setNegativeButton(R.string.discard_action, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int whichButton) { dismissDialog(DIALOG_CONFIRM_DISCARD_ON_BACK); Toast.makeText(MessageCompose.this, getString(R.string.message_discarded_toast), Toast.LENGTH_LONG).show(); onDiscard(); } }) .create(); case DIALOG_CHOOSE_IDENTITY: Context context = new ContextThemeWrapper(this, (K9.getK9Theme() == K9.Theme.LIGHT) ? R.style.Theme_K9_Dialog_Light : R.style.Theme_K9_Dialog_Dark); Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.send_as); final IdentityAdapter adapter = new IdentityAdapter(context); builder.setAdapter(adapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { IdentityContainer container = (IdentityContainer) adapter.getItem(which); onAccountChosen(container.account, container.identity); } }); return builder.create(); case DIALOG_CONFIRM_DISCARD: { return new AlertDialog.Builder(this) .setTitle(R.string.dialog_confirm_delete_title) .setMessage(R.string.dialog_confirm_delete_message) .setPositiveButton(R.string.dialog_confirm_delete_confirm_button, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { onDiscard(); } }) .setNegativeButton(R.string.dialog_confirm_delete_cancel_button, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { } }) .create(); } } return super.onCreateDialog(id); } public void saveDraftEventually() { changesMadeSinceLastSave = true; } public void loadQuotedTextForEdit() { if (relatedMessageReference == null) { // shouldn't happen... throw new IllegalStateException("tried to edit quoted message with no referenced message"); } messageLoaderHelper.asyncStartOrResumeLoadingMessage(relatedMessageReference, null); } /** * 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 messageViewInfo * The source message used to populate the various text fields. */ private void processSourceMessage(MessageViewInfo messageViewInfo) { try { switch (action) { case REPLY: case REPLY_ALL: { processMessageToReplyTo(messageViewInfo); break; } case FORWARD: { processMessageToForward(messageViewInfo); break; } case EDIT_DRAFT: { processDraftMessage(messageViewInfo); break; } default: { Timber.w("processSourceMessage() called with unsupported action"); break; } } } catch (MessagingException me) { /* * Let the user continue composing their message even if we have a problem processing * the source message. Log it as an error, though. */ Timber.e(me, "Error while processing source message: "); } finally { relatedMessageProcessed = true; changesMadeSinceLastSave = false; } updateMessageFormat(); } private void processMessageToReplyTo(MessageViewInfo messageViewInfo) throws MessagingException { Message message = messageViewInfo.message; if (message.getSubject() != null) { final String subject = PREFIX.matcher(message.getSubject()).replaceFirst(""); if (!subject.toLowerCase(Locale.US).startsWith("re:")) { subjectView.setText("Re: " + subject); } else { subjectView.setText(subject); } } else { subjectView.setText(""); } /* * If a reply-to was included with the message use that, otherwise use the from * or sender address. */ boolean isReplyAll = action == Action.REPLY_ALL; recipientPresenter.initFromReplyToMessage(message, isReplyAll); if (message.getMessageId() != null && message.getMessageId().length() > 0) { repliedToMessageId = message.getMessageId(); String[] refs = message.getReferences(); if (refs != null && refs.length > 0) { referencedMessageIds = TextUtils.join("", refs) + " " + repliedToMessageId; } else { referencedMessageIds = repliedToMessageId; } } else { Timber.d("could not get Message-ID."); } // Quote the message and setup the UI. quotedMessagePresenter.initFromReplyToMessage(messageViewInfo, action); if (action == Action.REPLY || action == Action.REPLY_ALL) { Identity useIdentity = IdentityHelper.getRecipientIdentityFromMessage(account, message); Identity defaultIdentity = account.getIdentity(0); if (useIdentity != defaultIdentity) { switchToIdentity(useIdentity); } } } private void processMessageToForward(MessageViewInfo messageViewInfo) throws MessagingException { Message message = messageViewInfo.message; String subject = message.getSubject(); if (subject != null && !subject.toLowerCase(Locale.US).startsWith("fwd:")) { subjectView.setText("Fwd: " + subject); } else { subjectView.setText(subject); } // "Be Like Thunderbird" - on forwarded messages, set the message ID // of the forwarded message in the references and the reply to. TB // only includes ID of the message being forwarded in the reference, // even if there are multiple references. if (!TextUtils.isEmpty(message.getMessageId())) { repliedToMessageId = message.getMessageId(); referencedMessageIds = repliedToMessageId; } else { Timber.d("could not get Message-ID."); } // Quote the message and setup the UI. quotedMessagePresenter.processMessageToForward(messageViewInfo); attachmentPresenter.processMessageToForward(messageViewInfo); } private void processDraftMessage(MessageViewInfo messageViewInfo) { Message message = messageViewInfo.message; draftId = MessagingController.getInstance(getApplication()).getId(message); subjectView.setText(message.getSubject()); recipientPresenter.initFromDraftMessage(message); // Read In-Reply-To header from draft final String[] inReplyTo = message.getHeader("In-Reply-To"); if (inReplyTo.length >= 1) { repliedToMessageId = inReplyTo[0]; } // Read References header from draft final String[] references = message.getHeader("References"); if (references.length >= 1) { referencedMessageIds = references[0]; } if (!relatedMessageProcessed) { attachmentPresenter.loadNonInlineAttachments(messageViewInfo); } // Decode the identity header when loading a draft. // See buildIdentityHeader(TextBody) for a detailed description of the composition of this blob. Map<IdentityField, String> k9identity = new HashMap<>(); String[] identityHeaders = message.getHeader(K9.IDENTITY_HEADER); if (identityHeaders.length > 0 && identityHeaders[0] != null) { k9identity = IdentityHeaderParser.parse(identityHeaders[0]); } Identity newIdentity = new Identity(); if (k9identity.containsKey(IdentityField.SIGNATURE)) { newIdentity.setSignatureUse(true); newIdentity.setSignature(k9identity.get(IdentityField.SIGNATURE)); signatureChanged = true; } else { if (message instanceof LocalMessage) { newIdentity.setSignatureUse(((LocalMessage) message).getFolder().getSignatureUse()); } newIdentity.setSignature(identity.getSignature()); } if (k9identity.containsKey(IdentityField.NAME)) { newIdentity.setName(k9identity.get(IdentityField.NAME)); identityChanged = true; } else { newIdentity.setName(identity.getName()); } if (k9identity.containsKey(IdentityField.EMAIL)) { newIdentity.setEmail(k9identity.get(IdentityField.EMAIL)); identityChanged = true; } else { newIdentity.setEmail(identity.getEmail()); } if (k9identity.containsKey(IdentityField.ORIGINAL_MESSAGE)) { relatedMessageReference = null; String originalMessage = k9identity.get(IdentityField.ORIGINAL_MESSAGE); MessageReference messageReference = MessageReference.parse(originalMessage); if (messageReference != null) { // Check if this is a valid account in our database Preferences prefs = Preferences.getPreferences(getApplicationContext()); Account account = prefs.getAccount(messageReference.getAccountUuid()); if (account != null) { relatedMessageReference = messageReference; } } } identity = newIdentity; updateSignature(); updateFrom(); quotedMessagePresenter.processDraftMessage(messageViewInfo, k9identity); } static class SendMessageTask extends AsyncTask<Void, Void, Void> { final Context context; final Account account; final Contacts contacts; final Message message; final Long draftId; final MessageReference messageReference; SendMessageTask(Context context, Account account, Contacts contacts, Message message, Long draftId, MessageReference messageReference) { this.context = context; this.account = account; this.contacts = contacts; this.message = message; this.draftId = draftId; this.messageReference = messageReference; } @Override protected Void doInBackground(Void... params) { try { contacts.markAsContacted(message.getRecipients(RecipientType.TO)); contacts.markAsContacted(message.getRecipients(RecipientType.CC)); contacts.markAsContacted(message.getRecipients(RecipientType.BCC)); updateReferencedMessage(); } catch (Exception e) { Timber.e(e, "Failed to mark contact as contacted."); } MessagingController.getInstance(context).sendMessage(account, message, null); if (draftId != null) { // TODO set draft id to invalid in MessageCompose! MessagingController.getInstance(context).deleteDraft(account, draftId); } return null; } /** * Set the flag on the referenced message(indicated we replied / forwarded the message) **/ private void updateReferencedMessage() { if (messageReference != null && messageReference.getFlag() != null) { Timber.d("Setting referenced message (%s, %s) flag to %s", messageReference.getFolderName(), messageReference.getUid(), messageReference.getFlag()); final Account account = Preferences.getPreferences(context) .getAccount(messageReference.getAccountUuid()); final String folderName = messageReference.getFolderName(); final String sourceMessageUid = messageReference.getUid(); MessagingController.getInstance(context).setFlag(account, folderName, sourceMessageUid, messageReference.getFlag(), true); } } } /** * 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. * * @param mailTo * The MailTo object we use to initialize message field */ private void initializeFromMailto(MailTo mailTo) { recipientPresenter.initFromMailto(mailTo); String subject = mailTo.getSubject(); if (subject != null && !subject.isEmpty()) { subjectView.setText(subject); } String body = mailTo.getBody(); if (body != null && !body.isEmpty()) { messageContentView.setCharacters(body); } } private void setCurrentMessageFormat(SimpleMessageFormat format) { // This method will later be used to enable/disable the rich text editing mode. currentMessageFormat = format; } public void updateMessageFormat() { MessageFormat origMessageFormat = account.getMessageFormat(); SimpleMessageFormat messageFormat; if (origMessageFormat == MessageFormat.TEXT) { // The user wants to send text/plain messages. We don't override that choice under // any circumstances. messageFormat = SimpleMessageFormat.TEXT; } else if (quotedMessagePresenter.isForcePlainText() && quotedMessagePresenter.includeQuotedText()) { // Right now we send a text/plain-only message when the quoted text was edited, no // matter what the user selected for the message format. messageFormat = SimpleMessageFormat.TEXT; } else if (recipientPresenter.isForceTextMessageFormat()) { // Right now we only support PGP inline which doesn't play well with HTML. So force // plain text in those cases. messageFormat = SimpleMessageFormat.TEXT; } else if (origMessageFormat == MessageFormat.AUTO) { if (action == Action.COMPOSE || quotedMessagePresenter.isQuotedTextText() || !quotedMessagePresenter.includeQuotedText()) { // If the message format is set to "AUTO" we use text/plain whenever possible. That // is, when composing new messages and replying to or forwarding text/plain // messages. messageFormat = SimpleMessageFormat.TEXT; } else { messageFormat = SimpleMessageFormat.HTML; } } else { // In all other cases use HTML messageFormat = SimpleMessageFormat.HTML; } setCurrentMessageFormat(messageFormat); } @Override public void onMessageBuildSuccess(MimeMessage message, boolean isDraft) { if (isDraft) { changesMadeSinceLastSave = false; currentMessageBuilder = null; if (action == Action.EDIT_DRAFT && relatedMessageReference != null) { message.setUid(relatedMessageReference.getUid()); } // TODO more appropriate logic here? not sure boolean saveRemotely = !recipientPresenter.getCurrentCryptoStatus().shouldUsePgpMessageBuilder(); new SaveMessageTask(getApplicationContext(), account, contacts, internalMessageHandler, message, draftId, saveRemotely).execute(); if (finishAfterDraftSaved) { finish(); } else { setProgressBarIndeterminateVisibility(false); } } else { currentMessageBuilder = null; new SendMessageTask(getApplicationContext(), account, contacts, message, draftId != INVALID_DRAFT_ID ? draftId : null, relatedMessageReference).execute(); finish(); } } @Override public void onMessageBuildCancel() { currentMessageBuilder = null; setProgressBarIndeterminateVisibility(false); } @Override public void onMessageBuildException(MessagingException me) { Timber.e(me, "Error sending message"); Toast.makeText(MessageCompose.this, getString(R.string.send_failed_reason, me.getLocalizedMessage()), Toast.LENGTH_LONG).show(); currentMessageBuilder = null; setProgressBarIndeterminateVisibility(false); } @Override public void onMessageBuildReturnPendingIntent(PendingIntent pendingIntent, int requestCode) { requestCode |= REQUEST_MASK_MESSAGE_BUILDER; try { startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0); } catch (SendIntentException e) { Timber.e(e, "Error starting pending intent from builder!"); } } public void launchUserInteractionPendingIntent(PendingIntent pendingIntent, int requestCode) { requestCode |= REQUEST_MASK_RECIPIENT_PRESENTER; try { startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0); } catch (SendIntentException e) { e.printStackTrace(); } } public void loadLocalMessageForDisplay(MessageViewInfo messageViewInfo, Action action) { // We check to see if we've previously processed the source message since this // could be called when switching from HTML to text replies. If that happens, we // only want to update the UI with quoted text (which picks the appropriate // part). if (relatedMessageProcessed) { try { quotedMessagePresenter.populateUIWithQuotedMessage(messageViewInfo, true, action); } catch (MessagingException e) { // Hm, if we couldn't populate the UI after source reprocessing, let's just delete it? quotedMessagePresenter.showOrHideQuotedText(QuotedTextMode.HIDE); Timber.e(e, "Could not re-process source message; deleting quoted text to be safe."); } updateMessageFormat(); } else { processSourceMessage(messageViewInfo); relatedMessageProcessed = true; } } private MessageLoaderCallbacks messageLoaderCallbacks = new MessageLoaderCallbacks() { @Override public void onMessageDataLoadFinished(LocalMessage message) { // nothing to do here, we don't care about message headers } @Override public void onMessageDataLoadFailed() { internalMessageHandler.sendEmptyMessage(MSG_PROGRESS_OFF); Toast.makeText(MessageCompose.this, R.string.status_invalid_id_error, Toast.LENGTH_LONG).show(); } @Override public void onMessageViewInfoLoadFinished(MessageViewInfo messageViewInfo) { internalMessageHandler.sendEmptyMessage(MSG_PROGRESS_OFF); loadLocalMessageForDisplay(messageViewInfo, action); } @Override public void onMessageViewInfoLoadFailed(MessageViewInfo messageViewInfo) { internalMessageHandler.sendEmptyMessage(MSG_PROGRESS_OFF); Toast.makeText(MessageCompose.this, R.string.status_invalid_id_error, Toast.LENGTH_LONG).show(); } @Override public void setLoadingProgress(int current, int max) { // nvm - we don't have a progress bar } @Override public void startIntentSenderForMessageLoaderHelper(IntentSender si, int requestCode, Intent fillIntent, int flagsMask, int flagValues, int extraFlags) { try { requestCode |= REQUEST_MASK_LOADER_HELPER; startIntentSenderForResult(si, requestCode, fillIntent, flagsMask, flagValues, extraFlags); } catch (SendIntentException e) { Timber.e(e, "Irrecoverable error calling PendingIntent!"); } } @Override public void onDownloadErrorMessageNotFound() { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(MessageCompose.this, R.string.status_invalid_id_error, Toast.LENGTH_LONG).show(); } }); } @Override public void onDownloadErrorNetworkError() { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(MessageCompose.this, R.string.status_network_error, Toast.LENGTH_LONG).show(); } }); } }; private void initializeActionBar() { ActionBar actionBar = getActionBar(); actionBar.setDisplayHomeAsUpEnabled(true); } // TODO We miss callbacks for this listener if they happens while we are paused! public MessagingListener messagingListener = new SimpleMessagingListener() { @Override public void messageUidChanged(Account account, String folder, String oldUid, String newUid) { if (relatedMessageReference == null) { return; } Account sourceAccount = Preferences.getPreferences(MessageCompose.this) .getAccount(relatedMessageReference.getAccountUuid()); String sourceFolder = relatedMessageReference.getFolderName(); String sourceMessageUid = relatedMessageReference.getUid(); boolean changedMessageIsCurrent = account.equals(sourceAccount) && folder.equals(sourceFolder) && oldUid.equals(sourceMessageUid); if (changedMessageIsCurrent) { relatedMessageReference = relatedMessageReference.withModifiedUid(newUid); } } }; AttachmentMvpView attachmentMvpView = new AttachmentMvpView() { private HashMap<Uri, View> attachmentViews = new HashMap<>(); @Override public void showWaitingForAttachmentDialog(WaitingAction waitingAction) { String title; switch (waitingAction) { case SEND: { title = getString(R.string.fetching_attachment_dialog_title_send); break; } case SAVE: { title = getString(R.string.fetching_attachment_dialog_title_save); break; } default: { return; } } ProgressDialogFragment fragment = ProgressDialogFragment.newInstance(title, getString(R.string.fetching_attachment_dialog_message)); fragment.show(getFragmentManager(), FRAGMENT_WAITING_FOR_ATTACHMENT); } @Override public void dismissWaitingForAttachmentDialog() { ProgressDialogFragment fragment = (ProgressDialogFragment) getFragmentManager().findFragmentByTag(FRAGMENT_WAITING_FOR_ATTACHMENT); if (fragment != null) { fragment.dismiss(); } } @Override @SuppressLint("InlinedApi") public void showPickAttachmentDialog(int requestCode) { requestCode |= REQUEST_MASK_ATTACHMENT_PRESENTER; Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); i.addCategory(Intent.CATEGORY_OPENABLE); i.setType("*/*"); isInSubActivity = true; startActivityForResult(Intent.createChooser(i, null), requestCode); } @Override public void addAttachmentView(final Attachment attachment) { View view = getLayoutInflater().inflate(R.layout.message_compose_attachment, attachmentsView, false); attachmentViews.put(attachment.uri, view); View deleteButton = view.findViewById(R.id.attachment_delete); deleteButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { attachmentPresenter.onClickRemoveAttachment(attachment.uri); } }); updateAttachmentView(attachment); attachmentsView.addView(view); } @Override public void updateAttachmentView(Attachment attachment) { View view = attachmentViews.get(attachment.uri); if (view == null) { throw new IllegalArgumentException(); } TextView nameView = (TextView) view.findViewById(R.id.attachment_name); boolean hasMetadata = (attachment.state != Attachment.LoadingState.URI_ONLY); if (hasMetadata) { nameView.setText(attachment.name); } else { nameView.setText(R.string.loading_attachment); } View progressBar = view.findViewById(R.id.progressBar); boolean isLoadingComplete = (attachment.state == Attachment.LoadingState.COMPLETE); progressBar.setVisibility(isLoadingComplete ? View.GONE : View.VISIBLE); } @Override public void removeAttachmentView(Attachment attachment) { View view = attachmentViews.get(attachment.uri); attachmentsView.removeView(view); attachmentViews.remove(attachment.uri); } @Override public void performSendAfterChecks() { MessageCompose.this.performSendAfterChecks(); } @Override public void performSaveAfterChecks() { MessageCompose.this.performSaveAfterChecks(); } @Override public void showMissingAttachmentsPartialMessageWarning() { Toast.makeText(MessageCompose.this, getString(R.string.message_compose_attachments_skipped_toast), Toast.LENGTH_LONG).show(); } }; private Handler internalMessageHandler = 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_SAVED_DRAFT: draftId = (Long) msg.obj; 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; } } }; public enum Action { COMPOSE(R.string.compose_title_compose), REPLY(R.string.compose_title_reply), REPLY_ALL(R.string.compose_title_reply_all), FORWARD(R.string.compose_title_forward), EDIT_DRAFT(R.string.compose_title_compose); private final int titleResource; Action(@StringRes int titleResource) { this.titleResource = titleResource; } @StringRes public int getTitleResource() { return titleResource; } } }