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 mShowHideCheckBox;
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        mShowHideCheckBox = (CheckBox) findViewById(R.id.hide_quoted_text);
89        mShowHideCheckBox.setChecked(true);
90        mShowHideCheckBox.setOnClickListener(this);
91        sQuoteBegin = context.getResources().getString(R.string.quote_begin);
92        findViewById(R.id.hide_quoted_text_label).setOnClickListener(this);
93
94
95        mRespondInlineButton = (Button) findViewById(R.id.respond_inline_button);
96        if (mRespondInlineButton != null) {
97            mRespondInlineButton.setEnabled(false);
98        }
99    }
100
101    public void onDestroy() {
102        if (mQuotedTextWebView != null) {
103            mQuotedTextWebView.destroy();
104        }
105    }
106
107    /**
108     * Allow the user to include quoted text.
109     * @param allow
110     */
111    public void allowQuotedText(boolean allow) {
112        View quotedTextRow = findViewById(R.id.quoted_text_row);
113        if (quotedTextRow != null) {
114            quotedTextRow.setVisibility(allow? View.VISIBLE: View.INVISIBLE);
115        }
116    }
117
118    /**
119     * Allow the user to respond inline.
120     * @param allow
121     */
122    public void allowRespondInline(boolean allow) {
123        if (mRespondInlineButton != null) {
124            mRespondInlineButton.setVisibility(allow? View.VISIBLE : View.GONE);
125        }
126    }
127
128    /**
129     * Returns the quoted text if the user hasn't dismissed it, otherwise
130     * returns null.
131     */
132    public CharSequence getQuotedTextIfIncluded() {
133        if (mIncludeText) {
134            return mQuotedText;
135        }
136        return null;
137    }
138
139    /**
140     * Always returns the quoted text.
141     */
142    public CharSequence getQuotedText() {
143        return mQuotedText;
144    }
145
146    /**
147     * @return whether or not the user has selected to include quoted text.
148     */
149    public boolean isTextIncluded() {
150        return mIncludeText;
151    }
152
153    public void setShowHideListener(ShowHideQuotedTextListener listener) {
154        mShowHideListener = listener;
155    }
156
157
158    public void setRespondInlineListener(RespondInlineListener listener) {
159        mRespondInlineListener = listener;
160    }
161
162    @Override
163    public void onClick(View v) {
164        final int id = v.getId();
165
166        if (id == R.id.respond_inline_button) {
167            respondInline();
168        } else if (id == R.id.hide_quoted_text) {
169            updateCheckedState(mShowHideCheckBox.isChecked());
170        } else if (id == R.id.hide_quoted_text_label) {
171            updateCheckedState(!mShowHideCheckBox.isChecked());
172        }
173    }
174
175    /**
176     * Update the state of the checkbox for the QuotedTextView as if it were
177     * tapped by the user. Also updates the visibility of the QuotedText area.
178     * @param checked Either true or false.
179     */
180    public void updateCheckedState(boolean checked) {
181        mShowHideCheckBox.setChecked(checked);
182        updateQuotedTextVisibility(checked);
183        if (mShowHideListener != null) {
184            mShowHideListener.onShowHideQuotedText(checked);
185        }
186    }
187
188    private void updateQuotedTextVisibility(boolean show) {
189        mQuotedTextWebView.setVisibility(show ? View.VISIBLE : View.GONE);
190        mIncludeText = show;
191    }
192
193    private void populateData() {
194        String backgroundColor = getContext().getResources().getString(
195                R.string.quoted_text_background_color_string);
196        String fontColor = getContext().getResources().getString(
197                R.string.quoted_text_font_color_string);
198        String html = "<head><style type=\"text/css\">* body { background-color: "
199                + backgroundColor + "; color: " + fontColor + "; }</style></head>"
200                + mQuotedText.toString();
201        mQuotedTextWebView.loadDataWithBaseURL(null, html, "text/html", "utf-8", null);
202    }
203
204    private void respondInline() {
205        // Copy the text in the quoted message to the body of the
206        // message after stripping the html.
207        final String plainText = Utils.convertHtmlToPlainText(getQuotedText().toString());
208        if (mRespondInlineListener != null) {
209            mRespondInlineListener.onRespondInline("\n" + plainText);
210        }
211        // Set quoted text to unchecked and not visible.
212        updateCheckedState(false);
213        mRespondInlineButton.setVisibility(View.GONE);
214        // Hide everything to do with quoted text.
215        View quotedTextView = findViewById(R.id.quoted_text_area);
216        if (quotedTextView != null) {
217            quotedTextView.setVisibility(View.GONE);
218        }
219    }
220
221    /**
222     * Interface for listeners that want to be notified when quoted text
223     * is shown / hidden.
224     */
225    public interface ShowHideQuotedTextListener {
226        public void onShowHideQuotedText(boolean show);
227    }
228
229    /**
230     * Interface for listeners that want to be notified when the user
231     * chooses to respond inline.
232     */
233    public interface RespondInlineListener {
234        public void onRespondInline(String text);
235    }
236
237    private static String getHtmlText(Message message) {
238        if (message.bodyHtml != null) {
239            return message.bodyHtml;
240        } else if (message.bodyText != null) {
241            // STOPSHIP Sanitize this
242            return Html.toHtml(new SpannedString(message.bodyText));
243        } else {
244            return "";
245        }
246    }
247
248    public void setQuotedText(int action, Message refMessage, boolean allow) {
249        setVisibility(View.VISIBLE);
250        String htmlText = getHtmlText(refMessage);
251        StringBuilder quotedText = new StringBuilder();
252        DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT);
253        Date date = new Date(refMessage.dateReceivedMs);
254        Resources resources = getContext().getResources();
255        if (action == ComposeActivity.REPLY || action == ComposeActivity.REPLY_ALL) {
256            quotedText.append(sQuoteBegin);
257            quotedText
258                    .append(String.format(
259                            resources.getString(R.string.reply_attribution),
260                            dateFormat.format(date),
261                            Utils.cleanUpString(
262                                    refMessage.getFrom(), true)));
263            quotedText.append(HEADER_SEPARATOR);
264            quotedText.append(BLOCKQUOTE_BEGIN);
265            quotedText.append(htmlText);
266            quotedText.append(BLOCKQUOTE_END);
267            quotedText.append(QUOTE_END);
268        } else if (action == ComposeActivity.FORWARD) {
269            quotedText.append(sQuoteBegin);
270            quotedText
271                    .append(String.format(resources.getString(R.string.forward_attribution), Utils
272                            .cleanUpString(refMessage.getFrom(),
273                                    true /* remove empty quotes */), dateFormat.format(date), Utils
274                            .cleanUpString(refMessage.subject,
275                                    false /* don't remove empty quotes */), Utils.cleanUpString(
276                            refMessage.getTo(), true)));
277            String ccAddresses = refMessage.getCc();
278            quotedText.append(String.format(resources.getString(R.string.cc_attribution),
279                    Utils.cleanUpString(ccAddresses, true /* remove empty quotes */)));
280            quotedText.append(HEADER_SEPARATOR);
281            quotedText.append(BLOCKQUOTE_BEGIN);
282            quotedText.append(htmlText);
283            quotedText.append(BLOCKQUOTE_END);
284            quotedText.append(QUOTE_END);
285        }
286        setQuotedText(quotedText);
287        allowQuotedText(allow);
288        // If there is quoted text, we always allow respond inline, since this
289        // may be a forward.
290        allowRespondInline(true);
291    }
292
293    public void setQuotedTextFromDraft(CharSequence htmlText, boolean forward) {
294        setVisibility(View.VISIBLE);
295        setQuotedText(htmlText);
296        allowQuotedText(!forward);
297        // If there is quoted text, we always allow respond inline, since this
298        // may be a forward.
299        allowRespondInline(true);
300    }
301
302    public void setQuotedTextFromHtml(CharSequence htmlText, boolean shouldQuoteText) {
303        setVisibility(VISIBLE);
304        if (shouldQuoteText) {
305            final StringBuilder quotedText = new StringBuilder();
306            final Resources resources = getContext().getResources();
307            quotedText.append(sQuoteBegin);
308            quotedText.append(
309                    String.format(resources.getString(R.string.forward_attribution_no_headers)));
310            quotedText.append(HEADER_SEPARATOR);
311            quotedText.append(BLOCKQUOTE_BEGIN);
312            quotedText.append(htmlText);
313            quotedText.append(BLOCKQUOTE_END);
314            quotedText.append(QUOTE_END);
315            setQuotedText(quotedText);
316        } else {
317            setQuotedText(htmlText);
318        }
319        findViewById(R.id.divider_bar).setVisibility(GONE);
320        findViewById(R.id.quoted_text_button_bar).setVisibility(GONE);
321    }
322    /**
323     * Set quoted text. Some use cases may not want to display the check box (i.e. forwarding) so
324     * allow control of that.
325     */
326    private void setQuotedText(CharSequence quotedText) {
327        mQuotedText = quotedText;
328        populateData();
329        if (mRespondInlineButton != null) {
330            if (!TextUtils.isEmpty(quotedText)) {
331                mRespondInlineButton.setVisibility(View.VISIBLE);
332                mRespondInlineButton.setEnabled(true);
333                mRespondInlineButton.setOnClickListener(this);
334            } else {
335                // No text to copy; disable the respond inline button.
336                mRespondInlineButton.setVisibility(View.GONE);
337                mRespondInlineButton.setEnabled(false);
338            }
339        }
340    }
341
342    public static boolean containsQuotedText(String text) {
343        int pos = text.indexOf(sQuoteBegin);
344        return pos >= 0;
345    }
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