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      */
printConversation(Context context, MessageCursor cursor, Map<String, Address> addressCache, String baseUri, boolean useJavascript)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      */
printMessage(Context context, Message message, String subject, Map<String, Address> addressCache, String baseUri, boolean useJavascript)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 
buildPrintJobName(Context context, String name)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"})
printHtml(Context context, String html, String baseUri, String subject, boolean useJavascript)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      */
buildConversationHtml(Context context, MessageCursor cursor, Map<String, Address> addressCache, boolean useJavascript)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      */
buildMessageHtml(Context context, Message message, String subject, Map<String, Address> addressCache, boolean useJavascript)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      */
appendSingleMessageHtml(Context context, Resources res, Message message, Map<String, Address> addressCache, HtmlPrintTemplates templates, FormattedDateBuilder dateBuilder)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      */
renderRecipients(Resources res, Map<String, Address> addressCache, Message message)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      */
buildEmailDiv(Resources res, StringBuilder recipients, String emailList, String divStart, String divEnd, int headingId)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      */
renderEmailList(Resources resources, String[] emails, Map<String, Address> addressCache)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      */
renderAttachments( Context context, Resources resources, Message message)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      */
getIconFilename(String mimeType)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