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 <email>". 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