1 /**
2  * Copyright (c) 2011, Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.mail.compose;
17 
18 import android.content.Context;
19 import android.content.res.Resources;
20 import android.text.Html;
21 import android.text.SpannedString;
22 import android.text.TextUtils;
23 import android.util.AttributeSet;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.view.View.OnClickListener;
27 import android.webkit.WebSettings;
28 import android.webkit.WebView;
29 import android.widget.Button;
30 import android.widget.CheckBox;
31 import android.widget.LinearLayout;
32 
33 import com.android.mail.R;
34 import com.android.mail.providers.Message;
35 import com.android.mail.utils.Utils;
36 
37 import java.text.DateFormat;
38 import java.util.Date;
39 
40 /*
41  * View for displaying the quoted text in the compose screen for a reply
42  * or forward. A close button is included in the upper right to remove
43  * the quoted text from the message.
44  */
45 class QuotedTextView extends LinearLayout implements OnClickListener {
46     // HTML tags used to quote reply content
47     // The following style must be in-sync with
48     // pinto.app.MessageUtil.QUOTE_STYLE and
49     // java/com/google/caribou/ui/pinto/modules/app/messageutil.js
50     // BEG_QUOTE_BIDI is also available there when we support BIDI
51     private static final String BLOCKQUOTE_BEGIN = "<blockquote class=\"quote\" style=\""
52             + "margin:0 0 0 .8ex;" + "border-left:1px #ccc solid;" + "padding-left:1ex\">";
53     private static final String BLOCKQUOTE_END = "</blockquote>";
54     private static final String QUOTE_END = "</div>";
55 
56     // Separates the attribution headers (Subject, To, etc) from the body in
57     // quoted text.
58     private static final String HEADER_SEPARATOR = "<br type='attribution'>";
59     private static final int HEADER_SEPARATOR_LENGTH = HEADER_SEPARATOR.length();
60 
61     private CharSequence mQuotedText;
62     private WebView mQuotedTextWebView;
63     private ShowHideQuotedTextListener mShowHideListener;
64     private CheckBox mQuotedTextCheckBox;
65     private boolean mIncludeText = true;
66     private Button mRespondInlineButton;
67     private RespondInlineListener mRespondInlineListener;
68     private static String sQuoteBegin;
69 
QuotedTextView(Context context)70     public QuotedTextView(Context context) {
71         this(context, null);
72     }
73 
QuotedTextView(Context context, AttributeSet attrs)74     public QuotedTextView(Context context, AttributeSet attrs) {
75         this(context, attrs, -1);
76     }
77 
QuotedTextView(Context context, AttributeSet attrs, int defStyle)78     public QuotedTextView(Context context, AttributeSet attrs, int defStyle) {
79         super(context, attrs);
80         LayoutInflater factory = LayoutInflater.from(context);
81         factory.inflate(R.layout.quoted_text, this);
82 
83         mQuotedTextWebView = (WebView) findViewById(R.id.quoted_text_web_view);
84         Utils.restrictWebView(mQuotedTextWebView);
85         WebSettings settings = mQuotedTextWebView.getSettings();
86         settings.setBlockNetworkLoads(true);
87 
88         mQuotedTextCheckBox = (CheckBox) findViewById(R.id.hide_quoted_text);
89         mQuotedTextCheckBox.setChecked(true);
90         mQuotedTextCheckBox.setOnClickListener(this);
91         sQuoteBegin = context.getResources().getString(R.string.quote_begin);
92 
93 
94         mRespondInlineButton = (Button) findViewById(R.id.respond_inline_button);
95         if (mRespondInlineButton != null) {
96             mRespondInlineButton.setEnabled(false);
97         }
98     }
99 
onDestroy()100     public void onDestroy() {
101         if (mQuotedTextWebView != null) {
102             mQuotedTextWebView.destroy();
103         }
104     }
105 
106     /**
107      * Allow the user to include quoted text.
108      * @param allow
109      */
allowQuotedText(boolean allow)110     public void allowQuotedText(boolean allow) {
111         if (mQuotedTextCheckBox != null) {
112             mQuotedTextCheckBox.setVisibility(allow ? View.VISIBLE : View.INVISIBLE);
113         }
114     }
115 
116     /**
117      * Allow the user to respond inline.
118      * @param allow
119      */
allowRespondInline(boolean allow)120     public void allowRespondInline(boolean allow) {
121         if (mRespondInlineButton != null) {
122             mRespondInlineButton.setVisibility(allow? View.VISIBLE : View.GONE);
123         }
124     }
125 
126     /**
127      * Returns the quoted text if the user hasn't dismissed it, otherwise
128      * returns null.
129      */
getQuotedTextIfIncluded()130     public CharSequence getQuotedTextIfIncluded() {
131         if (mIncludeText) {
132             return mQuotedText;
133         }
134         return null;
135     }
136 
137     /**
138      * Always returns the quoted text.
139      */
getQuotedText()140     public CharSequence getQuotedText() {
141         return mQuotedText;
142     }
143 
144     /**
145      * @return whether or not the user has selected to include quoted text.
146      */
isTextIncluded()147     public boolean isTextIncluded() {
148         return mIncludeText;
149     }
150 
setShowHideListener(ShowHideQuotedTextListener listener)151     public void setShowHideListener(ShowHideQuotedTextListener listener) {
152         mShowHideListener = listener;
153     }
154 
155 
setRespondInlineListener(RespondInlineListener listener)156     public void setRespondInlineListener(RespondInlineListener listener) {
157         mRespondInlineListener = listener;
158     }
159 
160     @Override
onClick(View v)161     public void onClick(View v) {
162         final int id = v.getId();
163 
164         if (id == R.id.respond_inline_button) {
165             respondInline();
166         } else if (id == R.id.hide_quoted_text) {
167             updateCheckedState(mQuotedTextCheckBox.isChecked());
168         }
169     }
170 
171     /**
172      * Update the state of the checkbox for the QuotedTextView as if it were
173      * tapped by the user. Also updates the visibility of the QuotedText area.
174      * @param checked Either true or false.
175      */
updateCheckedState(boolean checked)176     public void updateCheckedState(boolean checked) {
177         mQuotedTextCheckBox.setChecked(checked);
178         updateQuotedTextVisibility(checked);
179         if (mShowHideListener != null) {
180             mShowHideListener.onShowHideQuotedText(checked);
181         }
182     }
183 
updateQuotedTextVisibility(boolean show)184     private void updateQuotedTextVisibility(boolean show) {
185         mQuotedTextWebView.setVisibility(show ? View.VISIBLE : View.GONE);
186         mIncludeText = show;
187     }
188 
populateData()189     private void populateData() {
190         String fontColor = getContext().getResources().getString(
191                 R.string.quoted_text_font_color_string);
192         String html = "<head><style type=\"text/css\">* body { color: " +
193                 fontColor + "; }</style></head>" + mQuotedText.toString();
194         mQuotedTextWebView.loadDataWithBaseURL(null, html, "text/html", "utf-8", null);
195     }
196 
respondInline()197     private void respondInline() {
198         // Copy the text in the quoted message to the body of the
199         // message after stripping the html.
200         final String plainText = Utils.convertHtmlToPlainText(getQuotedText().toString());
201         if (mRespondInlineListener != null) {
202             mRespondInlineListener.onRespondInline("\n" + plainText);
203         }
204         // Set quoted text to unchecked and not visible.
205         updateCheckedState(false);
206         mRespondInlineButton.setVisibility(View.GONE);
207         // Hide everything to do with quoted text.
208         View quotedTextView = findViewById(R.id.quoted_text_area);
209         if (quotedTextView != null) {
210             quotedTextView.setVisibility(View.GONE);
211         }
212     }
213 
214     /**
215      * Interface for listeners that want to be notified when quoted text
216      * is shown / hidden.
217      */
218     public interface ShowHideQuotedTextListener {
onShowHideQuotedText(boolean show)219         public void onShowHideQuotedText(boolean show);
220     }
221 
222     /**
223      * Interface for listeners that want to be notified when the user
224      * chooses to respond inline.
225      */
226     public interface RespondInlineListener {
onRespondInline(String text)227         public void onRespondInline(String text);
228     }
229 
getHtmlText(Message message)230     private static String getHtmlText(Message message) {
231         if (message.bodyHtml != null) {
232             return message.bodyHtml;
233         } else if (message.bodyText != null) {
234             // STOPSHIP Sanitize this
235             return Html.toHtml(new SpannedString(message.bodyText));
236         } else {
237             return "";
238         }
239     }
240 
setQuotedText(int action, Message refMessage, boolean allow)241     public void setQuotedText(int action, Message refMessage, boolean allow) {
242         setVisibility(View.VISIBLE);
243         String htmlText = getHtmlText(refMessage);
244         StringBuilder quotedText = new StringBuilder();
245         DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT);
246         Date date = new Date(refMessage.dateReceivedMs);
247         Resources resources = getContext().getResources();
248         if (action == ComposeActivity.REPLY || action == ComposeActivity.REPLY_ALL) {
249             quotedText.append(sQuoteBegin);
250             quotedText
251                     .append(String.format(
252                             resources.getString(R.string.reply_attribution),
253                             dateFormat.format(date),
254                             Utils.cleanUpString(
255                                     refMessage.getFrom(), true)));
256             quotedText.append(HEADER_SEPARATOR);
257             quotedText.append(BLOCKQUOTE_BEGIN);
258             quotedText.append(htmlText);
259             quotedText.append(BLOCKQUOTE_END);
260             quotedText.append(QUOTE_END);
261         } else if (action == ComposeActivity.FORWARD) {
262             quotedText.append(sQuoteBegin);
263             quotedText
264                     .append(String.format(resources.getString(R.string.forward_attribution), Utils
265                             .cleanUpString(refMessage.getFrom(),
266                                     true /* remove empty quotes */), dateFormat.format(date), Utils
267                             .cleanUpString(refMessage.subject,
268                                     false /* don't remove empty quotes */), Utils.cleanUpString(
269                             refMessage.getTo(), true)));
270             String ccAddresses = refMessage.getCc();
271             quotedText.append(String.format(resources.getString(R.string.cc_attribution),
272                     Utils.cleanUpString(ccAddresses, true /* remove empty quotes */)));
273             quotedText.append(HEADER_SEPARATOR);
274             quotedText.append(BLOCKQUOTE_BEGIN);
275             quotedText.append(htmlText);
276             quotedText.append(BLOCKQUOTE_END);
277             quotedText.append(QUOTE_END);
278         }
279         setQuotedText(quotedText);
280         allowQuotedText(allow);
281         // If there is quoted text, we always allow respond inline, since this
282         // may be a forward.
283         allowRespondInline(true);
284     }
285 
setQuotedTextFromDraft(CharSequence htmlText, boolean forward)286     public void setQuotedTextFromDraft(CharSequence htmlText, boolean forward) {
287         setVisibility(View.VISIBLE);
288         setQuotedText(htmlText);
289         allowQuotedText(!forward);
290         // If there is quoted text, we always allow respond inline, since this
291         // may be a forward.
292         allowRespondInline(true);
293     }
294 
setQuotedTextFromHtml(CharSequence htmlText, boolean shouldQuoteText)295     public void setQuotedTextFromHtml(CharSequence htmlText, boolean shouldQuoteText) {
296         setVisibility(VISIBLE);
297         if (shouldQuoteText) {
298             final StringBuilder quotedText = new StringBuilder();
299             final Resources resources = getContext().getResources();
300             quotedText.append(sQuoteBegin);
301             quotedText.append(
302                     String.format(resources.getString(R.string.forward_attribution_no_headers)));
303             quotedText.append(HEADER_SEPARATOR);
304             quotedText.append(BLOCKQUOTE_BEGIN);
305             quotedText.append(htmlText);
306             quotedText.append(BLOCKQUOTE_END);
307             quotedText.append(QUOTE_END);
308             setQuotedText(quotedText);
309         } else {
310             setQuotedText(htmlText);
311         }
312         findViewById(R.id.divider_bar).setVisibility(GONE);
313         findViewById(R.id.quoted_text_button_bar).setVisibility(GONE);
314     }
315     /**
316      * Set quoted text. Some use cases may not want to display the check box (i.e. forwarding) so
317      * allow control of that.
318      */
setQuotedText(CharSequence quotedText)319     private void setQuotedText(CharSequence quotedText) {
320         mQuotedText = quotedText;
321         populateData();
322         if (mRespondInlineButton != null) {
323             if (!TextUtils.isEmpty(quotedText)) {
324                 mRespondInlineButton.setVisibility(View.VISIBLE);
325                 mRespondInlineButton.setEnabled(true);
326                 mRespondInlineButton.setOnClickListener(this);
327             } else {
328                 // No text to copy; disable the respond inline button.
329                 mRespondInlineButton.setVisibility(View.GONE);
330                 mRespondInlineButton.setEnabled(false);
331             }
332         }
333     }
334 
containsQuotedText(String text)335     public static boolean containsQuotedText(String text) {
336         int pos = text.indexOf(sQuoteBegin);
337         return pos >= 0;
338     }
339 
340     /**
341      * Returns the index of the actual quoted text and NOT the meta information such as:
342      * "On April 4, 2013 Joe Smith <jsmith@example.com> wrote:" that is part of the original
343      * message when replying and including the original text.
344      * @param text HTML text that includes quoted text.
345      * @return The offset found.
346      */
getQuotedTextOffset(String text)347     public static int getQuotedTextOffset(String text) {
348         return text.indexOf(QuotedTextView.HEADER_SEPARATOR)
349                 + QuotedTextView.HEADER_SEPARATOR_LENGTH;
350     }
351 
352     /**
353      * Find the index of where the entire block of quoted text, quotes, divs,
354      * attribution and all, begins.
355      */
findQuotedTextIndex(CharSequence htmlText)356     public static int findQuotedTextIndex(CharSequence htmlText) {
357         if (TextUtils.isEmpty(htmlText)) {
358             return -1;
359         }
360         String textString = htmlText.toString();
361         return textString.indexOf(sQuoteBegin);
362     }
363 
setUpperDividerVisible(boolean visible)364     public void setUpperDividerVisible(boolean visible) {
365         findViewById(R.id.upper_quotedtext_divider_bar).setVisibility(
366                 visible ? View.VISIBLE : View.GONE);
367     }
368 }
369