Print single message and offscreen webview.

b/10712542.
Also supports Eml printing and secure printing (though
that's email only and has some buggy rendering).
Also fixes the no subject crash. b/11136365

Change-Id: Ie5f6e7d7e1762c115df3169b6e62dc439545f08c
diff --git a/res/menu/eml_viewer_menu.xml b/res/menu/eml_viewer_menu.xml
index 662d95c..a229790 100644
--- a/res/menu/eml_viewer_menu.xml
+++ b/res/menu/eml_viewer_menu.xml
@@ -17,6 +17,10 @@
 -->
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/print_message"
+          android:title="@string/print"
+          android:showAsAction="never" />
+
     <!-- Always available -->
     <item android:id="@+id/settings"
           android:title="@string/menu_settings"
diff --git a/res/raw/template_print_conversation_upper.html b/res/raw/template_print_conversation_upper.html
index 62e68bf..bc46f67 100644
--- a/res/raw/template_print_conversation_upper.html
+++ b/res/raw/template_print_conversation_upper.html
@@ -68,9 +68,6 @@
                     <img src="file:///android_asset/images/ic_launcher_mail.png"
                          width=143 height=59 alt="%s">
                 </td>
-                <td align=right>
-                    <font size=-1 color=#777><b>%s &lt;%s&gt;</b></font>
-                </td>
             </tr>
         </table>
         <hr>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ae86128..1d37213 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -966,4 +966,8 @@
     <!-- Provider name for widgets -->
     <string name="widget_provider" translatable="false">com.android.mail.widget.WidgetProvider</string>
 
+    <string name="print_job_name" translatable="false">Unified Email - <xliff:g id="subject">%1$s</xliff:g></string>
+    <!-- Title to display when an email has no subject. [CHAR LIMIT=50]-->
+    <string name="no_subject">(no subject)</string>
+
 </resources>
diff --git a/src/com/android/mail/browse/EmlMessageViewFragment.java b/src/com/android/mail/browse/EmlMessageViewFragment.java
index c0f7f9b..ad22232 100644
--- a/src/com/android/mail/browse/EmlMessageViewFragment.java
+++ b/src/com/android/mail/browse/EmlMessageViewFragment.java
@@ -28,6 +28,7 @@
 import android.os.Handler;
 import android.provider.OpenableColumns;
 import android.view.LayoutInflater;
+import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.webkit.WebView;
@@ -149,6 +150,7 @@
         mWebViewClient = new EmlWebViewClient(null);
         mViewController = new SecureConversationViewController(this);
 
+        setHasOptionsMenu(true);
         getActivity().getActionBar().setTitle(R.string.attached_message);
     }
 
@@ -165,6 +167,18 @@
         mViewController.onActivityCreated(savedInstanceState);
     }
 
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        final int itemId = item.getItemId();
+        if (itemId == R.id.print_message) {
+            mViewController.printMessage();
+        } else {
+            return super.onOptionsItemSelected(item);
+        }
+
+        return true;
+    }
+
     // Start SecureConversationViewControllerCallbacks
 
     @Override
diff --git a/src/com/android/mail/browse/EmlViewerActivity.java b/src/com/android/mail/browse/EmlViewerActivity.java
index cc631c6..f40b3ef 100644
--- a/src/com/android/mail/browse/EmlViewerActivity.java
+++ b/src/com/android/mail/browse/EmlViewerActivity.java
@@ -134,7 +134,6 @@
         final int itemId = item.getItemId();
         if (itemId == android.R.id.home) {
             finish();
-            return true;
         } else if (itemId == R.id.settings) {
             Utils.showSettings(this, mAccount);
         } else if (itemId == R.id.help_info_menu_item) {
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index 677be9d..ce24bf5 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -55,12 +55,14 @@
 import com.android.mail.perf.Timer;
 import com.android.mail.photomanager.LetterTileProvider;
 import com.android.mail.preferences.AccountPreferences;
+import com.android.mail.print.PrintUtils;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Address;
 import com.android.mail.providers.Conversation;
 import com.android.mail.providers.Folder;
 import com.android.mail.providers.Message;
 import com.android.mail.providers.UIProvider;
+import com.android.mail.ui.AbstractConversationViewFragment;
 import com.android.mail.ui.ImageCanvas;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
@@ -869,14 +871,16 @@
             ComposeActivity.replyAll(getContext(), getAccount(), mMessage);
         } else if (id == R.id.forward) {
             ComposeActivity.forward(getContext(), getAccount(), mMessage);
+        } else if (id == R.id.print_message) {
+            printMessage();
         } else if (id == R.id.report_rendering_problem) {
             final String text = getContext().getString(R.string.report_rendering_problem_desc);
             ComposeActivity.reportRenderingFeedback(getContext(), getAccount(), mMessage,
-                text + "\n\n" + mCallbacks.getMessageTransforms(mMessage));
+                    text + "\n\n" + mCallbacks.getMessageTransforms(mMessage));
         } else if (id == R.id.report_rendering_improvement) {
             final String text = getContext().getString(R.string.report_rendering_improvement_desc);
             ComposeActivity.reportRenderingFeedback(getContext(), getAccount(), mMessage,
-                text + "\n\n" + mCallbacks.getMessageTransforms(mMessage));
+                    text + "\n\n" + mCallbacks.getMessageTransforms(mMessage));
         } else if (id == R.id.star) {
             final boolean newValue = !v.isSelected();
             v.setSelected(newValue);
@@ -917,8 +921,20 @@
         return handled;
     }
 
+    private void printMessage() {
+        // Secure conversation view does not use a conversation view adapter
+        // so it's safe to test for existence as a signal to use javascript or not.
+        final boolean useJavascript = mMessageHeaderItem.getAdapter() != null;
+        final Account account = mAccountController.getAccount();
+        final Conversation conversation = mMessage.getConversation();
+        final String baseUri =
+                AbstractConversationViewFragment.buildBaseUri(account, conversation);
+        PrintUtils.printMessage(getContext(), mMessage, conversation.subject,
+                mAddressCache, conversation.getBaseUri(baseUri), useJavascript);
+    }
+
     /**
-     * Set to true if the user should not be able to perfrom message actions
+     * Set to true if the user should not be able to perform message actions
      * on the message such as reply/reply all/forward/star/etc.
      *
      * Default is false.
diff --git a/src/com/android/mail/print/HtmlPrintTemplates.java b/src/com/android/mail/print/HtmlPrintTemplates.java
index 27dfba3..cf0399c 100644
--- a/src/com/android/mail/print/HtmlPrintTemplates.java
+++ b/src/com/android/mail/print/HtmlPrintTemplates.java
@@ -19,6 +19,7 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.text.TextUtils;
 
 import com.android.mail.R;
 import com.android.mail.ui.AbstractHtmlTemplates;
@@ -51,8 +52,7 @@
      * until {@link #endPrintConversation()} or {@link #endPrintConversationNoJavascript()}
      * is called.
      */
-    public void startPrintConversation(String accountName, String accountAddress,
-            String subject, int numMessages) {
+    public void startPrintConversation(String subject, int numMessages) {
         if (mInProgress) {
             throw new IllegalStateException("Should not call startPrintConversation twice");
         }
@@ -62,9 +62,11 @@
         final Resources res = mContext.getResources();
         final String numMessageString = res.getQuantityString(
                 R.plurals.num_messages, numMessages, numMessages);
+
+        final String printedSubject = TextUtils.isEmpty(subject)
+                ? res.getString(R.string.no_subject) : subject;
         append(mConversationUpper, mContext.getString(R.string.app_name),
-                accountName == null ? "" : accountName,
-                accountAddress, subject, numMessageString);
+                printedSubject, numMessageString);
 
         mInProgress = true;
     }
diff --git a/src/com/android/mail/print/Printer.java b/src/com/android/mail/print/PrintUtils.java
similarity index 66%
rename from src/com/android/mail/print/Printer.java
rename to src/com/android/mail/print/PrintUtils.java
index 0a3cd49..082bc57 100644
--- a/src/com/android/mail/print/Printer.java
+++ b/src/com/android/mail/print/PrintUtils.java
@@ -19,16 +19,20 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.print.PrintAttributes;
+import android.print.PrintManager;
 import android.text.TextUtils;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
 
 import com.android.mail.FormattedDateBuilder;
 import com.android.mail.R;
-import com.android.mail.browse.ConversationMessage;
 import com.android.mail.browse.MessageCursor;
-import com.android.mail.providers.Account;
+
 import com.android.mail.providers.Address;
 import com.android.mail.providers.Attachment;
 import com.android.mail.providers.Conversation;
+import com.android.mail.providers.Message;
 import com.android.mail.providers.UIProvider;
 import com.android.mail.utils.AttachmentUtils;
 import com.android.mail.utils.Utils;
@@ -37,17 +41,63 @@
 import java.util.Map;
 
 /**
- * Static class that provides a {@link #print} function to build a print html document.
+ * Utility class that provides utility functions to print
+ * either a conversation or message.
  */
-public class Printer {
+public class PrintUtils {
     private static final String DIV_START = "<div>";
     private static final String REPLY_TO_DIV_START = "<div class=\"replyto\">";
     private static final String DIV_END = "</div>";
 
     /**
+     * Prints an entire conversation.
+     */
+    public static void printConversation(Context context,
+            MessageCursor cursor, Map<String, Address> addressCache,
+            String baseUri, boolean useJavascript) {
+        final String convHtml = buildConversationHtml(context, cursor,
+                        addressCache, useJavascript);
+        printHtml(context, convHtml, baseUri, cursor.getConversation().subject, useJavascript);
+    }
+
+    /**
+     * Prints one message.
+     */
+    public static void printMessage(Context context, Message message, String subject,
+            Map<String, Address> addressCache, String baseUri, boolean useJavascript) {
+        final String msgHtml = buildMessageHtml(context, message,
+                subject, addressCache, useJavascript);
+        printHtml(context, msgHtml, baseUri, subject, useJavascript);
+    }
+
+    /**
+     * Prints the html provided using the framework printing APIs.
+     *
+     * Sets up a webview to perform the printing work.
+     */
+    private static void printHtml(Context context, String html,
+            String baseUri, String subject, boolean useJavascript) {
+        final WebView webView = new WebView(context);
+        final WebSettings settings = webView.getSettings();
+        settings.setBlockNetworkImage(false);
+        settings.setJavaScriptEnabled(useJavascript);
+        webView.loadDataWithBaseURL(baseUri, html,
+                "text/html", "utf-8", null);
+        final PrintManager printManager =
+                (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
+
+        final String printJobName = TextUtils.isEmpty(subject)
+                ? context.getString(R.string.app_name)
+                : context.getString(R.string.print_job_name, subject);
+        printManager.print(printJobName,
+                webView.createPrintDocumentAdapter(),
+                new PrintAttributes.Builder().build());
+    }
+
+    /**
      * Builds an html document that is suitable for printing and returns it as a {@link String}.
      */
-    public static String print(Context context, Account account,
+    private static String buildConversationHtml(Context context,
             MessageCursor cursor, Map<String, Address> addressCache, boolean useJavascript) {
         final HtmlPrintTemplates templates = new HtmlPrintTemplates(context);
         final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
@@ -56,24 +106,14 @@
             throw new IllegalStateException("trying to print without a conversation");
         }
 
-        // TODO - remove account name(not account.name which is email address) or get it somehow
         final Conversation conversation = cursor.getConversation();
-        templates.startPrintConversation("", account.name,
-                conversation.subject, conversation.getNumMessages());
+        templates.startPrintConversation(conversation.subject, conversation.getNumMessages());
 
         // for each message in the conversation, add message html
         final Resources res = context.getResources();
         do {
-            final ConversationMessage message = cursor.getMessage();
-            final Address fromAddress = Utils.getAddress(addressCache, message.getFrom());
-            final long when = message.dateReceivedMs;
-            final String date = res.getString(R.string.date_message_received_print,
-                    dateBuilder.formatLongDayAndDate(when), dateBuilder.formatLongTime(when));
-
-
-            templates.appendMessage(fromAddress.getName(), fromAddress.getAddress(), date,
-                    renderRecipients(res, addressCache, message), message.getBodyAsHtml(),
-                    renderAttachments(context, res, message));
+            final Message message = cursor.getMessage();
+            appendSingleMessageHtml(context, res, message, addressCache, templates, dateBuilder);
         } while (cursor.moveToNext());
 
         // only include JavaScript if specifically requested
@@ -82,11 +122,48 @@
     }
 
     /**
+     * Builds an html document suitable for printing and returns it as a {@link String}.
+     */
+    private static String buildMessageHtml(Context context, Message message,
+            String subject, Map<String, Address> addressCache, boolean useJavascript) {
+        final HtmlPrintTemplates templates = new HtmlPrintTemplates(context);
+        final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
+
+        templates.startPrintConversation(subject, 1 /* numMessages */);
+
+        // add message html
+        final Resources res = context.getResources();
+        appendSingleMessageHtml(context, res, message, addressCache, templates, dateBuilder);
+
+        // only include JavaScript if specifically requested
+        return useJavascript ?
+                templates.endPrintConversation() : templates.endPrintConversationNoJavascript();
+    }
+
+    /**
+     * Adds the html for a single message to the
+     * {@link HtmlPrintTemplates} provided.
+     */
+    private static void appendSingleMessageHtml(Context context, Resources res,
+            Message message, Map<String, Address> addressCache,
+            HtmlPrintTemplates templates, FormattedDateBuilder dateBuilder) {
+        final Address fromAddress = Utils.getAddress(addressCache, message.getFrom());
+        final long when = message.dateReceivedMs;
+        final String date = res.getString(R.string.date_message_received_print,
+                dateBuilder.formatLongDayAndDate(when), dateBuilder.formatLongTime(when));
+
+
+        templates.appendMessage(fromAddress.getName(), fromAddress.getAddress(), date,
+                renderRecipients(res, addressCache, message), message.getBodyAsHtml(),
+                renderAttachments(context, res, message));
+    }
+
+    /**
      * Builds html for the message header. Specifically, the (optional) lists of
      * reply-to, to, cc, and bcc.
      */
-    private static String renderRecipients(Resources res, Map<String, Address> addressCache,
-            ConversationMessage message) {
+    private static String renderRecipients(Resources res,
+            Map<String, Address> addressCache, Message message) {
         final StringBuilder recipients = new StringBuilder();
 
         // reply-to
@@ -163,7 +240,7 @@
      * Builds and returns html for a message's attachments.
      */
     private static String renderAttachments(
-            Context context, Resources resources, ConversationMessage message) {
+            Context context, Resources resources, Message message) {
         if (!message.hasAttachments) {
             return "";
         }
@@ -186,7 +263,6 @@
             sb.append("<tr><td><table cellspacing=\"0\" cellpadding=\"0\"><tr>");
 
             // TODO - thumbnail previews of images
-
             sb.append("<td><img width=\"16\" height=\"16\" src=\"file:///android_asset/images/")
                     .append(getIconFilename(attachment.getContentType()))
                     .append("\"></td><td width=\"7\"></td><td><b>")
diff --git a/src/com/android/mail/ui/AbstractConversationViewFragment.java b/src/com/android/mail/ui/AbstractConversationViewFragment.java
index fc5218f..3e48190 100644
--- a/src/com/android/mail/ui/AbstractConversationViewFragment.java
+++ b/src/com/android/mail/ui/AbstractConversationViewFragment.java
@@ -221,9 +221,13 @@
      * (such as one that does not rely on account and/or conversation.
      */
     protected void setBaseUri() {
+        mBaseUri = buildBaseUri(mAccount, mConversation);
+    }
+
+    public static String buildBaseUri(Account account, Conversation conversation) {
         // Since the uri specified in the conversation base uri may not be unique, we specify a
         // base uri that us guaranteed to be unique for this conversation.
-        mBaseUri = "x-thread://" + mAccount.getEmailAddress().hashCode() + "/" + mConversation.id;
+        return "x-thread://" + account.getEmailAddress().hashCode() + "/" + conversation.id;
     }
 
     @Override
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index 63ade39..e42b6e6 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -27,9 +27,6 @@
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.print.PrintAttributes;
-import android.print.PrintJob;
-import android.print.PrintManager;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.ScaleGestureDetector;
@@ -67,7 +64,7 @@
 import com.android.mail.browse.WebViewContextMenu;
 import com.android.mail.content.ObjectCursor;
 import com.android.mail.preferences.AccountPreferences;
-import com.android.mail.print.Printer;
+import com.android.mail.print.PrintUtils;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Address;
 import com.android.mail.providers.Conversation;
@@ -1580,17 +1577,7 @@
     }
 
     protected void printConversation() {
-        // TODO - offscreen webview stuff so that we don't clobber
-        final String convHtml =
-                Printer.print(getContext(), mAccount, getMessageCursor(),
-                        mAddressCache, true /* userJavascript */);
-        mWebView.getSettings().setBlockNetworkImage(false);
-        mWebView.loadDataWithBaseURL(
-                mConversation.getBaseUri(mBaseUri), convHtml, "text/html", "utf-8", null);
-        final PrintManager printManager =
-                (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE);
-        printManager.print(mConversation.subject,
-                mWebView.createPrintDocumentAdapter(),
-                new PrintAttributes.Builder().build());
+        PrintUtils.printConversation(getContext(), getMessageCursor(), mAddressCache,
+                mConversation.getBaseUri(mBaseUri), true /* useJavascript */);
     }
 }
diff --git a/src/com/android/mail/ui/SecureConversationViewController.java b/src/com/android/mail/ui/SecureConversationViewController.java
index 19a57e3..c65ea5a 100644
--- a/src/com/android/mail/ui/SecureConversationViewController.java
+++ b/src/com/android/mail/ui/SecureConversationViewController.java
@@ -37,6 +37,8 @@
 import com.android.mail.browse.MessageHeaderView;
 import com.android.mail.browse.MessageScrollView;
 import com.android.mail.browse.MessageWebView;
+import com.android.mail.print.PrintUtils;
+import com.android.mail.providers.Conversation;
 import com.android.mail.providers.Message;
 import com.android.mail.utils.ConversationViewUtils;
 
@@ -185,8 +187,12 @@
         mConversationHeaderView.setSubject(subject);
     }
 
-    public void printConversation() {
-        // TODO - implement this
+    public void printMessage() {
+        final Conversation conversation = mMessage.getConversation();
+        PrintUtils.printMessage(mCallbacks.getContext(), mMessage,
+                conversation != null ? conversation.subject : mMessage.subject,
+                mCallbacks.getAddressCache(), mCallbacks.getBaseUri(), false /* useJavascript */);
+
     }
 
     // Start MessageHeaderViewCallbacks implementations
diff --git a/src/com/android/mail/ui/SecureConversationViewFragment.java b/src/com/android/mail/ui/SecureConversationViewFragment.java
index 9ee32e4..051940c 100644
--- a/src/com/android/mail/ui/SecureConversationViewFragment.java
+++ b/src/com/android/mail/ui/SecureConversationViewFragment.java
@@ -267,6 +267,6 @@
     }
 
     protected void printConversation() {
-        mViewController.printConversation();
+        mViewController.printMessage();
     }
 }