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 */
16package com.android.mail.compose;
17
18import android.content.Context;
19import android.content.res.Resources;
20import android.text.Html;
21import android.text.SpannedString;
22import android.text.TextUtils;
23import android.util.AttributeSet;
24import android.view.LayoutInflater;
25import android.view.View;
26import android.view.View.OnClickListener;
27import android.webkit.WebSettings;
28import android.webkit.WebView;
29import android.widget.Button;
30import android.widget.CheckBox;
31import android.widget.LinearLayout;
32
33import com.android.mail.R;
34import com.android.mail.providers.Message;
35import com.android.mail.utils.Utils;
36
37import java.text.DateFormat;
38import 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 */
45class 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
70    public QuotedTextView(Context context) {
71        this(context, null);
72    }
73
74    public QuotedTextView(Context context, AttributeSet attrs) {
75        this(context, attrs, -1);
76    }
77
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
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     */
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     */
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     */
130    public CharSequence getQuotedTextIfIncluded() {
131        if (mIncludeText) {
132            return mQuotedText;
133        }
134        return null;
135    }
136
137    /**
138     * Always returns the quoted text.
139     */
140    public CharSequence getQuotedText() {
141        return mQuotedText;
142    }
143
144    /**
145     * @return whether or not the user has selected to include quoted text.
146     */
147    public boolean isTextIncluded() {
148        return mIncludeText;
149    }
150
151    public void setShowHideListener(ShowHideQuotedTextListener listener) {
152        mShowHideListener = listener;
153    }
154
155
156    public void setRespondInlineListener(RespondInlineListener listener) {
157        mRespondInlineListener = listener;
158    }
159
160    @Override
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     */
176    public void updateCheckedState(boolean checked) {
177        mQuotedTextCheckBox.setChecked(checked);
178        updateQuotedTextVisibility(checked);
179        if (mShowHideListener != null) {
180            mShowHideListener.onShowHideQuotedText(checked);
181        }
182    }
183
184    private void updateQuotedTextVisibility(boolean show) {
185        mQuotedTextWebView.setVisibility(show ? View.VISIBLE : View.GONE);
186        mIncludeText = show;
187    }
188
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
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 {
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 {
227        public void onRespondInline(String text);
228    }
229
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
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
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
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     */
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
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     */
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     */
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
364    public void setUpperDividerVisible(boolean visible) {
365        findViewById(R.id.upper_quotedtext_divider_bar).setVisibility(
366                visible ? View.VISIBLE : View.GONE);
367    }
368}
369