package com.fsck.k9.ui.messageview; import java.util.HashMap; import java.util.Map; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.AttributeSet; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.View; import android.view.View.OnCreateContextMenuListener; import android.webkit.WebView; import android.webkit.WebView.HitTestResult; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.fsck.k9.R; import com.fsck.k9.helper.ClipboardManager; import com.fsck.k9.helper.Contacts; import com.fsck.k9.message.html.HtmlConverter; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Address; import com.fsck.k9.mailstore.AttachmentResolver; import com.fsck.k9.mailstore.AttachmentViewInfo; import com.fsck.k9.mailstore.MessageViewInfo; import com.fsck.k9.view.MessageHeader.OnLayoutChangedListener; import com.fsck.k9.view.MessageWebView; import com.fsck.k9.view.MessageWebView.OnPageFinishedListener; public class MessageContainerView extends LinearLayout implements OnLayoutChangedListener, OnCreateContextMenuListener { private static final int MENU_ITEM_LINK_VIEW = Menu.FIRST; private static final int MENU_ITEM_LINK_SHARE = Menu.FIRST + 1; private static final int MENU_ITEM_LINK_COPY = Menu.FIRST + 2; private static final int MENU_ITEM_IMAGE_VIEW = Menu.FIRST; private static final int MENU_ITEM_IMAGE_SAVE = Menu.FIRST + 1; private static final int MENU_ITEM_IMAGE_COPY = Menu.FIRST + 2; private static final int MENU_ITEM_PHONE_CALL = Menu.FIRST; private static final int MENU_ITEM_PHONE_SAVE = Menu.FIRST + 1; private static final int MENU_ITEM_PHONE_COPY = Menu.FIRST + 2; private static final int MENU_ITEM_EMAIL_SEND = Menu.FIRST; private static final int MENU_ITEM_EMAIL_SAVE = Menu.FIRST + 1; private static final int MENU_ITEM_EMAIL_COPY = Menu.FIRST + 2; private MessageWebView mMessageContentView; private LinearLayout mAttachments; private View unsignedTextContainer; private View unsignedTextDivider; private TextView unsignedText; private View mAttachmentsContainer; private boolean showingPictures; private LayoutInflater mInflater; private AttachmentViewCallback attachmentCallback; private SavedState mSavedState; private ClipboardManager mClipboardManager; private Map<AttachmentViewInfo, AttachmentView> attachmentViewMap = new HashMap<>(); private Map<Uri, AttachmentViewInfo> attachments = new HashMap<>(); private boolean hasHiddenExternalImages; private String currentHtmlText; private AttachmentResolver currentAttachmentResolver; @Override public void onFinishInflate() { super.onFinishInflate(); mMessageContentView = (MessageWebView) findViewById(R.id.message_content); if (!isInEditMode()) { mMessageContentView.configure(); } mMessageContentView.setOnCreateContextMenuListener(this); mMessageContentView.setVisibility(View.VISIBLE); mAttachmentsContainer = findViewById(R.id.attachments_container); mAttachments = (LinearLayout) findViewById(R.id.attachments); unsignedTextContainer = findViewById(R.id.message_unsigned_container); unsignedTextDivider = findViewById(R.id.message_unsigned_divider); unsignedText = (TextView) findViewById(R.id.message_unsigned_text); showingPictures = false; Context context = getContext(); mInflater = LayoutInflater.from(context); mClipboardManager = ClipboardManager.getInstance(context); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu); WebView webview = (WebView) v; WebView.HitTestResult result = webview.getHitTestResult(); if (result == null) { return; } int type = result.getType(); Context context = getContext(); switch (type) { case HitTestResult.SRC_ANCHOR_TYPE: { final String url = result.getExtra(); OnMenuItemClickListener listener = new OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case MENU_ITEM_LINK_VIEW: { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivityIfAvailable(getContext(), intent); break; } case MENU_ITEM_LINK_SHARE: { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TEXT, url); startActivityIfAvailable(getContext(), intent); break; } case MENU_ITEM_LINK_COPY: { String label = getContext().getString( R.string.webview_contextmenu_link_clipboard_label); mClipboardManager.setText(label, url); break; } } return true; } }; menu.setHeaderTitle(url); menu.add(Menu.NONE, MENU_ITEM_LINK_VIEW, 0, context.getString(R.string.webview_contextmenu_link_view_action)) .setOnMenuItemClickListener(listener); menu.add(Menu.NONE, MENU_ITEM_LINK_SHARE, 1, context.getString(R.string.webview_contextmenu_link_share_action)) .setOnMenuItemClickListener(listener); menu.add(Menu.NONE, MENU_ITEM_LINK_COPY, 2, context.getString(R.string.webview_contextmenu_link_copy_action)) .setOnMenuItemClickListener(listener); break; } case HitTestResult.IMAGE_TYPE: case HitTestResult.SRC_IMAGE_ANCHOR_TYPE: { final Uri uri = Uri.parse(result.getExtra()); if (uri == null) { return; } final AttachmentViewInfo attachmentViewInfo = getAttachmentViewInfoIfCidUri(uri); final boolean inlineImage = attachmentViewInfo != null; OnMenuItemClickListener listener = new OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case MENU_ITEM_IMAGE_VIEW: { if (inlineImage) { attachmentCallback.onViewAttachment(attachmentViewInfo); } else { Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivityIfAvailable(getContext(), intent); } break; } case MENU_ITEM_IMAGE_SAVE: { if (inlineImage) { attachmentCallback.onSaveAttachment(attachmentViewInfo); } else { //TODO: Use download manager new DownloadImageTask(getContext()).execute(uri.toString()); } break; } case MENU_ITEM_IMAGE_COPY: { String label = getContext().getString( R.string.webview_contextmenu_image_clipboard_label); mClipboardManager.setText(label, uri.toString()); break; } } return true; } }; menu.setHeaderTitle(inlineImage ? context.getString(R.string.webview_contextmenu_image_title) : uri.toString()); menu.add(Menu.NONE, MENU_ITEM_IMAGE_VIEW, 0, context.getString(R.string.webview_contextmenu_image_view_action)) .setOnMenuItemClickListener(listener); menu.add(Menu.NONE, MENU_ITEM_IMAGE_SAVE, 1, inlineImage ? context.getString(R.string.webview_contextmenu_image_save_action) : context.getString(R.string.webview_contextmenu_image_download_action)) .setOnMenuItemClickListener(listener); if (!inlineImage) { menu.add(Menu.NONE, MENU_ITEM_IMAGE_COPY, 2, context.getString(R.string.webview_contextmenu_image_copy_action)) .setOnMenuItemClickListener(listener); } break; } case HitTestResult.PHONE_TYPE: { final String phoneNumber = result.getExtra(); OnMenuItemClickListener listener = new OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case MENU_ITEM_PHONE_CALL: { Uri uri = Uri.parse(WebView.SCHEME_TEL + phoneNumber); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivityIfAvailable(getContext(), intent); break; } case MENU_ITEM_PHONE_SAVE: { Contacts contacts = Contacts.getInstance(getContext()); contacts.addPhoneContact(phoneNumber); break; } case MENU_ITEM_PHONE_COPY: { String label = getContext().getString( R.string.webview_contextmenu_phone_clipboard_label); mClipboardManager.setText(label, phoneNumber); break; } } return true; } }; menu.setHeaderTitle(phoneNumber); menu.add(Menu.NONE, MENU_ITEM_PHONE_CALL, 0, context.getString(R.string.webview_contextmenu_phone_call_action)) .setOnMenuItemClickListener(listener); menu.add(Menu.NONE, MENU_ITEM_PHONE_SAVE, 1, context.getString(R.string.webview_contextmenu_phone_save_action)) .setOnMenuItemClickListener(listener); menu.add(Menu.NONE, MENU_ITEM_PHONE_COPY, 2, context.getString(R.string.webview_contextmenu_phone_copy_action)) .setOnMenuItemClickListener(listener); break; } case WebView.HitTestResult.EMAIL_TYPE: { final String email = result.getExtra(); OnMenuItemClickListener listener = new OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case MENU_ITEM_EMAIL_SEND: { Uri uri = Uri.parse(WebView.SCHEME_MAILTO + email); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivityIfAvailable(getContext(), intent); break; } case MENU_ITEM_EMAIL_SAVE: { Contacts contacts = Contacts.getInstance(getContext()); contacts.createContact(new Address(email)); break; } case MENU_ITEM_EMAIL_COPY: { String label = getContext().getString( R.string.webview_contextmenu_email_clipboard_label); mClipboardManager.setText(label, email); break; } } return true; } }; menu.setHeaderTitle(email); menu.add(Menu.NONE, MENU_ITEM_EMAIL_SEND, 0, context.getString(R.string.webview_contextmenu_email_send_action)) .setOnMenuItemClickListener(listener); menu.add(Menu.NONE, MENU_ITEM_EMAIL_SAVE, 1, context.getString(R.string.webview_contextmenu_email_save_action)) .setOnMenuItemClickListener(listener); menu.add(Menu.NONE, MENU_ITEM_EMAIL_COPY, 2, context.getString(R.string.webview_contextmenu_email_copy_action)) .setOnMenuItemClickListener(listener); break; } } } private AttachmentViewInfo getAttachmentViewInfoIfCidUri(Uri uri) { if (!"cid".equals(uri.getScheme())) { return null; } String cid = uri.getSchemeSpecificPart(); Uri internalUri = currentAttachmentResolver.getAttachmentUriForContentId(cid); return attachments.get(internalUri); } private void startActivityIfAvailable(Context context, Intent intent) { try { context.startActivity(intent); } catch (ActivityNotFoundException e) { Toast.makeText(context, R.string.error_activity_not_found, Toast.LENGTH_LONG).show(); } } public MessageContainerView(Context context, AttributeSet attrs) { super(context, attrs); } private boolean isShowingPictures() { return showingPictures; } private void setLoadPictures(boolean enable) { mMessageContentView.blockNetworkData(!enable); showingPictures = enable; } public void showPictures() { setLoadPictures(true); refreshDisplayedContent(); } public void enableAttachmentButtons() { for (AttachmentView attachmentView : attachmentViewMap.values()) { attachmentView.enableButtons(); } } public void disableAttachmentButtons() { for (AttachmentView attachmentView : attachmentViewMap.values()) { attachmentView.disableButtons(); } } public void displayMessageViewContainer(MessageViewInfo messageViewInfo, final OnRenderingFinishedListener onRenderingFinishedListener, boolean automaticallyLoadPictures, boolean hideUnsignedTextDivider, AttachmentViewCallback attachmentCallback) { this.attachmentCallback = attachmentCallback; resetView(); renderAttachments(messageViewInfo); if (mSavedState != null) { if (mSavedState.showingPictures) { setLoadPictures(true); } mSavedState = null; } String textToDisplay = messageViewInfo.text; if (textToDisplay != null && !isShowingPictures()) { if (Utility.hasExternalImages(textToDisplay)) { if (automaticallyLoadPictures) { setLoadPictures(true); } else { hasHiddenExternalImages = true; } } } if (textToDisplay == null) { textToDisplay = HtmlConverter.wrapStatusMessage(getContext().getString(R.string.webview_empty_message)); } OnPageFinishedListener onPageFinishedListener = new OnPageFinishedListener() { @Override public void onPageFinished() { onRenderingFinishedListener.onLoadFinished(); } }; displayHtmlContentWithInlineAttachments( textToDisplay, messageViewInfo.attachmentResolver, onPageFinishedListener); if (!TextUtils.isEmpty(messageViewInfo.extraText)) { unsignedTextContainer.setVisibility(View.VISIBLE); unsignedTextDivider.setVisibility(hideUnsignedTextDivider ? View.GONE : View.VISIBLE); unsignedText.setText(messageViewInfo.extraText); } } public boolean hasHiddenExternalImages() { return hasHiddenExternalImages; } private void displayHtmlContentWithInlineAttachments(String htmlText, AttachmentResolver attachmentResolver, OnPageFinishedListener onPageFinishedListener) { currentHtmlText = htmlText; currentAttachmentResolver = attachmentResolver; mMessageContentView.displayHtmlContentWithInlineAttachments(htmlText, attachmentResolver, onPageFinishedListener); } private void refreshDisplayedContent() { mMessageContentView.displayHtmlContentWithInlineAttachments(currentHtmlText, currentAttachmentResolver, null); } private void clearDisplayedContent() { mMessageContentView.displayHtmlContentWithInlineAttachments("", null, null); unsignedTextContainer.setVisibility(View.GONE); unsignedText.setText(""); } public void renderAttachments(MessageViewInfo messageViewInfo) { if (messageViewInfo.attachments != null) { for (AttachmentViewInfo attachment : messageViewInfo.attachments) { attachments.put(attachment.internalUri, attachment); if (attachment.inlineAttachment) { continue; } AttachmentView view = (AttachmentView) mInflater.inflate(R.layout.message_view_attachment, mAttachments, false); view.setCallback(attachmentCallback); view.setAttachment(attachment); attachmentViewMap.put(attachment, view); mAttachments.addView(view); } } if (messageViewInfo.extraAttachments != null) { for (AttachmentViewInfo attachment : messageViewInfo.extraAttachments) { attachments.put(attachment.internalUri, attachment); if (attachment.inlineAttachment) { continue; } LockedAttachmentView view = (LockedAttachmentView) mInflater .inflate(R.layout.message_view_attachment_locked, mAttachments, false); view.setCallback(attachmentCallback); view.setAttachment(attachment); // attachments.put(attachment, view); mAttachments.addView(view); } } } public void zoom(KeyEvent event) { if (event.isShiftPressed()) { mMessageContentView.zoomIn(); } else { mMessageContentView.zoomOut(); } } public void beginSelectingText() { mMessageContentView.emulateShiftHeld(); } public void resetView() { setLoadPictures(false); mAttachments.removeAllViews(); currentHtmlText = null; currentAttachmentResolver = null; /* * Clear the WebView content * * For some reason WebView.clearView() doesn't clear the contents when the WebView changes * its size because the button to download the complete message was previously shown and * is now hidden. */ clearDisplayedContent(); } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState savedState = new SavedState(superState); savedState.attachmentViewVisible = (mAttachmentsContainer != null && mAttachmentsContainer.getVisibility() == View.VISIBLE); savedState.showingPictures = showingPictures; return savedState; } @Override public void onRestoreInstanceState(Parcelable state) { if(!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState savedState = (SavedState)state; super.onRestoreInstanceState(savedState.getSuperState()); mSavedState = savedState; } @Override public void onLayoutChanged() { if (mMessageContentView != null) { mMessageContentView.invalidate(); } } public void enableAttachmentButtons(AttachmentViewInfo attachment) { getAttachmentView(attachment).enableButtons(); } public void disableAttachmentButtons(AttachmentViewInfo attachment) { getAttachmentView(attachment).disableButtons(); } public void refreshAttachmentThumbnail(AttachmentViewInfo attachment) { getAttachmentView(attachment).refreshThumbnail(); } private AttachmentView getAttachmentView(AttachmentViewInfo attachment) { return attachmentViewMap.get(attachment); } static class SavedState extends BaseSavedState { boolean attachmentViewVisible; boolean showingPictures; public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); this.attachmentViewVisible = (in.readInt() != 0); this.showingPictures = (in.readInt() != 0); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt((this.attachmentViewVisible) ? 1 : 0); out.writeInt((this.showingPictures) ? 1 : 0); } } interface OnRenderingFinishedListener { void onLoadFinished(); } }