hook up 'show pictures' button
Add 'show pictures' button for messages where the provider
claims it embeds external resources. In Gmail, this is true when
a message contains external image hotlinks. Inline image
attachments are not yet handled.
'always show' tells the provider to whitelist the message sender
to allow images in the future (works with Gmail provider).
Change-Id: I1b174d8fad9da481ec305caff06d109f9d215093
diff --git a/assets/script.js b/assets/script.js
index 1e9a2a8..e8a070e 100644
--- a/assets/script.js
+++ b/assets/script.js
@@ -15,6 +15,7 @@
* limitations under the License.
*/
+var BLOCKED_SRC_ATTR = "blocked-src";
/**
* Returns the page offset of an element.
@@ -87,6 +88,46 @@
}
}
+function hideUnsafeImages() {
+ var i, bodyCount;
+ var j, imgCount;
+ var body, image;
+ var images;
+ var showImages;
+ var bodies = document.getElementsByClassName("mail-message-content");
+ for (i = 0, bodyCount = bodies.length; i < bodyCount; i++) {
+ body = bodies[i];
+ showImages = body.classList.contains("mail-show-images");
+
+ images = body.getElementsByTagName("img");
+ for (j = 0, imgCount = images.length; j < imgCount; j++) {
+ image = images[j];
+ attachImageLoadListener(image);
+ // TODO: handle inline image attachments for all supported protocols
+ if (!showImages) {
+ blockImage(image);
+ }
+ }
+ }
+}
+
+function attachImageLoadListener(imageElement) {
+ // Reset the src attribute to the empty string because onload will only fire if the src
+ // attribute is set after the onload listener.
+ var originalSrc = imageElement.src;
+ imageElement.src = '';
+ imageElement.onload = measurePositions;
+ imageElement.src = originalSrc;
+}
+
+function blockImage(imageElement) {
+ var src = imageElement.src;
+ if (src.indexOf("http://") == 0 || src.indexOf("https://") == 0) {
+ imageElement.setAttribute(BLOCKED_SRC_ATTR, src);
+ imageElement.src = "data:";
+ }
+}
+
function measurePositions() {
var headerHeights;
var headerBottoms;
@@ -108,7 +149,28 @@
window.mail.onWebContentGeometryChange(headerBottoms, headerHeights);
}
+// BEGIN Java->JavaScript handlers
+function unblockImages(messageDomId) {
+ var i, images, imgCount, image, blockedSrc;
+ var msg = document.getElementById(messageDomId);
+ if (!msg) {
+ console.log("can't unblock, no matching message for id: " + messageDomId);
+ return;
+ }
+ images = msg.getElementsByTagName("img");
+ for (i = 0, imgCount = images.length; i < imgCount; i++) {
+ image = images[i];
+ blockedSrc = image.getAttribute(BLOCKED_SRC_ATTR);
+ if (blockedSrc) {
+ image.src = blockedSrc;
+ image.removeAttribute(BLOCKED_SRC_ATTR);
+ }
+ }
+}
+// END Java->JavaScript handlers
+
collapseQuotedText();
+hideUnsafeImages();
shrinkWideMessages();
measurePositions();
diff --git a/src/com/android/mail/browse/ConversationContainer.java b/src/com/android/mail/browse/ConversationContainer.java
index ee11997..e9c7151 100644
--- a/src/com/android/mail/browse/ConversationContainer.java
+++ b/src/com/android/mail/browse/ConversationContainer.java
@@ -381,8 +381,16 @@
LogUtils.d(TAG, "*** IN header container onMeasure spec for w/h=%d/%d", widthMeasureSpec,
heightMeasureSpec);
- measureChildren(widthMeasureSpec, heightMeasureSpec);
+ if (mWebView.getVisibility() != GONE) {
+ measureChild(mWebView, widthMeasureSpec, heightMeasureSpec);
+ }
mWidthMeasureSpec = widthMeasureSpec;
+ for (int i = 0, overlayCount = getOverlayCount(); i < overlayCount; i++) {
+ final View overlayView = getOverlayAt(i);
+ if (overlayView.getVisibility() != GONE) {
+ measureItem(overlayView);
+ }
+ }
}
@Override
@@ -463,11 +471,6 @@
mOverlayBottoms.length, mOverlayHeights.length);
}
- // TODO: don't remove visible views. not an issue yet since this is only called once.
- while (getOverlayCount() > 0) {
- removeView(getOverlayAt(0));
- }
-
// hack to bootstrap initial display of headers
handleScroll(0, mOffsetY);
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index ee6a5de..e2e1dd4 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -19,6 +19,7 @@
import com.google.common.annotations.VisibleForTesting;
import android.app.LoaderManager;
+import android.content.AsyncQueryHandler;
import android.content.Context;
import android.content.Loader;
import android.database.Cursor;
@@ -88,8 +89,6 @@
private static final String LOG_TAG = new LogUtils().getLogTag();
private MessageHeaderViewCallbacks mCallbacks;
- private long mLocalMessageId = UIProvider.INVALID_CONVERSATION_ID;
- private long mServerMessageId;
private boolean mSizeChanged;
private TextView mSenderNameView;
@@ -176,6 +175,17 @@
private final LayoutInflater mInflater;
+ private AsyncQueryHandler mQueryHandler;
+
+ public interface MessageHeaderViewCallbacks {
+ void setMessageSpacerHeight(Message msg, int height);
+
+ void setMessageExpanded(Message msg, boolean expanded,
+ int spacerHeight);
+
+ void showExternalResources(Message msg);
+ }
+
public MessageHeaderView(Context context) {
this(context, null);
}
@@ -219,23 +229,6 @@
}
}
- public interface MessageHeaderViewCallbacks {
- void setMessageSpacerHeight(long localMessageId, int height);
-
- void setMessageExpanded(long localMessageId, long serverMessageId, boolean expanded,
- int spacerHeight);
-
- Timer getLoadTimer();
-
- void onHeaderCreated(long headerId);
-
- void onHeaderDrawn(long headerId);
-
- void showExternalResources(long localMessageId);
-
- void setDisplayImagesFromSender(String fromAddress);
- }
-
/**
* Associate the header with a contact info source for later contact
* presence/photo lookup.
@@ -259,10 +252,6 @@
return (MessageHeaderView) parent.findViewWithTag(localMessageId);
}
- public long getLocalMessageId() {
- return mLocalMessageId;
- }
-
public boolean isExpanded() {
return mIsExpanded;
}
@@ -294,7 +283,7 @@
* @return true if the headers are displaying data for the same message
*/
public boolean matches(MessageHeaderView other) {
- return other != null && mLocalMessageId == other.mLocalMessageId;
+ return other != null && mMessage != null && mMessage.equals(other.mMessage);
}
/**
@@ -304,12 +293,11 @@
* renderUpperHeaderFrom().
*/
public void unbind() {
- mLocalMessageId = UIProvider.INVALID_MESSAGE_ID;
+ mMessage = null;
}
public void renderUpperHeaderFrom(MessageHeaderView other) {
- mLocalMessageId = other.mLocalMessageId;
- mServerMessageId = other.mServerMessageId;
+ mMessage = other.mMessage;
mSender = other.mSender;
mDefaultReplyAll = other.mDefaultReplyAll;
@@ -352,11 +340,6 @@
mExpandedDetailsValid = false;
mMessage = message;
- mLocalMessageId = mMessage.id;
- mServerMessageId = mMessage.serverId;
- if (mCallbacks != null) {
- mCallbacks.onHeaderCreated(mLocalMessageId);
- }
mTimestampMs = mMessage.dateReceivedMs;
if (mDateBuilder != null) {
@@ -903,7 +886,7 @@
// mSizeChanged.
int h = forceMeasuredHeight();
if (mCallbacks != null) {
- mCallbacks.setMessageExpanded(mLocalMessageId, mServerMessageId, mIsExpanded, h);
+ mCallbacks.setMessageExpanded(mMessage, mIsExpanded, h);
}
}
@@ -933,9 +916,13 @@
setMessageDetailsExpanded(mDetailsExpanded);
if (mShowImagePrompt) {
showImagePrompt();
+ } else {
+ hideShowImagePrompt();
}
if (mMessage.hasAttachments) {
showAttachments();
+ } else {
+ hideAttachments();
}
}
if (mBottomBorderView != null) {
@@ -1014,8 +1001,8 @@
private void showImagePrompt() {
if (mImagePromptView == null) {
- ViewGroup v = (ViewGroup) LayoutInflater.from(getContext()).inflate(
- R.layout.conversation_message_show_pics, this, false);
+ ViewGroup v = (ViewGroup) mInflater.inflate(R.layout.conversation_message_show_pics,
+ this, false);
addView(v);
v.setOnClickListener(this);
v.setTag(SHOW_IMAGE_PROMPT_ONCE);
@@ -1033,7 +1020,7 @@
switch (state) {
case SHOW_IMAGE_PROMPT_ONCE:
if (mCallbacks != null) {
- mCallbacks.showExternalResources(mLocalMessageId);
+ mCallbacks.showExternalResources(mMessage);
}
ImageView descriptionViewIcon = (ImageView) v.findViewById(R.id.show_pictures_icon);
descriptionViewIcon.setContentDescription(getResources().getString(
@@ -1047,9 +1034,8 @@
mSizeChanged = true;
break;
case SHOW_IMAGE_PROMPT_ALWAYS:
- if (mCallbacks != null) {
- mCallbacks.setDisplayImagesFromSender(mSender.getAddress());
- }
+ mMessage.markAlwaysShowImages(getQueryHandler(), 0 /* token */, null /* cookie */);
+
mShowImagePrompt = false;
v.setTag(null);
v.setVisibility(GONE);
@@ -1060,6 +1046,13 @@
}
}
+ private AsyncQueryHandler getQueryHandler() {
+ if (mQueryHandler == null) {
+ mQueryHandler = new AsyncQueryHandler(getContext().getContentResolver()) {};
+ }
+ return mQueryHandler;
+ }
+
/**
* Makes collapsed details visible. If necessary, will inflate details
* layout and render using saved-off state (senders, timestamp, etc).
@@ -1075,8 +1068,7 @@
// separate layout and inflated alongside either collapsed or
// expanded, whichever is
// first.
- LayoutInflater.from(getContext()).inflate(R.layout.conversation_message_details_header,
- this);
+ mInflater.inflate(R.layout.conversation_message_details_header, this);
mBottomBorderView = findViewById(R.id.details_bottom_border);
mCollapsedDetailsView = (ViewGroup) findViewById(R.id.details_collapsed_content);
@@ -1102,8 +1094,8 @@
private void showExpandedDetails() {
// lazily create expanded details view
if (mExpandedDetailsView == null) {
- View v = LayoutInflater.from(getContext()).inflate(
- R.layout.conversation_message_details_header_expanded, this, false);
+ View v = mInflater.inflate(R.layout.conversation_message_details_header_expanded,
+ this, false);
// Insert expanded details into the parent linear layout immediately
// after the
@@ -1138,7 +1130,7 @@
// propagate new size to webview header spacer
// only do this for known size changes
if (mCallbacks != null) {
- mCallbacks.setMessageSpacerHeight(mLocalMessageId, h);
+ mCallbacks.setMessageSpacerHeight(mMessage, h);
}
mSizeChanged = false;
@@ -1247,9 +1239,6 @@
if (transform) {
canvas.restoreToCount(saved);
}
- if (mCallbacks != null) {
- mCallbacks.onHeaderDrawn(mLocalMessageId);
- }
}
@Override
@@ -1264,7 +1253,7 @@
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Timer t = new Timer();
if (Timer.ENABLE_TIMER && !mPreMeasuring) {
- t.count("header measure id=" + mLocalMessageId);
+ t.count("header measure id=" + mMessage.id);
t.start(MEASURE_TAG);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
diff --git a/src/com/android/mail/providers/Message.java b/src/com/android/mail/providers/Message.java
index 3401915..4ac282a 100644
--- a/src/com/android/mail/providers/Message.java
+++ b/src/com/android/mail/providers/Message.java
@@ -16,8 +16,11 @@
package com.android.mail.providers;
+import com.android.mail.providers.UIProvider.MessageColumns;
import com.android.mail.utils.Utils;
+import android.content.AsyncQueryHandler;
+import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Parcel;
@@ -65,6 +68,23 @@
}
@Override
+ public boolean equals(Object o) {
+ if (o == null || !(o instanceof Message)) {
+ return false;
+ }
+ final Uri otherUri = ((Message) o).uri;
+ if (uri == null) {
+ return (otherUri == null);
+ }
+ return uri.equals(otherUri);
+ }
+
+ @Override
+ public int hashCode() {
+ return uri == null ? 0 : uri.hashCode();
+ }
+
+ @Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(id);
dest.writeLong(serverId);
@@ -220,4 +240,32 @@
}
return mReplyToAddresses;
}
+
+ /**
+ * Returns whether a "Show Pictures" button should initially appear for this message. If the
+ * button is shown, the message must also block all non-local images in the body. Inversely, if
+ * the button is not shown, the message must show all images within (or else the user would be
+ * stuck with no images and no way to reveal them).
+ *
+ * @return true if a "Show Pictures" button should appear.
+ */
+ public boolean shouldShowImagePrompt() {
+ return embedsExternalResources && !alwaysShowImages;
+ }
+
+ /**
+ * Helper method to command a provider to mark all messages from this sender with the
+ * {@link MessageColumns#ALWAYS_SHOW_IMAGES} flag set.
+ *
+ * @param handler a caller-provided handler to run the query on
+ * @param token (optional) token to identify the command to the handler
+ * @param cookie (optional) cookie to pass to the handler
+ */
+ public void markAlwaysShowImages(AsyncQueryHandler handler, int token, Object cookie) {
+ final ContentValues values = new ContentValues(1);
+ values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, 1);
+
+ handler.startUpdate(token, cookie, uri, values, null, null);
+ }
+
}
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index d1cd36b..ecf327e 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -53,6 +53,8 @@
import com.android.mail.browse.ConversationViewHeader;
import com.android.mail.browse.ConversationWebView;
import com.android.mail.browse.MessageHeaderView;
+import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
+import com.android.mail.perf.Timer;
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.ListParams;
@@ -68,7 +70,8 @@
*/
public final class ConversationViewFragment extends Fragment implements
LoaderManager.LoaderCallbacks<Cursor>,
- ConversationViewHeader.ConversationViewHeaderCallbacks {
+ ConversationViewHeader.ConversationViewHeaderCallbacks,
+ MessageHeaderViewCallbacks {
private static final String LOG_TAG = new LogUtils().getLogTag();
@@ -153,7 +156,7 @@
mTemplates = new HtmlConversationTemplates(mContext);
mAdapter = new MessageListAdapter(mActivity.getActivityContext(),
- null /* cursor */, mAccount, getLoaderManager());
+ null /* cursor */, mAccount, getLoaderManager(), this);
mConversationContainer.setOverlayAdapter(mAdapter);
mDensity = getResources().getDisplayMetrics().density;
@@ -198,9 +201,7 @@
}
});
- WebSettings settings = mWebView.getSettings();
-
- settings.setBlockNetworkImage(true);
+ final WebSettings settings = mWebView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setUseWideViewPort(true);
@@ -295,9 +296,19 @@
// FIXME: measure the header (and the attachments) and insert spacers of appropriate size
final int spacerH = (Utils.useTabletUI(mContext)) ? 112 : 96;
+
+ boolean allowNetworkImages = false;
+
while (messageCursor.moveToPosition(++pos)) {
- mTemplates.appendMessageHtml(messageCursor.get(), true, false, 1.0f, spacerH);
+ final Message msg = messageCursor.get();
+ // TODO: save/restore 'show pics' state
+ final boolean safeForImages = msg.alwaysShowImages /* || savedStateSaysSafe */;
+ allowNetworkImages |= safeForImages;
+ mTemplates.appendMessageHtml(msg, true /* expanded */, safeForImages, 1.0f, spacerH);
}
+
+ mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
+
return mTemplates.endConversation(mBaseUri, 320);
}
@@ -306,6 +317,7 @@
// if its not in undo bar, dismiss the undo bar.
}
+ // BEGIN conversation header callbacks
@Override
public void onFoldersClicked() {
if (mChangeFoldersMenuItem == null) {
@@ -326,6 +338,26 @@
// TODO: hook this up to action bar
return subject;
}
+ // END conversation header callbacks
+
+ // START message header callbacks
+ @Override
+ public void setMessageSpacerHeight(Message msg, int height) {
+ // TODO: update message HTML spacer height
+ // TODO: expand this concept to handle bottom-aligned attachments
+ }
+
+ @Override
+ public void setMessageExpanded(Message msg, boolean expanded, int spacerHeight) {
+ // TODO: show/hide the HTML message body and update the spacer height
+ }
+
+ @Override
+ public void showExternalResources(Message msg) {
+ mWebView.getSettings().setBlockNetworkImage(false);
+ mWebView.loadUrl("javascript:unblockImages('" + mTemplates.getMessageDomId(msg) + "');");
+ }
+ // END message header callbacks
private static class MessageLoader extends CursorLoader {
private boolean mDeliveredFirstResults = false;
@@ -386,21 +418,25 @@
private final FormattedDateBuilder mDateBuilder;
private final Account mAccount;
private final LoaderManager mLoaderManager;
+ private final MessageHeaderViewCallbacks mCallbacks;
public MessageListAdapter(Context context, Cursor messageCursor, Account account,
- LoaderManager loaderManager) {
+ LoaderManager loaderManager, MessageHeaderViewCallbacks cb) {
super(context, R.layout.conversation_message_header, messageCursor, 0);
mDateBuilder = new FormattedDateBuilder(context);
mAccount = account;
mLoaderManager = loaderManager;
+ mCallbacks = cb;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
- Message m = ((MessageCursor) cursor).get();
+ final Message msg = ((MessageCursor) cursor).get();
MessageHeaderView header = (MessageHeaderView) view;
- header.initialize(mDateBuilder, mAccount, mLoaderManager, true, false, false);
- header.bind(m);
+ header.setCallbacks(mCallbacks);
+ header.initialize(mDateBuilder, mAccount, mLoaderManager, true /* expanded */,
+ msg.shouldShowImagePrompt(), false /* defaultReplyAll */);
+ header.bind(msg);
}
}
diff --git a/src/com/android/mail/ui/HtmlConversationTemplates.java b/src/com/android/mail/ui/HtmlConversationTemplates.java
index c14142a..e6b8c62 100644
--- a/src/com/android/mail/ui/HtmlConversationTemplates.java
+++ b/src/com/android/mail/ui/HtmlConversationTemplates.java
@@ -86,7 +86,7 @@
* The text replacement for {@link #sAbsoluteImgUrlPattern}. The "src" attribute is set to
* something inert and not left unset to minimize interactions with existing JS.
*/
- private static final String IMG_URL_REPLACEMENT = "$1src='data:' gm-src$2";
+ private static final String IMG_URL_REPLACEMENT = "$1src='data:' blocked-src$2";
private static boolean sLoadedTemplates;
private static String sSuperCollapsed;
@@ -130,7 +130,7 @@
boolean safeForImages, float zoomValue, int headerHeight) {
final String bodyDisplay = isExpanded ? "block" : "none";
- final String showImagesClass = safeForImages ? "gm-show-images" : "";
+ final String showImagesClass = safeForImages ? "mail-show-images" : "";
String body = "";
if (!TextUtils.isEmpty(message.bodyHtml)) {
@@ -158,7 +158,7 @@
}
append(sMessage,
- MESSAGE_PREFIX + message.id,
+ getMessageDomId(message),
MESSAGE_PREFIX + message.serverId,
headerHeight,
showImagesClass,
@@ -168,6 +168,10 @@
);
}
+ public String getMessageDomId(Message msg) {
+ return MESSAGE_PREFIX + msg.id;
+ }
+
public void startConversation(int conversationHeaderHeight) {
if (mInProgress) {
throw new IllegalStateException("must call startConversation first");