Merge "View inline images in photo viewer. b/5555553." into ub-mail-master
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();
 }