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
18package com.android.mail.print;
19
20import android.annotation.SuppressLint;
21import android.content.Context;
22import android.content.res.Resources;
23import android.print.PrintAttributes;
24import android.print.PrintManager;
25import android.text.TextUtils;
26import android.webkit.WebSettings;
27import android.webkit.WebView;
28
29import com.android.emailcommon.mail.Address;
30import com.android.mail.FormattedDateBuilder;
31import com.android.mail.R;
32import com.android.mail.browse.MessageCursor;
33
34import com.android.mail.providers.Attachment;
35import com.android.mail.providers.Conversation;
36import com.android.mail.providers.Message;
37import com.android.mail.providers.UIProvider;
38import com.android.mail.utils.AttachmentUtils;
39import com.android.mail.utils.Utils;
40
41import java.util.List;
42import java.util.Map;
43
44/**
45 * Utility class that provides utility functions to print
46 * either a conversation or message.
47 */
48public 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