View inline images in photo viewer. b/5555553.

Uses the existing javascript image src rewriting
step to build a mapping of urls to message ids.

This mapping is passed into the InlineAttachmentViewIntentBuilder
so that it can use this information along with the
conversation id and account name to build attachment
and attachment list uris so that a photo viewer open
intent can be created.

Additionally, SecureConversationViewController will need a different
mechanism for this UI to work in Email/the eml viewer.

Change-Id: If14800348fe2191d0633bf768b8cb4e9746f6578
diff --git a/assets/script.js b/assets/script.js
index 4667add..79987d3 100644
--- a/assets/script.js
+++ b/assets/script.js
@@ -447,6 +447,9 @@
     var msgContentDiv, image;
     var images;
     var showImages;
+    var k = 0;
+    var urls = new Array();
+    var messageIds = new Array();
     for (i = 0, msgContentCount = msgContentDivs.length; i < msgContentCount; i++) {
         msgContentDiv = msgContentDivs[i];
         showImages = msgContentDiv.classList.contains("mail-show-images");
@@ -454,7 +457,12 @@
         images = msgContentDiv.getElementsByTagName("img");
         for (j = 0, imgCount = images.length; j < imgCount; j++) {
             image = images[j];
-            rewriteRelativeImageSrc(image);
+            var src = rewriteRelativeImageSrc(image);
+            if (src) {
+                urls[k] = src;
+                messageIds[k] = msgContentDiv.parentNode.id;
+                k++;
+            }
             attachImageLoadListener(image);
             // TODO: handle inline image attachments for all supported protocols
             if (!showImages) {
@@ -462,11 +470,14 @@
             }
         }
     }
+
+    window.mail.onInlineAttachmentsParsed(urls, messageIds);
 }
 
 /**
- * Changes relative paths to absolute path by pre-pending the account uri
+ * Changes relative paths to absolute path by pre-pending the account uri.
  * @param {Element} imgElement Image for which the src path will be updated.
+ * @returns the rewritten image src string or null if the imgElement was not rewritten.
  */
 function rewriteRelativeImageSrc(imgElement) {
     var src = imgElement.src;
@@ -476,7 +487,10 @@
         // The conversation specifies a different base uri than the document
         src = CONVERSATION_BASE_URI + src.substring(DOC_BASE_URI.length);
         imgElement.src = src;
+        return src;
     }
+
+    return null;
 };
 
 
diff --git a/proguard.flags b/proguard.flags
index 2d4cfcf..1ac96a5 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -51,10 +51,6 @@
   public <methods>;
 }
 
--keepclasseswithmembers class com.android.mail.ui.ConversationViewFragment$MessageJsBridge {
-  public <methods>;
-}
-
 -keepclasseswithmembers class com.android.mail.ui.TwoPaneLayout {
   *** setFoldersLeft(...);
   *** setListBitmapLeft(...);
diff --git a/res/menu/webview_context_menu.xml b/res/menu/webview_context_menu.xml
index af7a59e..790e4f4 100644
--- a/res/menu/webview_context_menu.xml
+++ b/res/menu/webview_context_menu.xml
@@ -50,8 +50,6 @@
     <group android:id="@+id/IMAGE_MENU">
         <item android:id="@+id/view_image_context_menu_id"
               android:title="@string/contextmenu_view_image"/>
-        <item android:id="@+id/save_image_context_menu_id"
-              android:title="@string/contextmenu_save_image"/>
     </group>
 </menu>
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9850566..c1b7f39 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -260,8 +260,6 @@
     <string name="contextmenu_copylink">Copy link URL</string>
     <!-- Menu item to view an image  [CHAR LIMIT=50]-->
     <string name="contextmenu_view_image">View image</string>
-    <!-- Menu item to save an image  [CHAR LIMIT=50]-->
-    <string name="contextmenu_save_image">Save image</string>
     <!-- Menu item to dial a number  [CHAR LIMIT=50]-->
     <string name="contextmenu_dial_dot">Dial\u2026</string>
     <!-- Menu item to send an SMS  [CHAR LIMIT=50]-->
diff --git a/src/com/android/mail/browse/EmlMessageViewFragment.java b/src/com/android/mail/browse/EmlMessageViewFragment.java
index 6b73e15..04eabd6 100644
--- a/src/com/android/mail/browse/EmlMessageViewFragment.java
+++ b/src/com/android/mail/browse/EmlMessageViewFragment.java
@@ -250,6 +250,11 @@
         return mAccountUri;
     }
 
+    @Override
+    public boolean shouldAlwaysShowImages() {
+        return false;
+    }
+
     // End SecureConversationViewControllerCallbacks
 
     private class MessageLoadCallbacks
diff --git a/src/com/android/mail/browse/InlineAttachmentViewIntentBuilder.java b/src/com/android/mail/browse/InlineAttachmentViewIntentBuilder.java
new file mode 100644
index 0000000..a8c2d37
--- /dev/null
+++ b/src/com/android/mail/browse/InlineAttachmentViewIntentBuilder.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.browse;
+
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Builds an intent to be used when the user long presses an
+ * inline image and selects "View image".
+ */
+public interface InlineAttachmentViewIntentBuilder {
+
+    /**
+     * Creates an intent to be used when the user long presses an inline image and
+     * selects "View image." Null should be returned if "View image" should not be
+     * shown.
+     * @param context Used to create the intent.
+     * @param url The url of the image that was long-pressed.
+     * @return An intent that should be used when the user long presses an
+     * inline image and selects "View Image" or {@code null} if there should not
+     * be a "View image" option for this url.
+     */
+    Intent createInlineAttachmentViewIntent(Context context, String url);
+}
diff --git a/src/com/android/mail/browse/InlineAttachmentViewIntentBuilderCreator.java b/src/com/android/mail/browse/InlineAttachmentViewIntentBuilderCreator.java
new file mode 100644
index 0000000..e57e2a6
--- /dev/null
+++ b/src/com/android/mail/browse/InlineAttachmentViewIntentBuilderCreator.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.browse;
+
+import java.util.Map;
+
+/**
+ * Creates {@link InlineAttachmentViewIntentBuilder}s. Only one
+ * of these should ever exist and it should be set statically in
+ * the {@link android.app.Application} class of each app.
+ */
+public interface InlineAttachmentViewIntentBuilderCreator {
+    InlineAttachmentViewIntentBuilder createInlineAttachmentViewIntentBuilder(
+            Map<String, String> urlToMessageIdMap, String account, long conversationId);
+}
diff --git a/src/com/android/mail/browse/InlineAttachmentViewIntentBuilderCreatorHolder.java b/src/com/android/mail/browse/InlineAttachmentViewIntentBuilderCreatorHolder.java
new file mode 100644
index 0000000..eba609d
--- /dev/null
+++ b/src/com/android/mail/browse/InlineAttachmentViewIntentBuilderCreatorHolder.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.browse;
+
+/**
+ * Holds an {@link InlineAttachmentViewIntentBuilderCreator} that is used to create
+ * {@link InlineAttachmentViewIntentBuilder}s for the conversation views. <p/>
+ *
+ * Unfortunately, this pattern requires three layers. The holder (the top layer) is created at
+ * application start and should have its creator set in the {@link android.app.Application}
+ * so that each app has a creator that provides app-specific functionality.
+ * Typically, that functionality is creating a different type of
+ * {@link InlineAttachmentViewIntentBuilder} to do app-specific work. <p/>
+ *
+ * The middle layer is the {@link InlineAttachmentViewIntentBuilderCreator}. Only one of
+ * these exist and is created at {@link android.app.Application} start time (usually
+ * in a static block). During conversation view setup, this is used to create
+ * an {@link InlineAttachmentViewIntentBuilder}. The creation needs to be done at this
+ * time so that each conversation view can have its own builder that is passed
+ * conversation-specific data at builder creation time. <p/>
+ *
+ * The bottom layer is the {@link InlineAttachmentViewIntentBuilder}. This builder
+ * is passed into a {@link com.android.mail.browse.WebViewContextMenu} and used
+ * when an image is long-pressed to determine whether "View image" should be a menu
+ * option and what intent should fire when "View image" is selected.
+ */
+public class InlineAttachmentViewIntentBuilderCreatorHolder {
+    private static InlineAttachmentViewIntentBuilderCreator sCreator;
+
+    public static void setInlineAttachmentViewIntentCreator(
+            InlineAttachmentViewIntentBuilderCreator creator) {
+        sCreator = creator;
+    }
+
+    public static InlineAttachmentViewIntentBuilderCreator getInlineAttachmentViewIntentCreator() {
+        return sCreator;
+    }
+}
diff --git a/src/com/android/mail/browse/WebViewContextMenu.java b/src/com/android/mail/browse/WebViewContextMenu.java
index e8a9ca4..7f2bf0b 100644
--- a/src/com/android/mail/browse/WebViewContextMenu.java
+++ b/src/com/android/mail/browse/WebViewContextMenu.java
@@ -53,11 +53,12 @@
 public class WebViewContextMenu implements OnCreateContextMenuListener,
         MenuItem.OnMenuItemClickListener {
 
+    private final Activity mActivity;
+    private final InlineAttachmentViewIntentBuilder mIntentBuilder;
+
     private final boolean mSupportsDial;
     private final boolean mSupportsSms;
 
-    private Activity mActivity;
-
     protected static enum MenuType {
         OPEN_MENU,
         COPY_LINK_MENU,
@@ -72,8 +73,9 @@
         COPY_GEO_MENU,
     }
 
-    public WebViewContextMenu(Activity host) {
+    public WebViewContextMenu(Activity host, InlineAttachmentViewIntentBuilder builder) {
         mActivity = host;
+        mIntentBuilder = builder;
 
         // Query the package manager to see if the device
         // has an app that supports ACTION_DIAL or ACTION_SENDTO
@@ -187,8 +189,6 @@
         menu.setGroupVisible(R.id.GEO_MENU, type == WebView.HitTestResult.GEO_TYPE);
         menu.setGroupVisible(R.id.ANCHOR_MENU, type == WebView.HitTestResult.SRC_ANCHOR_TYPE
                 || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
-        menu.setGroupVisible(R.id.IMAGE_MENU, type == WebView.HitTestResult.IMAGE_TYPE
-                || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
 
         // Setup custom handling depending on the type
         switch (type) {
@@ -273,34 +273,67 @@
                 break;
 
             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
-            case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
-                menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)).setVisible(
-                        showShareLinkMenuItem());
-
-                // The documentation for WebView indicates that if the HitTestResult is
-                // SRC_ANCHOR_TYPE or the url would be specified in the extra.  We don't need to
-                // call requestFocusNodeHref().  If we wanted to handle UNKNOWN HitTestResults, we
-                // would.  With this knowledge, we can just set the title
-                menu.setHeaderTitle(extra);
-
-                menu.findItem(getMenuResIdForMenuType(MenuType.COPY_LINK_MENU)).
-                        setOnMenuItemClickListener(new Copy(extra));
-
-                final MenuItem openLinkMenuItem =
-                        menu.findItem(getMenuResIdForMenuType(MenuType.OPEN_MENU));
-                // remove the on click listener
-                openLinkMenuItem.setOnMenuItemClickListener(null);
-                openLinkMenuItem.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(extra)));
-
-                menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)).
-                        setOnMenuItemClickListener(new Share(extra));
+                setupAnchorMenu(extra, menu);
                 break;
+            case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
+                // Deliberately do not break because we want to fall through to image type.
+                setupAnchorMenu(extra, menu);
             case WebView.HitTestResult.IMAGE_TYPE:
+                // The image menu will be visible whenever the
+                // intent builder returns an intent. If it returns null,
+                // the image menu will not be shown.
+                menu.setGroupVisible(R.id.IMAGE_MENU, setupImageMenu(extra, menu));
+                break;
             default:
                 break;
         }
     }
 
+    private void setupAnchorMenu(String extra, ContextMenu menu) {
+        menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)).setVisible(
+                showShareLinkMenuItem());
+
+        // The documentation for WebView indicates that if the HitTestResult is
+        // SRC_ANCHOR_TYPE or the url would be specified in the extra.  We don't need to
+        // call requestFocusNodeHref().  If we wanted to handle UNKNOWN HitTestResults, we
+        // would.  With this knowledge, we can just set the title
+        menu.setHeaderTitle(extra);
+
+        menu.findItem(getMenuResIdForMenuType(MenuType.COPY_LINK_MENU)).
+                setOnMenuItemClickListener(new Copy(extra));
+
+        final MenuItem openLinkMenuItem =
+                menu.findItem(getMenuResIdForMenuType(MenuType.OPEN_MENU));
+        // remove the on click listener
+        openLinkMenuItem.setOnMenuItemClickListener(null);
+        openLinkMenuItem.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(extra)));
+
+        menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)).
+                setOnMenuItemClickListener(new Share(extra));
+    }
+
+    /**
+     * Used to setup the image menu group if the {@link android.webkit.WebView.HitTestResult}
+     * is of type {@link android.webkit.WebView.HitTestResult#IMAGE_TYPE} or
+     * {@link android.webkit.WebView.HitTestResult#SRC_IMAGE_ANCHOR_TYPE}.
+     * @param url Url that was long pressed.
+     * @param menu The {@link android.view.ContextMenu} that is about to be shown.
+     * @return {@code true} if the view image menu item should be visible.
+     * {@code false}, otherwise.
+     */
+    protected boolean setupImageMenu(String url, ContextMenu menu) {
+        final Intent intent = mIntentBuilder.createInlineAttachmentViewIntent(mActivity, url);
+        if (intent == null) {
+            return false;
+        }
+
+        final MenuItem menuItem = menu.findItem(R.id.view_image_context_menu_id);
+        menuItem.setOnMenuItemClickListener(null);
+        menuItem.setIntent(intent);
+
+        return true;
+    }
+
     @Override
     public boolean onMenuItemClick(MenuItem item) {
         return onMenuItemSelected(item);
diff --git a/src/com/android/mail/photo/MailPhotoViewActivity.java b/src/com/android/mail/photo/MailPhotoViewActivity.java
index 510562f..a0618ba 100644
--- a/src/com/android/mail/photo/MailPhotoViewActivity.java
+++ b/src/com/android/mail/photo/MailPhotoViewActivity.java
@@ -19,6 +19,7 @@
 
 import android.app.ActionBar;
 import android.content.Context;
+import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
@@ -106,15 +107,20 @@
      */
     public static void startMailPhotoViewActivity(final Context context, final Uri imageListUri,
             final String initialPhotoUri) {
-        final Intents.PhotoViewIntentBuilder builder =
-                Intents.newPhotoViewIntentBuilder(context,
-                        "com.android.mail.photo.MailPhotoViewActivity");
-        builder
-                .setPhotosUri(imageListUri.toString())
+        context.startActivity(
+                buildMailPhotoViewActivityIntent(context, imageListUri, initialPhotoUri));
+    }
+
+    public static Intent buildMailPhotoViewActivityIntent(
+            final Context context, final Uri imageListUri, final String initialPhotoUri) {
+        final Intents.PhotoViewIntentBuilder builder = Intents.newPhotoViewIntentBuilder(
+                context, "com.android.mail.photo.MailPhotoViewActivity");
+
+        builder.setPhotosUri(imageListUri.toString())
                 .setProjection(UIProvider.ATTACHMENT_PROJECTION)
                 .setInitialPhotoUri(initialPhotoUri);
 
-        context.startActivity(builder.build());
+        return builder.build();
     }
 
     @Override
diff --git a/src/com/android/mail/ui/AbstractConversationViewFragment.java b/src/com/android/mail/ui/AbstractConversationViewFragment.java
index 06542c1..5425f7d 100644
--- a/src/com/android/mail/ui/AbstractConversationViewFragment.java
+++ b/src/com/android/mail/ui/AbstractConversationViewFragment.java
@@ -47,6 +47,7 @@
 import com.android.mail.providers.Conversation;
 import com.android.mail.providers.Folder;
 import com.android.mail.providers.ListParams;
+import com.android.mail.providers.Settings;
 import com.android.mail.providers.UIProvider;
 import com.android.mail.providers.UIProvider.CursorStatus;
 import com.android.mail.utils.LogTag;
@@ -688,4 +689,8 @@
     }
 
     protected abstract void printConversation();
+
+    public boolean shouldAlwaysShowImages() {
+        return (mAccount != null) && (mAccount.settings.showImages == Settings.ShowImages.ALWAYS);
+    }
 }
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index 2793164..76c4b4e 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -28,6 +28,7 @@
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.support.v4.text.BidiFormatter;
+import android.support.v4.util.ArrayMap;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -56,6 +57,8 @@
 import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
 import com.android.mail.browse.ConversationViewHeader;
 import com.android.mail.browse.ConversationWebView;
+import com.android.mail.browse.InlineAttachmentViewIntentBuilderCreator;
+import com.android.mail.browse.InlineAttachmentViewIntentBuilderCreatorHolder;
 import com.android.mail.browse.MailWebView.ContentSizeChangeListener;
 import com.android.mail.browse.MessageCursor;
 import com.android.mail.browse.MessageHeaderView;
@@ -210,6 +213,11 @@
     private BidiFormatter sBidiFormatter;
 
     /**
+     * Contains a mapping between inline image attachments and their local message id.
+     */
+    private Map<String, String> mUrlToMessageIdMap;
+
+    /**
      * Constructor needs to be public to handle orientation changes and activity lifecycle events.
      */
     public ConversationViewFragment() {}
@@ -280,7 +288,14 @@
         mSideMarginPx = resources.getDimensionPixelOffset(
                 R.dimen.conversation_message_content_margin_side);
 
-        mWebView.setOnCreateContextMenuListener(new WebViewContextMenu(getActivity()));
+        mUrlToMessageIdMap = new ArrayMap<String, String>();
+        final InlineAttachmentViewIntentBuilderCreator creator =
+                InlineAttachmentViewIntentBuilderCreatorHolder.
+                getInlineAttachmentViewIntentCreator();
+        mWebView.setOnCreateContextMenuListener(new WebViewContextMenu(getActivity(),
+                creator.createInlineAttachmentViewIntentBuilder(
+                mUrlToMessageIdMap, mAccount.getEmailAddress(),
+                mConversation != null ? mConversation.id : -1)));
 
         // set this up here instead of onCreateView to ensure the latest Account is loaded
         setupOverviewMode();
@@ -370,9 +385,15 @@
         final WebChromeClient wcc = new WebChromeClient() {
             @Override
             public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
-                LogUtils.i(LOG_TAG, "JS: %s (%s:%d) f=%s", consoleMessage.message(),
-                        consoleMessage.sourceId(), consoleMessage.lineNumber(),
-                        ConversationViewFragment.this);
+                if (consoleMessage.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
+                    LogUtils.wtf(LOG_TAG, "JS: %s (%s:%d) f=%s", consoleMessage.message(),
+                            consoleMessage.sourceId(), consoleMessage.lineNumber(),
+                            ConversationViewFragment.this);
+                } else {
+                    LogUtils.i(LOG_TAG, "JS: %s (%s:%d) f=%s", consoleMessage.message(),
+                            consoleMessage.sourceId(), consoleMessage.lineNumber(),
+                            ConversationViewFragment.this);
+                }
                 return true;
             }
         };
@@ -686,8 +707,7 @@
         int collapsedStart = -1;
         ConversationMessage prevCollapsedMsg = null;
 
-        final boolean alwaysShowImages = (mAccount != null) &&
-                (mAccount.settings.showImages == Settings.ShowImages.ALWAYS);
+        final boolean alwaysShowImages = shouldAlwaysShowImages();
 
         boolean prevSafeForImages = alwaysShowImages;
 
@@ -1141,17 +1161,14 @@
      *
      */
     private class MailJsBridge {
-
-        @SuppressWarnings("unused")
         @JavascriptInterface
         public void onWebContentGeometryChange(final String[] overlayTopStrs,
                 final String[] overlayBottomStrs) {
-            getHandler().post(new FragmentRunnable("onWebContentGeometryChange",
-                    ConversationViewFragment.this) {
-
-                @Override
-                public void go() {
-                    try {
+            try {
+                getHandler().post(new FragmentRunnable("onWebContentGeometryChange",
+                        ConversationViewFragment.this) {
+                    @Override
+                    public void go() {
                         if (!mViewsCreated) {
                             LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views"
                                     + " are gone, %s", ConversationViewFragment.this);
@@ -1167,14 +1184,13 @@
                             }
                             mDiff = 0;
                         }
-                    } catch (Throwable t) {
-                        LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
                     }
-                }
-            });
+                });
+            } catch (Throwable t) {
+                LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
+            }
         }
 
-        @SuppressWarnings("unused")
         @JavascriptInterface
         public String getTempMessageBodies() {
             try {
@@ -1191,7 +1207,6 @@
             }
         }
 
-        @SuppressWarnings("unused")
         @JavascriptInterface
         public String getMessageBody(String domId) {
             try {
@@ -1216,7 +1231,6 @@
             }
         }
 
-        @SuppressWarnings("unused")
         @JavascriptInterface
         public String getMessageSender(String domId) {
             try {
@@ -1241,31 +1255,33 @@
             }
         }
 
-        @SuppressWarnings("unused")
         @JavascriptInterface
         public void onContentReady() {
-            getHandler().post(new FragmentRunnable("onContentReady",
-                    ConversationViewFragment.this) {
-                @Override
-                public void go() {
-                    try {
-                        if (mWebViewLoadStartMs != 0) {
-                            LogUtils.i(LOG_TAG, "IN CVF.onContentReady, f=%s vis=%s t=%sms",
-                                    ConversationViewFragment.this,
-                                    isUserVisible(),
-                                    (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
+            try {
+                getHandler().post(new FragmentRunnable("onContentReady",
+                        ConversationViewFragment.this) {
+                    @Override
+                    public void go() {
+                        try {
+                            if (mWebViewLoadStartMs != 0) {
+                                LogUtils.i(LOG_TAG, "IN CVF.onContentReady, f=%s vis=%s t=%sms",
+                                        ConversationViewFragment.this,
+                                        isUserVisible(),
+                                        (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
+                            }
+                            revealConversation();
+                        } catch (Throwable t) {
+                            LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
+                            // Still try to show the conversation.
+                            revealConversation();
                         }
-                        revealConversation();
-                    } catch (Throwable t) {
-                        LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
-                        // Still try to show the conversation.
-                        revealConversation();
                     }
-                }
-            });
+                });
+            } catch (Throwable t) {
+                LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
+            }
         }
 
-        @SuppressWarnings("unused")
         @JavascriptInterface
         public float getScrollYPercent() {
             try {
@@ -1276,7 +1292,6 @@
             }
         }
 
-        @SuppressWarnings("unused")
         @JavascriptInterface
         public void onMessageTransform(String messageDomId, String transformText) {
             try {
@@ -1285,7 +1300,29 @@
                 onConversationTransformed();
             } catch (Throwable t) {
                 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onMessageTransform");
-                return;
+            }
+        }
+
+        @JavascriptInterface
+        public void onInlineAttachmentsParsed(final String[] urls, final String[] messageIds) {
+            try {
+                getHandler().post(new FragmentRunnable("onInlineAttachmentsParsed",
+                        ConversationViewFragment.this) {
+                    @Override
+                    public void go() {
+                        try {
+                            for (int i = 0, size = urls.length; i < size; i++) {
+                                mUrlToMessageIdMap.put(urls[i], messageIds[i]);
+                            }
+                        } catch (ArrayIndexOutOfBoundsException e) {
+                            LogUtils.e(LOG_TAG, e,
+                                    "Number of urls does not match number of message ids - %s:%s",
+                                    urls.length, messageIds.length);
+                        }
+                    }
+                });
+            } catch (Throwable t) {
+                LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onInlineAttachmentsParsed");
             }
         }
     }
diff --git a/src/com/android/mail/ui/HtmlConversationTemplates.java b/src/com/android/mail/ui/HtmlConversationTemplates.java
index 104d0c0..3e37a68 100644
--- a/src/com/android/mail/ui/HtmlConversationTemplates.java
+++ b/src/com/android/mail/ui/HtmlConversationTemplates.java
@@ -36,6 +36,7 @@
      * Prefix applied to a message id for use as a div id
      */
     public static final String MESSAGE_PREFIX = "m";
+    public static final int MESSAGE_PREFIX_LENGTH = MESSAGE_PREFIX.length();
 
     private static final String TAG = LogTag.getLogTag();
 
diff --git a/src/com/android/mail/ui/SecureConversationViewController.java b/src/com/android/mail/ui/SecureConversationViewController.java
index e8e8417..0ed03f6 100644
--- a/src/com/android/mail/ui/SecureConversationViewController.java
+++ b/src/com/android/mail/ui/SecureConversationViewController.java
@@ -33,6 +33,8 @@
 import com.android.mail.browse.ConversationViewAdapter;
 import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
 import com.android.mail.browse.ConversationViewHeader;
+import com.android.mail.browse.InlineAttachmentViewIntentBuilderCreator;
+import com.android.mail.browse.InlineAttachmentViewIntentBuilderCreatorHolder;
 import com.android.mail.browse.MessageFooterView;
 import com.android.mail.browse.MessageHeaderView;
 import com.android.mail.browse.MessageScrollView;
@@ -98,8 +100,12 @@
         mWebView = (MessageWebView) rootView.findViewById(R.id.webview);
         mWebView.setOverScrollMode(View.OVER_SCROLL_NEVER);
         mWebView.setWebViewClient(mCallbacks.getWebViewClient());
-        mWebView.setOnCreateContextMenuListener(
-                new WebViewContextMenu(mCallbacks.getFragment().getActivity()));
+        final InlineAttachmentViewIntentBuilderCreator creator =
+                InlineAttachmentViewIntentBuilderCreatorHolder.
+                getInlineAttachmentViewIntentCreator();
+        mWebView.setOnCreateContextMenuListener(new WebViewContextMenu(
+                mCallbacks.getFragment().getActivity(),
+                creator.createInlineAttachmentViewIntentBuilder(null, null, -1)));
         mWebView.setFocusable(false);
         final WebSettings settings = mWebView.getSettings();
 
@@ -151,7 +157,9 @@
     public void renderMessage(ConversationMessage message) {
         mMessage = message;
 
-        mWebView.getSettings().setBlockNetworkImage(!mMessage.alwaysShowImages);
+        final boolean alwaysShowImages = mCallbacks.shouldAlwaysShowImages();
+        mWebView.getSettings().setBlockNetworkImage(
+                !alwaysShowImages && !mMessage.alwaysShowImages);
 
         // Add formatting to message body
         // At this point, only adds margins.
diff --git a/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java b/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java
index 2e4f5d9..f53de7d 100644
--- a/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java
+++ b/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java
@@ -46,4 +46,5 @@
     public String getBaseUri();
     public boolean isViewOnlyMode();
     public Uri getAccountUri();
+    public boolean shouldAlwaysShowImages();
 }