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