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