Home | History | Annotate | Download | only in print
      1 /**
      2  * Copyright (C) 2013 Google Inc.
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.mail.print;
     19 
     20 import android.annotation.SuppressLint;
     21 import android.content.Context;
     22 import android.content.res.Resources;
     23 import android.print.PrintAttributes;
     24 import android.print.PrintManager;
     25 import android.text.TextUtils;
     26 import android.webkit.WebSettings;
     27 import android.webkit.WebView;
     28 
     29 import com.android.emailcommon.mail.Address;
     30 import com.android.mail.FormattedDateBuilder;
     31 import com.android.mail.R;
     32 import com.android.mail.browse.MessageCursor;
     33 
     34 import com.android.mail.providers.Attachment;
     35 import com.android.mail.providers.Conversation;
     36 import com.android.mail.providers.Message;
     37 import com.android.mail.providers.UIProvider;
     38 import com.android.mail.utils.AttachmentUtils;
     39 import com.android.mail.utils.Utils;
     40 
     41 import java.util.List;
     42 import java.util.Map;
     43 
     44 /**
     45  * Utility class that provides utility functions to print
     46  * either a conversation or message.
     47  */
     48 public class PrintUtils {
     49     private static final String DIV_START = "<div>";
     50     private static final String REPLY_TO_DIV_START = "<div class=\"replyto\">";
     51     private static final String DIV_END = "</div>";
     52 
     53     /**
     54      * Prints an entire conversation.
     55      */
     56     public static void printConversation(Context context,
     57             MessageCursor cursor, Map<String, Address> addressCache,
     58             String baseUri, boolean useJavascript) {
     59         if (cursor == null) {
     60             return;
     61         }
     62         final String convHtml = buildConversationHtml(context, cursor,
     63                         addressCache, useJavascript);
     64         printHtml(context, convHtml, baseUri, cursor.getConversation().subject, useJavascript);
     65     }
     66 
     67     /**
     68      * Prints one message.
     69      */
     70     public static void printMessage(Context context, Message message, String subject,
     71             Map<String, Address> addressCache, String baseUri, boolean useJavascript) {
     72         final String msgHtml = buildMessageHtml(context, message,
     73                 subject, addressCache, useJavascript);
     74         printHtml(context, msgHtml, baseUri, subject, useJavascript);
     75     }
     76 
     77     public static String buildPrintJobName(Context context, String name) {
     78         return TextUtils.isEmpty(name)
     79                 ? context.getString(R.string.app_name)
     80                 : context.getString(R.string.print_job_name, name);
     81     }
     82 
     83     /**
     84      * Prints the html provided using the framework printing APIs.
     85      *
     86      * Sets up a webview to perform the printing work.
     87      */
     88     @SuppressLint({"NewApi", "SetJavaScriptEnabled"})
     89     private static void printHtml(Context context, String html,
     90             String baseUri, String subject, boolean useJavascript) {
     91         final WebView webView = new WebView(context);
     92         final WebSettings settings = webView.getSettings();
     93         settings.setBlockNetworkImage(false);
     94         settings.setJavaScriptEnabled(useJavascript);
     95         webView.loadDataWithBaseURL(baseUri, html, "text/html", "utf-8", null);
     96         final PrintManager printManager =
     97                 (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
     98 
     99         final String printJobName = buildPrintJobName(context, subject);
    100         printManager.print(printJobName,
    101                 Utils.isRunningLOrLater() ?
    102                         webView.createPrintDocumentAdapter(printJobName) :
    103                         webView.createPrintDocumentAdapter(),
    104                 new PrintAttributes.Builder().build());
    105     }
    106 
    107     /**
    108      * Builds an html document that is suitable for printing and returns it as a {@link String}.
    109      */
    110     private static String buildConversationHtml(Context context,
    111             MessageCursor cursor, Map<String, Address> addressCache, boolean useJavascript) {
    112         final HtmlPrintTemplates templates = new HtmlPrintTemplates(context);
    113         final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
    114 
    115         if (!cursor.moveToFirst()) {
    116             throw new IllegalStateException("trying to print without a conversation");
    117         }
    118 
    119         final Conversation conversation = cursor.getConversation();
    120         templates.startPrintConversation(conversation.subject, conversation.getNumMessages());
    121 
    122         // for each message in the conversation, add message html
    123         final Resources res = context.getResources();
    124         do {
    125             final Message message = cursor.getMessage();
    126             appendSingleMessageHtml(context, res, message, addressCache, templates, dateBuilder);
    127         } while (cursor.moveToNext());
    128 
    129         // only include JavaScript if specifically requested
    130         return useJavascript ?
    131                 templates.endPrintConversation() : templates.endPrintConversationNoJavascript();
    132     }
    133 
    134     /**
    135      * Builds an html document suitable for printing and returns it as a {@link String}.
    136      */
    137     private static String buildMessageHtml(Context context, Message message,
    138             String subject, Map<String, Address> addressCache, boolean useJavascript) {
    139         final HtmlPrintTemplates templates = new HtmlPrintTemplates(context);
    140         final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
    141 
    142         templates.startPrintConversation(subject, 1 /* numMessages */);
    143 
    144         // add message html
    145         final Resources res = context.getResources();
    146         appendSingleMessageHtml(context, res, message, addressCache, templates, dateBuilder);
    147 
    148         // only include JavaScript if specifically requested
    149         return useJavascript ?
    150                 templates.endPrintConversation() : templates.endPrintConversationNoJavascript();
    151     }
    152 
    153     /**
    154      * Adds the html for a single message to the
    155      * {@link HtmlPrintTemplates} provided.
    156      */
    157     private static void appendSingleMessageHtml(Context context, Resources res,
    158             Message message, Map<String, Address> addressCache,
    159             HtmlPrintTemplates templates, FormattedDateBuilder dateBuilder) {
    160         final Address fromAddress = Utils.getAddress(addressCache, message.getFrom());
    161         final long when = message.dateReceivedMs;
    162         final String date = dateBuilder.formatDateTimeForPrinting(when);
    163 
    164         templates.appendMessage(fromAddress == null ? "" : fromAddress.getPersonal(),
    165                 fromAddress == null ? "" : fromAddress.getAddress(), date,
    166                 renderRecipients(res, addressCache, message), message.getBodyAsHtml(),
    167                 renderAttachments(context, res, message));
    168     }
    169 
    170     /**
    171      * Builds html for the message header. Specifically, the (optional) lists of
    172      * reply-to, to, cc, and bcc.
    173      */
    174     private static String renderRecipients(Resources res,
    175             Map<String, Address> addressCache, Message message) {
    176         final StringBuilder recipients = new StringBuilder();
    177 
    178         // reply-to
    179         final String replyTo = renderEmailList(res, message.getReplyToAddresses(), addressCache);
    180         buildEmailDiv(res, recipients, replyTo, REPLY_TO_DIV_START, DIV_END,
    181                 R.string.replyto_heading);
    182 
    183         // to
    184         // To has special semantics since the message can be a draft.
    185         // If it is a draft and there are no to addresses, we just print "Draft".
    186         // If it is a draft and there are to addresses, we print "Draft To: "
    187         // If not a draft, we just use "To: ".
    188         final boolean isDraft = message.draftType != UIProvider.DraftType.NOT_A_DRAFT;
    189         final String to = renderEmailList(res, message.getToAddresses(), addressCache);
    190         if (isDraft && to == null) {
    191             recipients.append(DIV_START).append(res.getString(R.string.draft_heading))
    192                     .append(DIV_END);
    193         } else {
    194             buildEmailDiv(res, recipients, to, DIV_START, DIV_END,
    195                     isDraft ? R.string.draft_to_heading : R.string.to_heading_no_space);
    196         }
    197 
    198         // cc
    199         final String cc = renderEmailList(res, message.getCcAddresses(), addressCache);
    200         buildEmailDiv(res, recipients, cc, DIV_START, DIV_END,
    201                 R.string.cc_heading);
    202 
    203         // bcc
    204         final String bcc = renderEmailList(res, message.getBccAddresses(), addressCache);
    205         buildEmailDiv(res, recipients, bcc, DIV_START, DIV_END,
    206                 R.string.bcc_heading);
    207 
    208         return recipients.toString();
    209     }
    210 
    211     /**
    212      * Appends an html div containing a list of emails based on the passed in data.
    213      */
    214     private static void buildEmailDiv(Resources res, StringBuilder recipients, String emailList,
    215             String divStart, String divEnd, int headingId) {
    216         if (emailList != null) {
    217             recipients.append(divStart).append(res.getString(headingId))
    218                     .append('\u0020').append(emailList).append(divEnd);
    219         }
    220     }
    221 
    222     /**
    223      * Builds and returns a list of comma-separated emails of the form "Name &lt;email&gt;".
    224      * If the email does not contain a name, "email" is used instead.
    225      */
    226     private static String renderEmailList(Resources resources, String[] emails,
    227             Map<String, Address> addressCache) {
    228         if (emails == null || emails.length == 0) {
    229             return null;
    230         }
    231         final String[] formattedEmails = new String[emails.length];
    232         for (int i = 0; i < emails.length; i++) {
    233             final Address email = Utils.getAddress(addressCache, emails[i]);
    234             final String name = email.getPersonal();
    235             final String address = email.getAddress();
    236 
    237             if (TextUtils.isEmpty(name)) {
    238                 formattedEmails[i] = address;
    239             } else {
    240                 formattedEmails[i] = resources.getString(R.string.address_print_display_format,
    241                         name, address);
    242             }
    243         }
    244 
    245         return TextUtils.join(resources.getString(R.string.enumeration_comma), formattedEmails);
    246     }
    247 
    248     /**
    249      * Builds and returns html for a message's attachments.
    250      */
    251     private static String renderAttachments(
    252             Context context, Resources resources, Message message) {
    253         if (!message.hasAttachments) {
    254             return "";
    255         }
    256 
    257         final int numAttachments = message.getAttachmentCount(false /* includeInline */);
    258 
    259         // if we have no attachments after filtering out inline attachments, return.
    260         if (numAttachments == 0) {
    261             return "";
    262         }
    263 
    264         final StringBuilder sb = new StringBuilder("<br clear=all>"
    265                 + "<div style=\"width:50%;border-top:2px #AAAAAA solid\"></div>"
    266                 + "<table class=att cellspacing=0 cellpadding=5 border=0>");
    267 
    268         // If the message has more than one attachment, list the number of attachments.
    269         if (numAttachments > 1) {
    270             sb.append("<tr><td colspan=2><b style=\"padding-left:3\">")
    271                     .append(resources.getQuantityString(
    272                             R.plurals.num_attachments, numAttachments, numAttachments))
    273                     .append("</b></td></tr>");
    274         }
    275 
    276         final List<Attachment> attachments = message.getAttachments();
    277         for (int i = 0, size = attachments.size(); i < size; i++) {
    278             final Attachment attachment = attachments.get(i);
    279             // skip inline attachments
    280             if (attachment.isInlineAttachment()) {
    281                 continue;
    282             }
    283             sb.append("<tr><td><table cellspacing=\"0\" cellpadding=\"0\"><tr>");
    284 
    285             // TODO - thumbnail previews of images
    286             sb.append("<td><img width=\"16\" height=\"16\" src=\"file:///android_asset/images/")
    287                     .append(getIconFilename(attachment.getContentType()))
    288                     .append("\"></td><td width=\"7\"></td><td><b>")
    289                     .append(attachment.getName())
    290                     .append("</b><br>").append(
    291                     AttachmentUtils.convertToHumanReadableSize(context, attachment.size))
    292                     .append("</td></tr></table></td></tr>");
    293         }
    294 
    295         sb.append("</table>");
    296 
    297         return sb.toString();
    298     }
    299 
    300     /**
    301      * Returns an appropriate filename for various attachment mime types.
    302      */
    303     private static String getIconFilename(String mimeType) {
    304         if (mimeType.startsWith("application/msword") ||
    305                 mimeType.startsWith("application/vnd.oasis.opendocument.text") ||
    306                 mimeType.equals("application/rtf") ||
    307                 mimeType.equals("application/"
    308                         + "vnd.openxmlformats-officedocument.wordprocessingml.document")) {
    309             return "doc.gif";
    310         } else if (mimeType.startsWith("image/")) {
    311             return "graphic.gif";
    312         } else if (mimeType.startsWith("text/html")) {
    313             return "html.gif";
    314         } else if (mimeType.startsWith("application/pdf")) {
    315             return "pdf.gif";
    316         } else if (mimeType.endsWith("powerpoint") ||
    317                 mimeType.equals("application/vnd.oasis.opendocument.presentation") ||
    318                 mimeType.equals("application/"
    319                         + "vnd.openxmlformats-officedocument.presentationml.presentation")) {
    320             return "ppt.gif";
    321         } else if ((mimeType.startsWith("audio/")) ||
    322                 (mimeType.startsWith("music/"))) {
    323             return "sound.gif";
    324         } else if (mimeType.startsWith("text/plain")) {
    325             return "txt.gif";
    326         } else if (mimeType.endsWith("excel") ||
    327                 mimeType.equals("application/vnd.oasis.opendocument.spreadsheet") ||
    328                 mimeType.equals("application/"
    329                         + "vnd.openxmlformats-officedocument.spreadsheetml.sheet")) {
    330             return "xls.gif";
    331         } else if ((mimeType.endsWith("zip")) ||
    332                 (mimeType.endsWith("/x-compress")) ||
    333                 (mimeType.endsWith("/x-compressed"))) {
    334             return "zip.gif";
    335         } else {
    336             return "generic.gif";
    337         }
    338     }
    339 }
    340