package com.fsck.k9.activity; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; 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.ResolveInfo; import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.media.MediaScannerConnection; import android.media.MediaScannerConnection.MediaScannerConnectionClient; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.style.StyleSpan; import android.util.Config; import android.util.Log; import android.util.TypedValue; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.Window; import android.widget.Button; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.fsck.k9.Account; import com.fsck.k9.FontSizes; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.R; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.crypto.CryptoProvider; import com.fsck.k9.crypto.PgpData; import com.fsck.k9.helper.Contacts; import com.fsck.k9.helper.SizeFormatter; 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.Multipart; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.store.StorageManager; import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBodyPart; import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.store.LocalStore.LocalTextBody; import com.fsck.k9.provider.AttachmentProvider; import com.fsck.k9.view.AccessibleWebView; import com.fsck.k9.view.MessageWebView; import com.fsck.k9.view.ToggleScrollView; public class MessageView extends K9Activity implements OnClickListener { private static final String EXTRA_MESSAGE_REFERENCE = "com.fsck.k9.MessageView_messageReference"; private static final String EXTRA_MESSAGE_REFERENCES = "com.fsck.k9.MessageView_messageReferences"; private static final String EXTRA_NEXT = "com.fsck.k9.MessageView_next"; private static final String SHOW_PICTURES = "showPictures"; private static final String STATE_PGP_DATA = "pgpData"; private static final int ACTIVITY_CHOOSE_FOLDER_MOVE = 1; private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2; private TextView mFromView; private TextView mDateView; private TextView mTimeView; private TextView mToView; private TextView mCcView; private TextView mSubjectView; public View chip; private CheckBox mFlagged; private int defaultSubjectColor; private View mDecryptLayout; private Button mDecryptButton; private LinearLayout mCryptoSignatureLayout = null; private ImageView mCryptoSignatureStatusImage = null; private TextView mCryptoSignatureUserId = null; private TextView mCryptoSignatureUserIdRest = null; private MessageWebView mMessageContentView; private boolean mScreenReaderEnabled; private AccessibleWebView mAccessibleMessageContentView; private LinearLayout mHeaderContainer; private LinearLayout mAttachments; private LinearLayout mToContainerView; private LinearLayout mCcContainerView; private TextView mAdditionalHeadersView; private View mAttachmentIcon; private View mShowPicturesSection; private boolean mShowPictures; private Button mDownloadRemainder; private static Drawable answeredIcon; View next; View previous; private View mDelete; private View mArchive; private View mMove; private View mSpam; private ToggleScrollView mToggleScrollView; private Account mAccount; private MessageReference mMessageReference; private ArrayList mMessageReferences; private Message mMessage; private PgpData mPgpData = null; private static final int PREVIOUS = 1; private static final int NEXT = 2; private int mLastDirection = PREVIOUS; private MessageReference mNextMessage = null; private MessageReference mPreviousMessage = null; private Menu optionsMenu = null; private Listener mListener = new Listener(); private MessageViewHandler mHandler = new MessageViewHandler(); private FontSizes mFontSizes = K9.getFontSizes(); private Contacts mContacts; private StorageManager.StorageListener mStorageListener = new StorageListenerImplementation(); private final class StorageListenerImplementation implements StorageManager.StorageListener { @Override public void onUnmount(String providerId) { if (providerId.equals(mAccount.getLocalStorageProviderId())) { runOnUiThread(new Runnable() { @Override public void run() { onAccountUnavailable(); } }); } } @Override public void onMount(String providerId) { // no-op } } /** * Pair class is only available since API Level 5, so we need * this helper class unfortunately */ private static class HeaderEntry { public String label; public String value; public HeaderEntry(String label, String value) { this.label = label; this.value = value; } } @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_UP) { // Text selection is finished. Allow scrolling again. mToggleScrollView.setScrolling(true); } else if (K9.zoomControlsEnabled()) { // If we have system zoom controls enabled, disable scrolling so the screen isn't wiggling around while // trying to zoom. if (ev.getAction() == MotionEvent.ACTION_POINTER_2_DOWN) { mToggleScrollView.setScrolling(false); } else if (ev.getAction() == MotionEvent.ACTION_POINTER_2_UP) { mToggleScrollView.setScrolling(true); } } return super.dispatchTouchEvent(ev); } @Override public boolean dispatchKeyEvent(KeyEvent event) { boolean ret = false; if (KeyEvent.ACTION_DOWN == event.getAction()) { ret = onKeyDown(event.getKeyCode(), event); } if (!ret) { ret = super.dispatchKeyEvent(event); } return ret; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: { if (K9.useVolumeKeysForNavigationEnabled()) { onNext(); return true; } } case KeyEvent.KEYCODE_VOLUME_DOWN: { if (K9.useVolumeKeysForNavigationEnabled()) { onPrevious(); return true; } } case KeyEvent.KEYCODE_SHIFT_LEFT: case KeyEvent.KEYCODE_SHIFT_RIGHT: { /* * Selecting text started via shift key. Disable scrolling as * this causes problems when selecting text. */ mToggleScrollView.setScrolling(false); break; } case KeyEvent.KEYCODE_DEL: { onDelete(); return true; } case KeyEvent.KEYCODE_D: { onDelete(); return true; } case KeyEvent.KEYCODE_F: { onForward(); return true; } case KeyEvent.KEYCODE_A: { onReplyAll(); return true; } case KeyEvent.KEYCODE_R: { onReply(); return true; } case KeyEvent.KEYCODE_G: { onFlag(); return true; } case KeyEvent.KEYCODE_M: { onMove(); return true; } case KeyEvent.KEYCODE_S: { onRefile(mAccount.getSpamFolderName()); return true; } case KeyEvent.KEYCODE_V: { onRefile(mAccount.getArchiveFolderName()); return true; } case KeyEvent.KEYCODE_Y: { onCopy(); return true; } case KeyEvent.KEYCODE_J: case KeyEvent.KEYCODE_P: { onPrevious(); return true; } case KeyEvent.KEYCODE_N: case KeyEvent.KEYCODE_K: { onNext(); return true; } case KeyEvent.KEYCODE_Z: { if (event.isShiftPressed()) { mHandler.post(new Runnable() { public void run() { if (mScreenReaderEnabled) { mAccessibleMessageContentView.zoomIn(); } else { mMessageContentView.zoomIn(); } } }); } else { mHandler.post(new Runnable() { public void run() { if (mScreenReaderEnabled) { mAccessibleMessageContentView.zoomIn(); } else { mMessageContentView.zoomOut(); } } }); } return true; } case KeyEvent.KEYCODE_H: { Toast toast = Toast.makeText(this, R.string.message_help_key, Toast.LENGTH_LONG); toast.show(); return true; } } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { // Swallow these events too to avoid the audible notification of a volume change if (K9.useVolumeKeysForNavigationEnabled()) { if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP) || (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) { if (K9.DEBUG) Log.v(K9.LOG_TAG, "Swallowed key up."); return true; } } return super.onKeyUp(keyCode,event); } class MessageViewHandler extends Handler { public void progress(final boolean progress) { runOnUiThread(new Runnable() { public void run() { setProgressBarIndeterminateVisibility(progress); } }); } public void addAttachment(final View attachmentView) { runOnUiThread(new Runnable() { public void run() { mAttachments.addView(attachmentView); mAttachments.setVisibility(View.VISIBLE); } }); } public void removeAllAttachments() { runOnUiThread(new Runnable() { public void run() { for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { mAttachments.removeView(mAttachments.getChildAt(i)); } } }); } public void setAttachmentsEnabled(final boolean enabled) { runOnUiThread(new Runnable() { public void run() { for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag(); attachment.viewButton.setEnabled(enabled); attachment.downloadButton.setEnabled(enabled); } } }); } public void setHeaders( final Message message) throws MessagingException { final Contacts contacts = K9.showContactName() ? mContacts : null; final CharSequence from = Address.toFriendly(message.getFrom(), contacts); final String date = getDateFormat().format(message.getSentDate()); final String time = getTimeFormat().format(message.getSentDate()); final CharSequence to = Address.toFriendly(message.getRecipients(RecipientType.TO), contacts); final CharSequence cc = Address.toFriendly(message.getRecipients(RecipientType.CC), contacts); runOnUiThread(new Runnable() { public void run() { String subject = message.getSubject(); setTitle(subject); if (subject == null || subject.equals("")) { mSubjectView.setText(getText(R.string.general_no_subject)); } else { mSubjectView.setText(subject); } mFromView.setText(from); if (date != null) { mDateView.setText(date); mDateView.setVisibility(View.VISIBLE); } else { mDateView.setVisibility(View.GONE); } mTimeView.setText(time); mToContainerView.setVisibility((to != null && to.length() > 0) ? View.VISIBLE : View.GONE); mToView.setText(to); mCcContainerView.setVisibility((cc != null && cc.length() > 0) ? View.VISIBLE : View.GONE); mCcView.setText(cc); mAttachmentIcon.setVisibility(((LocalMessage) message).hasAttachments() ? View.VISIBLE : View.GONE); mFlagged.setChecked(message.isSet(Flag.FLAGGED)); mSubjectView.setTextColor(0xff000000 | defaultSubjectColor); chip.setBackgroundColor(mAccount.getChipColor()); chip.getBackground().setAlpha(!message.isSet(Flag.SEEN) ? 255 : 127); if (message.isSet(Flag.ANSWERED)) { mSubjectView.setCompoundDrawablesWithIntrinsicBounds(answeredIcon, null, null, null); } else { mSubjectView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); } if (mMessage.isSet(Flag.X_DOWNLOADED_FULL)) { mDownloadRemainder.setVisibility(View.GONE); } else { mDownloadRemainder.setEnabled(true); mDownloadRemainder.setVisibility(View.VISIBLE); } } }); } public void networkError() { runOnUiThread(new Runnable() { public void run() { Toast.makeText(MessageView.this, R.string.status_network_error, Toast.LENGTH_LONG).show(); } }); } public void invalidIdError() { runOnUiThread(new Runnable() { public void run() { Toast.makeText(MessageView.this, R.string.status_invalid_id_error, Toast.LENGTH_LONG).show(); } }); } public void attachmentSaved(final String filename) { runOnUiThread(new Runnable() { public void run() { Toast.makeText(MessageView.this, String.format( getString(R.string.message_view_status_attachment_saved), filename), Toast.LENGTH_LONG).show(); } }); } public void attachmentNotSaved() { runOnUiThread(new Runnable() { public void run() { Toast.makeText(MessageView.this, getString(R.string.message_view_status_attachment_not_saved), Toast.LENGTH_LONG).show(); } }); } public void fetchingAttachment() { runOnUiThread(new Runnable() { public void run() { Toast.makeText(MessageView.this, getString(R.string.message_view_fetching_attachment_toast), Toast.LENGTH_SHORT).show(); } }); } public void showShowPictures(final boolean show) { runOnUiThread(new Runnable() { public void run() { mShowPicturesSection.setVisibility(show ? View.VISIBLE : View.GONE); } }); } private void showHeaderContainer() { { runOnUiThread(new Runnable() { public void run() { mHeaderContainer.setVisibility(View.VISIBLE); } }); } } private void hideHeaderContainer() { { runOnUiThread(new Runnable() { public void run() { mHeaderContainer.setVisibility(View.GONE); } }); } } /** * Clear the text field for the additional headers display if they are * not shown, to save UI resources. */ public void hideAdditionalHeaders() { runOnUiThread(new Runnable() { public void run() { mAdditionalHeadersView.setVisibility(View.GONE); mAdditionalHeadersView.setText(""); mTopView.scrollTo(0, 0); } }); } /** * Set up and then show the additional headers view. Called by * {@link #onShowAdditionalHeaders()} and * {@link #setHeaders(Account, String, String, Message)} * (when switching between messages). */ public void showAdditionalHeaders() { runOnUiThread(new Runnable() { public void run() { Integer messageToShow = null; try { // Retrieve additional headers boolean allHeadersDownloaded = mMessage.isSet(Flag.X_GOT_ALL_HEADERS); List additionalHeaders = getAdditionalHeaders(mMessage); if (!additionalHeaders.isEmpty()) { // Show the additional headers that we have got. setupAdditionalHeadersView(additionalHeaders); mAdditionalHeadersView.setVisibility(View.VISIBLE); } if (!allHeadersDownloaded) { /* * Tell the user about the "save all headers" setting * * NOTE: This is only a temporary solution... in fact, * the system should download headers on-demand when they * have not been saved in their entirety initially. */ messageToShow = R.string.message_additional_headers_not_downloaded; } else if (additionalHeaders.isEmpty()) { // All headers have been downloaded, but there are no additional headers. messageToShow = R.string.message_no_additional_headers_available; } } catch (MessagingException e) { messageToShow = R.string.message_additional_headers_retrieval_failed; } // Show a message to the user, if any if (messageToShow != null) { Toast toast = Toast.makeText(MessageView.this, messageToShow, Toast.LENGTH_LONG); toast.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL, 0, 0); toast.show(); } } }); } /** * Set up the additional headers text view with the supplied header data. * * @param additionalHeaders * List of header entries. Each entry consists of a header * name and a header value. Header names may appear multiple * times. * * This method is always called from within the UI thread by * {@link #showAdditionalHeaders()}. */ private void setupAdditionalHeadersView(final List additionalHeaders) { SpannableStringBuilder sb = new SpannableStringBuilder(); boolean first = true; for (HeaderEntry additionalHeader : additionalHeaders) { if (!first) { sb.append("\n"); } else { first = false; } StyleSpan boldSpan = new StyleSpan(Typeface.BOLD); SpannableString label = new SpannableString(additionalHeader.label + ": "); label.setSpan(boldSpan, 0, label.length(), 0); sb.append(label); sb.append(MimeUtility.unfoldAndDecode(additionalHeader.value)); } mAdditionalHeadersView.setText(sb); } } static class Attachment { public String name; public String contentType; public long size; public LocalAttachmentBodyPart part; public Button viewButton; public Button downloadButton; public ImageView iconView; } public static void actionView(Context context, MessageReference messRef, List messReferences) { actionView(context, messRef, messReferences, null); } public static void actionView(Context context, MessageReference messRef, List messReferences, Bundle extras) { Intent i = new Intent(context, MessageView.class); i.putExtra(EXTRA_MESSAGE_REFERENCE, messRef); i.putExtra(EXTRA_MESSAGE_REFERENCES, (Serializable) messReferences); if (extras != null) { i.putExtras(extras); } context.startActivity(i); } @Override public void onCreate(Bundle icicle) { super.onCreate(icicle, false); mContacts = Contacts.getInstance(this); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.message_view); mHeaderContainer = (LinearLayout)findViewById(R.id.header_container); mFromView = (TextView)findViewById(R.id.from); mToView = (TextView)findViewById(R.id.to); mCcView = (TextView)findViewById(R.id.cc); mToContainerView = (LinearLayout)findViewById(R.id.to_container); mCcContainerView = (LinearLayout)findViewById(R.id.cc_container); mSubjectView = (TextView)findViewById(R.id.subject); defaultSubjectColor = mSubjectView.getCurrentTextColor(); mAdditionalHeadersView = (TextView)findViewById(R.id.additional_headers_view); chip = findViewById(R.id.chip); mDateView = (TextView)findViewById(R.id.date); mTimeView = (TextView)findViewById(R.id.time); mTopView = mToggleScrollView = (ToggleScrollView)findViewById(R.id.top_view); mMessageContentView = (MessageWebView)findViewById(R.id.message_content); mAccessibleMessageContentView = (AccessibleWebView) findViewById(R.id.accessible_message_content); mScreenReaderEnabled = isScreenReaderActive(); answeredIcon = getResources().getDrawable(R.drawable.ic_mms_answered_small); if (mScreenReaderEnabled) { mAccessibleMessageContentView.setVisibility(View.VISIBLE); mMessageContentView.setVisibility(View.GONE); } else { mAccessibleMessageContentView.setVisibility(View.GONE); mMessageContentView.setVisibility(View.VISIBLE); } mDecryptLayout = (View)findViewById(R.id.layout_decrypt); mDecryptButton = (Button)findViewById(R.id.btn_decrypt); mDecryptButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { try { String data = null; Part part = MimeUtility.findFirstPartByMimeType(mMessage, "text/plain"); if (part == null) { part = MimeUtility.findFirstPartByMimeType(mMessage, "text/html"); } if (part != null) { data = MimeUtility.getTextFromPart(part); } mAccount.getCryptoProvider().decrypt(MessageView.this, data, mPgpData); } catch (MessagingException me) { Log.e(K9.LOG_TAG, "Unable to decrypt email.", me); } } }); mCryptoSignatureLayout = (LinearLayout) findViewById(R.id.crypto_signature); mCryptoSignatureStatusImage = (ImageView) findViewById(R.id.ic_crypto_signature_status); mCryptoSignatureUserId = (TextView) findViewById(R.id.userId); mCryptoSignatureUserIdRest = (TextView) findViewById(R.id.userIdRest); mCryptoSignatureLayout.setVisibility(View.INVISIBLE); mAttachments = (LinearLayout)findViewById(R.id.attachments); mAttachmentIcon = findViewById(R.id.attachment); mShowPicturesSection = findViewById(R.id.show_pictures_section); mShowPictures = false; mDownloadRemainder = (Button)findViewById(R.id.download_remainder); mFlagged = (CheckBox)findViewById(R.id.flagged); mFlagged.setOnClickListener(new OnClickListener() { public void onClick(View v) { onFlag(); } }); mMessageContentView.configure(); mFromView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mFontSizes.getMessageViewSender()); mToView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mFontSizes.getMessageViewTo()); ((TextView)findViewById(R.id.to_label)).setTextSize(TypedValue.COMPLEX_UNIT_DIP, mFontSizes.getMessageViewTo()); mCcView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mFontSizes.getMessageViewCC()); ((TextView)findViewById(R.id.cc_label)).setTextSize(TypedValue.COMPLEX_UNIT_DIP, mFontSizes.getMessageViewCC()); mSubjectView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mFontSizes.getMessageViewSubject()); mTimeView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mFontSizes.getMessageViewTime()); mDateView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mFontSizes.getMessageViewDate()); mAdditionalHeadersView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mFontSizes.getMessageViewAdditionalHeaders()); mAdditionalHeadersView.setVisibility(View.GONE); mAttachments.setVisibility(View.GONE); mAttachmentIcon.setVisibility(View.GONE); setOnClickListener(R.id.from); setOnClickListener(R.id.reply); setOnClickListener(R.id.reply_all); setOnClickListener(R.id.delete); setOnClickListener(R.id.forward); setOnClickListener(R.id.next); setOnClickListener(R.id.previous); setOnClickListener(R.id.archive); setOnClickListener(R.id.move); setOnClickListener(R.id.spam); // To show full header setOnClickListener(R.id.header_container); setOnClickListener(R.id.reply_scrolling); // setOnClickListener(R.id.reply_all_scrolling); setOnClickListener(R.id.delete_scrolling); setOnClickListener(R.id.forward_scrolling); setOnClickListener(R.id.next_scrolling); setOnClickListener(R.id.previous_scrolling); setOnClickListener(R.id.archive_scrolling); setOnClickListener(R.id.move_scrolling); setOnClickListener(R.id.spam_scrolling); setOnClickListener(R.id.show_pictures); setOnClickListener(R.id.download_remainder); setTitle(""); Intent intent = getIntent(); Uri uri = intent.getData(); if (icicle != null) { mMessageReference = (MessageReference)icicle.getSerializable(EXTRA_MESSAGE_REFERENCE); mMessageReferences = (ArrayList)icicle.getSerializable(EXTRA_MESSAGE_REFERENCES); mPgpData = (PgpData) icicle.getSerializable(STATE_PGP_DATA); updateDecryptLayout(); } else { if (uri == null) { mMessageReference = (MessageReference)intent.getSerializableExtra(EXTRA_MESSAGE_REFERENCE); mMessageReferences = (ArrayList)intent.getSerializableExtra(EXTRA_MESSAGE_REFERENCES); } else { List segmentList = uri.getPathSegments(); if (segmentList.size() == 3) { String accountId = segmentList.get(0); Collection accounts = Preferences.getPreferences(this).getAvailableAccounts(); boolean found = false; for (Account account : accounts) { if (String.valueOf(account.getAccountNumber()).equals(accountId)) { mAccount = account; found = true; break; } } if (!found) { //TODO: Use ressource to externalize message Toast.makeText(this, "Invalid account id: " + accountId, Toast.LENGTH_LONG).show(); return; } mMessageReference = new MessageReference(); mMessageReference.accountUuid = mAccount.getUuid(); mMessageReference.folderName = segmentList.get(1); mMessageReference.uid = segmentList.get(2); mMessageReferences = new ArrayList(); } else { //TODO: Use ressource to externalize message Toast.makeText(this, "Invalid intent uri: " + uri.toString(), Toast.LENGTH_LONG).show(); return; } } } if (K9.DEBUG) Log.d(K9.LOG_TAG, "MessageView got message " + mMessageReference); if (intent.getBooleanExtra(EXTRA_NEXT, false)) { next.requestFocus(); } // Perhaps the ScrollButtons should be global, instead of account-specific mAccount = Preferences.getPreferences(this).getAccount(mMessageReference.accountUuid); Account.ScrollButtons scrollButtons = mAccount.getScrollMessageViewButtons(); if ((Account.ScrollButtons.ALWAYS == scrollButtons) || (Account.ScrollButtons.KEYBOARD_AVAILABLE == scrollButtons && (this.getResources().getConfiguration().hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO))) { scrollButtons(); } else // never or the keyboard is open { staticButtons(); } Account.ScrollButtons scrollMoveButtons = mAccount.getScrollMessageViewMoveButtons(); if ((Account.ScrollButtons.ALWAYS == scrollMoveButtons) || (Account.ScrollButtons.KEYBOARD_AVAILABLE == scrollMoveButtons && (this.getResources().getConfiguration().hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO))) { scrollMoveButtons(); } else { staticMoveButtons(); } if (!mAccount.getEnableMoveButtons()) { View buttons = findViewById(R.id.move_buttons); if (buttons != null) { buttons.setVisibility(View.GONE); } buttons = findViewById(R.id.scrolling_move_buttons); if (buttons != null) { buttons.setVisibility(View.GONE); } } displayMessage(mMessageReference); } private boolean isScreenReaderActive() { final String SCREENREADER_INTENT_ACTION = "android.accessibilityservice.AccessibilityService"; final String SCREENREADER_INTENT_CATEGORY = "android.accessibilityservice.category.FEEDBACK_SPOKEN"; // Restrict the set of intents to only accessibility services that have // the category FEEDBACK_SPOKEN (aka, screen readers). Intent screenReaderIntent = new Intent(SCREENREADER_INTENT_ACTION); screenReaderIntent.addCategory(SCREENREADER_INTENT_CATEGORY); List screenReaders = getPackageManager().queryIntentServices( screenReaderIntent, 0); ContentResolver cr = getContentResolver(); Cursor cursor = null; int status = 0; for (ResolveInfo screenReader : screenReaders) { // All screen readers are expected to implement a content provider // that responds to // content://.providers.StatusProvider cursor = cr.query(Uri.parse("content://" + screenReader.serviceInfo.packageName + ".providers.StatusProvider"), null, null, null, null); if (cursor != null) { cursor.moveToFirst(); // These content providers use a special cursor that only has // one element, // an integer that is 1 if the screen reader is running. status = cursor.getInt(0); cursor.close(); if (status == 1) { return true; } } } return false; } @Override protected void onSaveInstanceState(Bundle outState) { outState.putSerializable(EXTRA_MESSAGE_REFERENCE, mMessageReference); outState.putSerializable(EXTRA_MESSAGE_REFERENCES, mMessageReferences); outState.putSerializable(STATE_PGP_DATA, mPgpData); outState.putBoolean(SHOW_PICTURES, mShowPictures); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); mShowPictures = savedInstanceState.getBoolean(SHOW_PICTURES); setLoadPictures(mShowPictures); mPgpData = (PgpData) savedInstanceState.getSerializable(STATE_PGP_DATA); initializeCrypto(); updateDecryptLayout(); } private void displayMessage(MessageReference ref) { mMessageReference = ref; if (K9.DEBUG) Log.d(K9.LOG_TAG, "MessageView displaying message " + mMessageReference); mAccount = Preferences.getPreferences(this).getAccount(mMessageReference.accountUuid); mTopView.setVisibility(View.GONE); mTopView.scrollTo(0, 0); mMessageContentView.scrollTo(0, 0); mHandler.hideHeaderContainer(); mMessageContentView.clearView(); setLoadPictures(false); mAttachments.removeAllViews(); findSurroundingMessagesUid(); // start with fresh, empty PGP data mPgpData = null; initializeCrypto(); mTopView.setVisibility(View.VISIBLE); MessagingController.getInstance(getApplication()).loadMessageForView( mAccount, mMessageReference.folderName, mMessageReference.uid, mListener); setupDisplayMessageButtons(); } private void setupDisplayMessageButtons() { mDelete.setEnabled(true); next.setEnabled(mNextMessage != null); previous.setEnabled(mPreviousMessage != null); // If moving isn't support at all, then all of them must be disabled anyway. if (MessagingController.getInstance(getApplication()).isMoveCapable(mAccount)) { // Only enable the button if the Archive folder is not the current folder and not NONE. mArchive.setEnabled(!mMessageReference.folderName.equals(mAccount.getArchiveFolderName()) && !K9.FOLDER_NONE.equalsIgnoreCase(mAccount.getArchiveFolderName())); // Only enable the button if the Spam folder is not the current folder and not NONE. mSpam.setEnabled(!mMessageReference.folderName.equals(mAccount.getSpamFolderName()) && !K9.FOLDER_NONE.equalsIgnoreCase(mAccount.getSpamFolderName())); mMove.setEnabled(true); } else { disableMoveButtons(); } } private void staticButtons() { View buttons = findViewById(R.id.scrolling_buttons); if (buttons != null) { buttons.setVisibility(View.GONE); } next = findViewById(R.id.next); previous = findViewById(R.id.previous); mDelete = findViewById(R.id.delete); } private void scrollButtons() { View buttons = findViewById(R.id.bottom_buttons); if (buttons != null) { buttons.setVisibility(View.GONE); } next = findViewById(R.id.next_scrolling); previous = findViewById(R.id.previous_scrolling); mDelete = findViewById(R.id.delete_scrolling); } private void staticMoveButtons() { View buttons = findViewById(R.id.scrolling_move_buttons); if (buttons != null) { buttons.setVisibility(View.GONE); } mArchive = findViewById(R.id.archive); mMove = findViewById(R.id.move); mSpam = findViewById(R.id.spam); } private void scrollMoveButtons() { View buttons = findViewById(R.id.move_buttons); if (buttons != null) { buttons.setVisibility(View.GONE); } mArchive = findViewById(R.id.archive_scrolling); mMove = findViewById(R.id.move_scrolling); mSpam = findViewById(R.id.spam_scrolling); } private void disableButtons() { setLoadPictures(false); disableMoveButtons(); next.setEnabled(false); previous.setEnabled(false); mDelete.setEnabled(false); } private void disableMoveButtons() { mArchive.setEnabled(false); mMove.setEnabled(false); mSpam.setEnabled(false); } private void setOnClickListener(int viewCode) { View thisView = findViewById(viewCode); if (thisView != null) { thisView.setOnClickListener(this); } } private void findSurroundingMessagesUid() { mNextMessage = mPreviousMessage = null; int i = mMessageReferences.indexOf(mMessageReference); if (i < 0) return; if (i != 0) mNextMessage = mMessageReferences.get(i - 1); if (i != (mMessageReferences.size() - 1)) mPreviousMessage = mMessageReferences.get(i + 1); } @Override public void onResume() { super.onResume(); if (!mAccount.isAvailable(this)) { onAccountUnavailable(); return; } StorageManager.getInstance(getApplication()).addListener(mStorageListener); } @Override protected void onPause() { StorageManager.getInstance(getApplication()).removeListener(mStorageListener); super.onPause(); } protected void onAccountUnavailable() { finish(); // TODO inform user about account unavailability using Toast Accounts.listAccounts(this); } /** * Called from UI thread when user select Delete */ private void onDelete() { if (K9.confirmDelete()) { showDialog(R.id.dialog_confirm_delete); } else { delete(); } } /** * @param id * @return Never null */ protected Dialog createConfirmDeleteDialog(final int id) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.dialog_confirm_delete_title); builder.setMessage(R.string.dialog_confirm_delete_message); builder.setPositiveButton(R.string.dialog_confirm_delete_confirm_button, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dismissDialog(id); delete(); } }); builder.setNegativeButton(R.string.dialog_confirm_delete_cancel_button, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dismissDialog(id); } }); return builder.create(); } private void delete() { if (mMessage != null) { // Disable the delete button after it's tapped (to try to prevent // accidental clicks) disableButtons(); Message messageToDelete = mMessage; showNextMessageOrReturn(); MessagingController.getInstance(getApplication()).deleteMessages( new Message[]{messageToDelete}, null); } } private void onRefile(String dstFolder) { if (!MessagingController.getInstance(getApplication()).isMoveCapable(mAccount)) { return; } if (!MessagingController.getInstance(getApplication()).isMoveCapable(mMessage)) { Toast toast = Toast.makeText(this, R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG); toast.show(); return; } String srcFolder = mMessageReference.folderName; Message messageToMove = mMessage; if (K9.FOLDER_NONE.equalsIgnoreCase(dstFolder)) { return; } showNextMessageOrReturn(); MessagingController.getInstance(getApplication()) .moveMessage(mAccount, srcFolder, messageToMove, dstFolder, null); } private void showNextMessageOrReturn() { if (K9.messageViewReturnToList()) { finish(); } else { showNextMessage(); } } private void showNextMessage() { findSurroundingMessagesUid(); mMessageReferences.remove(mMessageReference); if (mLastDirection == NEXT && mNextMessage != null) { onNext(); } else if (mLastDirection == PREVIOUS && mPreviousMessage != null) { onPrevious(); } else if (mNextMessage != null) { onNext(); } else if (mPreviousMessage != null) { onPrevious(); } else { finish(); } } private void onClickSender() { if (mMessage != null) { try { final Address senderEmail = mMessage.getFrom()[0]; mContacts.createContact(this, senderEmail); } catch (Exception e) { Log.e(K9.LOG_TAG, "Couldn't create contact", e); } } } private void onReply() { if (mMessage != null) { MessageCompose.actionReply(this, mAccount, mMessage, false, mPgpData.getDecryptedData()); finish(); } } private void onReplyAll() { if (mMessage != null) { MessageCompose.actionReply(this, mAccount, mMessage, true, mPgpData.getDecryptedData()); finish(); } } private void onForward() { if (mMessage != null) { MessageCompose.actionForward(this, mAccount, mMessage, mPgpData.getDecryptedData()); finish(); } } private void onFlag() { if (mMessage != null) { MessagingController.getInstance(getApplication()).setFlag(mAccount, mMessage.getFolder().getName(), new String[]{mMessage.getUid()}, Flag.FLAGGED, !mMessage.isSet(Flag.FLAGGED)); try { mMessage.setFlag(Flag.FLAGGED, !mMessage.isSet(Flag.FLAGGED)); setHeaders(mMessage.getFolder().getName(), mMessage.getUid(), mMessage); prepareMenuItems(); } catch (MessagingException me) { Log.e(K9.LOG_TAG, "Could not set flag on local message", me); } } } private void onMove() { if ((!MessagingController.getInstance(getApplication()).isMoveCapable(mAccount)) || (mMessage == null)) { return; } if (!MessagingController.getInstance(getApplication()).isMoveCapable(mMessage)) { Toast toast = Toast.makeText(this, R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG); toast.show(); return; } startRefileActivity(ACTIVITY_CHOOSE_FOLDER_MOVE); } private void onCopy() { if ((!MessagingController.getInstance(getApplication()).isCopyCapable(mAccount)) || (mMessage == null)) { return; } if (!MessagingController.getInstance(getApplication()).isCopyCapable(mMessage)) { Toast toast = Toast.makeText(this, R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG); toast.show(); return; } startRefileActivity(ACTIVITY_CHOOSE_FOLDER_COPY); } private void startRefileActivity(int activity) { Intent intent = new Intent(this, ChooseFolder.class); intent.putExtra(ChooseFolder.EXTRA_ACCOUNT, mAccount.getUuid()); intent.putExtra(ChooseFolder.EXTRA_CUR_FOLDER, mMessageReference.folderName); intent.putExtra(ChooseFolder.EXTRA_SEL_FOLDER, mAccount.getLastSelectedFolderName()); intent.putExtra(ChooseFolder.EXTRA_MESSAGE, mMessageReference); startActivityForResult(intent, activity); } private void onShowAdditionalHeaders() { int currentVisibility = mAdditionalHeadersView.getVisibility(); if (currentVisibility == View.VISIBLE) { mHandler.hideAdditionalHeaders(); } else { mHandler.showAdditionalHeaders(); } } private List getAdditionalHeaders(final Message message) throws MessagingException { List additionalHeaders = new LinkedList(); /* * Remove "Subject" header as it is already shown in the standard * message view header. But do show "From", "To", and "Cc" again. * This time including the email addresses. See issue 1805. */ Set headerNames = new HashSet(message.getHeaderNames()); headerNames.remove("Subject"); for (String headerName : headerNames) { String[] headerValues = message.getHeader(headerName); for (String headerValue : headerValues) { additionalHeaders.add(new HeaderEntry(headerName, headerValue)); } } return additionalHeaders; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (mAccount.getCryptoProvider().onActivityResult(this, requestCode, resultCode, data, mPgpData)) { return; } if (resultCode != RESULT_OK) return; switch (requestCode) { case ACTIVITY_CHOOSE_FOLDER_MOVE: case ACTIVITY_CHOOSE_FOLDER_COPY: if (data == null) return; String destFolderName = data.getStringExtra(ChooseFolder.EXTRA_NEW_FOLDER); String srcFolderName = data.getStringExtra(ChooseFolder.EXTRA_CUR_FOLDER); MessageReference ref = (MessageReference) data.getSerializableExtra(ChooseFolder.EXTRA_MESSAGE); if (mMessageReference.equals(ref)) { mAccount.setLastSelectedFolderName(destFolderName); switch (requestCode) { case ACTIVITY_CHOOSE_FOLDER_MOVE: Message messageToMove = mMessage; showNextMessageOrReturn(); MessagingController.getInstance(getApplication()).moveMessage(mAccount, srcFolderName, messageToMove, destFolderName, null); break; case ACTIVITY_CHOOSE_FOLDER_COPY: MessagingController.getInstance(getApplication()).copyMessage(mAccount, srcFolderName, mMessage, destFolderName, null); break; } } break; } } private void onSendAlternate() { if (mMessage != null) { MessagingController.getInstance(getApplication()).sendAlternate(this, mAccount, mMessage); } } @Override protected void onNext() { if (mNextMessage == null) { Toast.makeText(this, getString(R.string.end_of_folder), Toast.LENGTH_SHORT).show(); return; } mLastDirection = NEXT; disableButtons(); if (K9.showAnimations()) { mTopView.startAnimation(outToLeftAnimation()); } displayMessage(mNextMessage); next.requestFocus(); } @Override protected void onPrevious() { if (mPreviousMessage == null) { Toast.makeText(this, getString(R.string.end_of_folder), Toast.LENGTH_SHORT).show(); return; } mLastDirection = PREVIOUS; disableButtons(); if (K9.showAnimations()) { mTopView.startAnimation(inFromRightAnimation()); } displayMessage(mPreviousMessage); previous.requestFocus(); } private void onMarkAsUnread() { if (mMessage != null) { MessagingController.getInstance(getApplication()).setFlag( mAccount, mMessageReference.folderName, new String[] { mMessage.getUid() }, Flag.SEEN, false); try { mMessage.setFlag(Flag.SEEN, false); setHeaders(mMessage.getFolder().getName(), mMessage.getUid(), mMessage); } catch (Exception e) { Log.e(K9.LOG_TAG, "Unable to unset SEEN flag on message", e); } } } /** * Creates a unique file in the given directory by appending a hyphen * and a number to the given filename. * * @param directory * @param filename * @return */ private File createUniqueFile(File directory, String filename) { File file = new File(directory, filename); if (!file.exists()) { return file; } // Get the extension of the file, if any. int index = filename.lastIndexOf('.'); String format; if (index != -1) { String name = filename.substring(0, index); String extension = filename.substring(index); format = name + "-%d" + extension; } else { format = filename + "-%d"; } for (int i = 2; i < Integer.MAX_VALUE; i++) { file = new File(directory, String.format(format, i)); if (!file.exists()) { return file; } } return null; } private void onDownloadRemainder() { if (mMessage.isSet(Flag.X_DOWNLOADED_FULL)) { return; } mDownloadRemainder.setEnabled(false); MessagingController.getInstance(getApplication()).loadMessageForViewRemote( mAccount, mMessageReference.folderName, mMessageReference.uid, mListener); } private void onDownloadAttachment(Attachment attachment) { if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { /* * Abort early if there's no place to save the attachment. We don't want to spend * the time downloading it and then abort. */ Toast.makeText(this, getString(R.string.message_view_status_attachment_not_saved), Toast.LENGTH_SHORT).show(); return; } if (mMessage != null) { MessagingController.getInstance(getApplication()).loadAttachment( mAccount, mMessage, attachment.part, new Object[] { true, attachment }, mListener); } } private void onViewAttachment(Attachment attachment) { if (mMessage != null) { MessagingController.getInstance(getApplication()).loadAttachment( mAccount, mMessage, attachment.part, new Object[] { false, attachment }, mListener); } } private void onShowPictures() { // TODO: Download attachments that are used as inline image setLoadPictures(true); } /** * Enable/disable image loading of the WebView. But always hide the * "Show pictures" button! * * @param enable true, if (network) images should be loaded. * false, otherwise. */ private void setLoadPictures(boolean enable) { mMessageContentView.blockNetworkData(!enable); mShowPictures = enable; mHandler.showShowPictures(false); } public void onClick(View view) { switch (view.getId()) { case R.id.from: onClickSender(); break; case R.id.reply: case R.id.reply_scrolling: onReply(); break; case R.id.reply_all: onReplyAll(); break; case R.id.delete: case R.id.delete_scrolling: onDelete(); break; case R.id.forward: case R.id.forward_scrolling: onForward(); break; case R.id.archive: case R.id.archive_scrolling: onRefile(mAccount.getArchiveFolderName()); break; case R.id.spam: case R.id.spam_scrolling: onRefile(mAccount.getSpamFolderName()); break; case R.id.move: case R.id.move_scrolling: onMove(); break; case R.id.next: case R.id.next_scrolling: onNext(); break; case R.id.previous: case R.id.previous_scrolling: onPrevious(); break; case R.id.download: onDownloadAttachment((Attachment) view.getTag()); break; case R.id.view: onViewAttachment((Attachment) view.getTag()); break; case R.id.show_pictures: onShowPictures(); break; case R.id.header_container: onShowAdditionalHeaders(); break; case R.id.download_remainder: onDownloadRemainder(); break; } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.delete: onDelete(); break; case R.id.reply: onReply(); break; case R.id.reply_all: onReplyAll(); break; case R.id.forward: onForward(); break; case R.id.send_alternate: onSendAlternate(); break; case R.id.mark_as_unread: onMarkAsUnread(); break; case R.id.flag: onFlag(); break; case R.id.archive: onRefile(mAccount.getArchiveFolderName()); break; case R.id.spam: onRefile(mAccount.getSpamFolderName()); break; case R.id.move: onMove(); break; case R.id.copy: onCopy(); break; case R.id.show_full_header: onShowAdditionalHeaders(); break; case R.id.select_text: mToggleScrollView.setScrolling(false); mMessageContentView.emulateShiftHeld(); break; default: return super.onOptionsItemSelected(item); } return true; } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.message_view_option, menu); optionsMenu = menu; prepareMenuItems(); if (!MessagingController.getInstance(getApplication()).isCopyCapable(mAccount)) { menu.findItem(R.id.copy).setVisible(false); } if (!MessagingController.getInstance(getApplication()).isMoveCapable(mAccount)) { menu.findItem(R.id.move).setVisible(false); menu.findItem(R.id.archive).setVisible(false); menu.findItem(R.id.spam).setVisible(false); } if (K9.FOLDER_NONE.equalsIgnoreCase(mAccount.getArchiveFolderName())) { menu.findItem(R.id.archive).setVisible(false); } if (K9.FOLDER_NONE.equalsIgnoreCase(mAccount.getSpamFolderName())) { menu.findItem(R.id.spam).setVisible(false); } return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { prepareMenuItems(); return super.onPrepareOptionsMenu(menu); } // TODO: when switching to API version 8, override onCreateDialog(int, Bundle) /** * @param id * The id of the dialog. * @return The dialog. If you return null, the dialog will not be created. * @see android.app.Activity#onCreateDialog(int) */ @Override protected Dialog onCreateDialog(final int id) { switch (id) { case R.id.dialog_confirm_delete: { return createConfirmDeleteDialog(id); } } return super.onCreateDialog(id); } private void prepareMenuItems() { Menu menu = optionsMenu; if (menu != null) { MenuItem flagItem = menu.findItem(R.id.flag); if (flagItem != null && mMessage != null) { flagItem.setTitle((mMessage.isSet(Flag.FLAGGED) ? R.string.unflag_action : R.string.flag_action)); } MenuItem additionalHeadersItem = menu.findItem(R.id.show_full_header); if (additionalHeadersItem != null) { additionalHeadersItem.setTitle((mAdditionalHeadersView.getVisibility() == View.VISIBLE) ? R.string.hide_full_header_action : R.string.show_full_header_action); } } } private Bitmap getPreviewIcon(Attachment attachment) { try { return BitmapFactory.decodeStream( getContentResolver().openInputStream( AttachmentProvider.getAttachmentThumbnailUri(mAccount, attachment.part.getAttachmentId(), 62, 62))); } catch (Exception e) { /* * We don't care what happened, we just return null for the preview icon. */ return null; } } private void renderAttachments(Part part, int depth) throws MessagingException { String contentType = MimeUtility.unfoldAndDecode(part.getContentType()); String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition()); String name = MimeUtility.getHeaderParameter(contentType, "name"); // Inline parts with a content-id are almost certainly components of an HTML message // not attachments. Don't show attachment download buttons for them. if (contentDisposition != null && MimeUtility.getHeaderParameter(contentDisposition, null).matches("^(?i:inline)") && part.getHeader("Content-ID") != null) { return; } if (name == null) { name = MimeUtility.getHeaderParameter(contentDisposition, "filename"); } if (name != null) { /* * We're guaranteed size because LocalStore.fetch puts it there. */ int size = Integer.parseInt(MimeUtility.getHeaderParameter(contentDisposition, "size")); Attachment attachment = new Attachment(); attachment.size = size; String mimeType = part.getMimeType(); if (MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE.equals(mimeType)) { mimeType = MimeUtility.getMimeTypeByExtension(name); } attachment.contentType = mimeType; attachment.name = name; attachment.part = (LocalAttachmentBodyPart) part; LayoutInflater inflater = getLayoutInflater(); View view = inflater.inflate(R.layout.message_view_attachment, null); TextView attachmentName = (TextView)view.findViewById(R.id.attachment_name); TextView attachmentInfo = (TextView)view.findViewById(R.id.attachment_info); ImageView attachmentIcon = (ImageView)view.findViewById(R.id.attachment_icon); Button attachmentView = (Button)view.findViewById(R.id.view); Button attachmentDownload = (Button)view.findViewById(R.id.download); if ((!MimeUtility.mimeTypeMatches(attachment.contentType, K9.ACCEPTABLE_ATTACHMENT_VIEW_TYPES)) || (MimeUtility.mimeTypeMatches(attachment.contentType, K9.UNACCEPTABLE_ATTACHMENT_VIEW_TYPES))) { attachmentView.setVisibility(View.GONE); } if ((!MimeUtility.mimeTypeMatches(attachment.contentType, K9.ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES)) || (MimeUtility.mimeTypeMatches(attachment.contentType, K9.UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))) { attachmentDownload.setVisibility(View.GONE); } if (attachment.size > K9.MAX_ATTACHMENT_DOWNLOAD_SIZE) { attachmentView.setVisibility(View.GONE); attachmentDownload.setVisibility(View.GONE); } attachment.viewButton = attachmentView; attachment.downloadButton = attachmentDownload; attachment.iconView = attachmentIcon; view.setTag(attachment); attachmentView.setOnClickListener(this); attachmentView.setTag(attachment); attachmentDownload.setOnClickListener(this); attachmentDownload.setTag(attachment); attachmentName.setText(name); attachmentInfo.setText(SizeFormatter.formatSize(getApplication(),size)); Bitmap previewIcon = getPreviewIcon(attachment); if (previewIcon != null) { attachmentIcon.setImageBitmap(previewIcon); } else { attachmentIcon.setImageResource(R.drawable.attached_image_placeholder); } mHandler.addAttachment(view); } if (part.getBody() instanceof Multipart) { Multipart mp = (Multipart) part.getBody(); for (int i = 0; i < mp.getCount(); i++) { renderAttachments(mp.getBodyPart(i), depth + 1); } } } private void setHeaders(String folder, String uid, final Message message) throws MessagingException { mHandler.setHeaders(message); // Update additional headers display, if visible if (mAdditionalHeadersView.getVisibility() == View.VISIBLE) { mHandler.showAdditionalHeaders(); } } class Listener extends MessagingListener { @Override public void loadMessageForViewHeadersAvailable(Account account, String folder, String uid, final Message message) { if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder) || !mMessageReference.accountUuid.equals(account.getUuid())) { return; } MessageView.this.mMessage = message; if (!message.isSet(Flag.X_DOWNLOADED_FULL) && !message.isSet(Flag.X_DOWNLOADED_PARTIAL)) { mHandler.post(new Runnable() { public void run() { mMessageContentView.loadUrl("file:///android_asset/downloading.html"); updateDecryptLayout(); } }); } try { setHeaders(folder, uid, message); mHandler.showHeaderContainer(); } catch (MessagingException me) { Log.e(K9.LOG_TAG, "loadMessageForViewHeadersAvailable", me); } } @Override public void loadMessageForViewBodyAvailable(Account account, String folder, String uid, Message message) { if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder) || !mMessageReference.accountUuid.equals(account.getUuid())) { return; } try { if (MessageView.this.mMessage!=null && MessageView.this.mMessage.isSet(Flag.X_DOWNLOADED_PARTIAL) && message.isSet(Flag.X_DOWNLOADED_FULL)) { setHeaders(folder, uid, message); mHandler.showHeaderContainer(); } MessageView.this.mMessage = message; mHandler.removeAllAttachments(); String text; String type = "text/html"; if (mPgpData.getDecryptedData() != null) { text = mPgpData.getDecryptedData(); type = "text/plain"; } else { // First try and fetch an HTML part. Part part = MimeUtility.findFirstPartByMimeType(mMessage, "text/html"); if (part == null) { // If that fails, try and get a text part. part = MimeUtility.findFirstPartByMimeType(mMessage, "text/plain"); if (part == null) { text = null; } else { LocalTextBody body = (LocalTextBody)part.getBody(); if (body == null) { text = null; } else { text = body.getBodyForDisplay(); } } } else { // We successfully found an HTML part; do the necessary character set decoding. text = MimeUtility.getTextFromPart(part); } } if (text != null) { final String emailText = text; mHandler.post(new Runnable() { public void run() { mTopView.scrollTo(0, 0); if (mScreenReaderEnabled) { mAccessibleMessageContentView.loadDataWithBaseURL("http://", emailText, "text/html", "utf-8", null); } else { mMessageContentView.loadDataWithBaseURL("http://", emailText, "text/html", "utf-8", null); mMessageContentView.scrollTo(0, 0); } updateDecryptLayout(); } }); // If the message contains external pictures and the "Show pictures" // button wasn't already pressed, see if the user's preferences has us // showing them anyway. if (hasExternalImages(text) && !mShowPictures) { if ((account.getShowPictures() == Account.ShowPictures.ALWAYS) || ((account.getShowPictures() == Account.ShowPictures.ONLY_FROM_CONTACTS) && mContacts.isInContacts(message.getFrom()[0].getAddress()))) { onShowPictures(); } else { mHandler.showShowPictures(true); } } } else { mHandler.post(new Runnable() { public void run() { mMessageContentView.loadUrl("file:///android_asset/empty.html"); updateDecryptLayout(); } }); } renderAttachments(mMessage, 0); } catch (Exception e) { if (Config.LOGV) { Log.v(K9.LOG_TAG, "loadMessageForViewBodyAvailable", e); } } }//loadMessageForViewBodyAvailable private static final String IMG_SRC_REGEX = "(?is:]+src\\s*=\\s*['\"]?([a-z]+)\\:)"; private final Pattern mImgPattern = Pattern.compile(IMG_SRC_REGEX); private boolean hasExternalImages(final String message) { Matcher imgMatches = mImgPattern.matcher(message); while (imgMatches.find()) { if (!imgMatches.group(1).equals("content")) { if (K9.DEBUG) { Log.d(K9.LOG_TAG, "External images found"); } return true; } } if (K9.DEBUG) { Log.d(K9.LOG_TAG, "No external images."); } return false; } @Override public void loadMessageForViewFailed(Account account, String folder, String uid, final Throwable t) { if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder) || !mMessageReference.accountUuid.equals(account.getUuid())) { return; } mHandler.post(new Runnable() { public void run() { setProgressBarIndeterminateVisibility(false); if (t instanceof IllegalArgumentException) { mHandler.invalidIdError(); } else { mHandler.networkError(); } if ((MessageView.this.mMessage == null) || !MessageView.this.mMessage.isSet(Flag.X_DOWNLOADED_PARTIAL)) { mMessageContentView.loadUrl("file:///android_asset/empty.html"); updateDecryptLayout(); } } }); } @Override public void loadMessageForViewFinished(Account account, String folder, String uid, Message message) { if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder) || !mMessageReference.accountUuid.equals(account.getUuid())) { return; } mHandler.post(new Runnable() { public void run() { setProgressBarIndeterminateVisibility(false); } }); } @Override public void loadMessageForViewStarted(Account account, String folder, String uid) { if (!mMessageReference.uid.equals(uid) || !mMessageReference.folderName.equals(folder) || !mMessageReference.accountUuid.equals(account.getUuid())) { return; } mHandler.post(new Runnable() { public void run() { updateDecryptLayout(); setProgressBarIndeterminateVisibility(true); } }); } @Override public void loadAttachmentStarted(Account account, Message message, Part part, Object tag, boolean requiresDownload) { if (mMessage!=message) { return; } mHandler.setAttachmentsEnabled(false); mHandler.progress(true); if (requiresDownload) { mHandler.fetchingAttachment(); } } @Override public void loadAttachmentFinished(Account account, Message message, Part part, Object tag) { if (mMessage!=message) { return; } mHandler.setAttachmentsEnabled(true); mHandler.progress(false); Object[] params = (Object[]) tag; boolean download = (Boolean) params[0]; Attachment attachment = (Attachment) params[1]; if (download) { try { File file = createUniqueFile(Environment.getExternalStorageDirectory(), attachment.name); Uri uri = AttachmentProvider.getAttachmentUri( mAccount, attachment.part.getAttachmentId()); InputStream in = getContentResolver().openInputStream(uri); OutputStream out = new FileOutputStream(file); IOUtils.copy(in, out); out.flush(); out.close(); in.close(); mHandler.attachmentSaved(file.getName()); new MediaScannerNotifier(MessageView.this, file); } catch (IOException ioe) { mHandler.attachmentNotSaved(); } } else { Uri uri = AttachmentProvider.getAttachmentUri( mAccount, attachment.part.getAttachmentId()); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(uri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); try { startActivity(intent); } catch (Exception e) { Log.e(K9.LOG_TAG, "Could not display attachment of type " + attachment.contentType, e); Toast toast = Toast.makeText(MessageView.this, getString(R.string.message_view_no_viewer, attachment.contentType), Toast.LENGTH_LONG); toast.show(); } } } @Override public void loadAttachmentFailed(Account account, Message message, Part part, Object tag, String reason) { if (mMessage!=message) { return; } mHandler.setAttachmentsEnabled(true); mHandler.progress(false); mHandler.networkError(); } } class MediaScannerNotifier implements MediaScannerConnectionClient { private MediaScannerConnection mConnection; private File mFile; public MediaScannerNotifier(Context context, File file) { mFile = file; mConnection = new MediaScannerConnection(context, this); mConnection.connect(); } public void onMediaScannerConnected() { mConnection.scanFile(mFile.getAbsolutePath(), null); } public void onScanCompleted(String path, Uri uri) { try { if (uri != null) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(uri); startActivity(intent); } } finally { mConnection.disconnect(); } } } private void initializeCrypto() { if (mPgpData != null) { return; } if (mAccount == null) { mAccount = Preferences.getPreferences(this).getAccount(mMessageReference.accountUuid); } mPgpData = new PgpData(); } /** * Fill the decrypt layout with signature data, if known, make controls visible, if * they should be visible. */ public void updateDecryptLayout() { if (mPgpData.getSignatureKeyId() != 0) { mCryptoSignatureUserIdRest.setText( getString(R.string.key_id, Long.toHexString(mPgpData.getSignatureKeyId() & 0xffffffffL))); String userId = mPgpData.getSignatureUserId(); if (userId == null) { userId = getString(R.string.unknown_crypto_signature_user_id); } String chunks[] = userId.split(" <", 2); String name = chunks[0]; if (chunks.length > 1) { mCryptoSignatureUserIdRest.setText("<" + chunks[1]); } mCryptoSignatureUserId.setText(name); if (mPgpData.getSignatureSuccess()) { mCryptoSignatureStatusImage.setImageResource(R.drawable.overlay_ok); } else if (mPgpData.getSignatureUnknown()) { mCryptoSignatureStatusImage.setImageResource(R.drawable.overlay_error); } else { mCryptoSignatureStatusImage.setImageResource(R.drawable.overlay_error); } mCryptoSignatureLayout.setVisibility(View.VISIBLE); mDecryptLayout.setVisibility(View.VISIBLE); } else { mCryptoSignatureLayout.setVisibility(View.INVISIBLE); } if (false || ((mMessage == null) && (mPgpData.getDecryptedData() == null))) { mDecryptLayout.setVisibility(View.GONE); return; } if (mPgpData.getDecryptedData() != null) { if (mPgpData.getSignatureKeyId() == 0) { mDecryptLayout.setVisibility(View.GONE); } else { // no need to show this after decryption/verification mDecryptButton.setVisibility(View.GONE); } return; } mDecryptButton.setVisibility(View.VISIBLE); CryptoProvider crypto = mAccount.getCryptoProvider(); if (crypto.isEncrypted(mMessage)) { mDecryptButton.setText(R.string.btn_decrypt); mDecryptLayout.setVisibility(View.VISIBLE); } else if (crypto.isSigned(mMessage)) { mDecryptButton.setText(R.string.btn_verify); mDecryptLayout.setVisibility(View.VISIBLE); } else { mDecryptLayout.setVisibility(View.GONE); try { // check for PGP/MIME encryption Part pgp = MimeUtility.findFirstPartByMimeType(mMessage, "application/pgp-encrypted"); if (pgp != null) { Toast.makeText(this, R.string.pgp_mime_unsupported, Toast.LENGTH_LONG).show(); } } catch (MessagingException e) { // nothing to do... } } } public void onDecryptDone() { // TODO: this might not be enough if the orientation was changed while in APG, // sometimes shows the original encrypted content mMessageContentView.loadDataWithBaseURL("email://", mPgpData.getDecryptedData(), "text/plain", "utf-8", null); updateDecryptLayout(); } }