1fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann/* 2fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann * Copyright (C) 2011 The Android Open Source Project 3fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann * 4fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann * Licensed under the Apache License, Version 2.0 (the "License"); 5fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann * you may not use this file except in compliance with the License. 6fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann * You may obtain a copy of the License at 7fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann * 8fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann * http://www.apache.org/licenses/LICENSE-2.0 9fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann * 10fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann * Unless required by applicable law or agreed to in writing, software 11fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann * distributed under the License is distributed on an "AS IS" BASIS, 12fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann * See the License for the specific language governing permissions and 14fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann * limitations under the License. 15fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann */ 16fbb876392608a36ec1d23704f29d8b46d5d23991Daniel Lehmann 171b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikovpackage com.android.contacts.util; 181b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 191b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikovimport android.content.Context; 201b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikovimport android.content.res.Resources; 211b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikovimport android.text.Html; 221b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikovimport android.text.Html.ImageGetter; 231b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikovimport android.text.Html.TagHandler; 241b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikovimport android.text.SpannableStringBuilder; 251b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikovimport android.text.Spanned; 26be7a9d511eed5a549226b2e1bc2ebd6f65018c4cKatherine Kuanimport android.text.TextUtils; 271b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikovimport android.text.style.ImageSpan; 281b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikovimport android.text.style.QuoteSpan; 291b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 30e0b2f1e2d01d1ac52ba207dc7ce76971d853298eChiao Chengimport com.android.contacts.R; 31e0b2f1e2d01d1ac52ba207dc7ce76971d853298eChiao Chengimport com.google.common.annotations.VisibleForTesting; 32e0b2f1e2d01d1ac52ba207dc7ce76971d853298eChiao Cheng 331b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov/** 341b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov * Provides static functions to perform custom HTML to text conversions. 351b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov * Specifically, it adjusts the color and padding of the vertical 361b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov * stripe on block quotes and alignment of inlined images. 371b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov */ 381b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikovpublic class HtmlUtils { 391b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 401b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov /** 413ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki * Converts HTML string to a {@link Spanned} text, adjusting formatting. Any extra new line 423ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki * characters at the end of the text will be trimmed. 431b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov */ 441b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov public static Spanned fromHtml(Context context, String text) { 45be7a9d511eed5a549226b2e1bc2ebd6f65018c4cKatherine Kuan if (TextUtils.isEmpty(text)) { 46be7a9d511eed5a549226b2e1bc2ebd6f65018c4cKatherine Kuan return null; 47be7a9d511eed5a549226b2e1bc2ebd6f65018c4cKatherine Kuan } 481b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov Spanned spanned = Html.fromHtml(text); 493ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki return postprocess(context, spanned); 501b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 511b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 521b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov /** 531b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov * Converts HTML string to a {@link Spanned} text, adjusting formatting and using a custom 543ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki * image getter. Any extra new line characters at the end of the text will be trimmed. 551b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov */ 561b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov public static CharSequence fromHtml(Context context, String text, ImageGetter imageGetter, 571b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov TagHandler tagHandler) { 58be7a9d511eed5a549226b2e1bc2ebd6f65018c4cKatherine Kuan if (TextUtils.isEmpty(text)) { 59be7a9d511eed5a549226b2e1bc2ebd6f65018c4cKatherine Kuan return null; 60be7a9d511eed5a549226b2e1bc2ebd6f65018c4cKatherine Kuan } 613ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki return postprocess(context, Html.fromHtml(text, imageGetter, tagHandler)); 621b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 631b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 641b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov /** 653ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki * Replaces some spans with custom versions of those. Any extra new line characters at the end 663ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki * of the text will be trimmed. 671b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov */ 683ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki @VisibleForTesting 693ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki static Spanned postprocess(Context context, Spanned original) { 703ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki if (original == null) { 713ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki return null; 723ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki } 733ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki final int length = original.length(); 743ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki if (length == 0) { 753ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki return original; // Bail early. 761b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 771b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 783ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // If it's a SpannableStringBuilder, just use it. Otherwise, create a new 793ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // SpannableStringBuilder based on the passed Spanned. 803ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki final SpannableStringBuilder builder; 813ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki if (original instanceof SpannableStringBuilder) { 823ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki builder = (SpannableStringBuilder) original; 833ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki } else { 843ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki builder = new SpannableStringBuilder(original); 853ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki } 861b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 873ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki final QuoteSpan[] quoteSpans = builder.getSpans(0, length, QuoteSpan.class); 881b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov if (quoteSpans != null && quoteSpans.length != 0) { 891b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov Resources resources = context.getResources(); 901b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov int color = resources.getColor(R.color.stream_item_stripe_color); 911b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov int width = resources.getDimensionPixelSize(R.dimen.stream_item_stripe_width); 921b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov for (int i = 0; i < quoteSpans.length; i++) { 931b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov replaceSpan(builder, quoteSpans[i], new StreamItemQuoteSpan(color, width)); 941b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 951b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 961b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 973ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki final ImageSpan[] imageSpans = builder.getSpans(0, length, ImageSpan.class); 981b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov if (imageSpans != null) { 991b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov for (int i = 0; i < imageSpans.length; i++) { 1001b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov ImageSpan span = imageSpans[i]; 1011b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov replaceSpan(builder, span, new ImageSpan(span.getDrawable(), 1021b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov ImageSpan.ALIGN_BASELINE)); 1031b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1041b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1053ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki 1063ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // Trim the trailing new line characters at the end of the text (which can be added 1073ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // when HTML block quote tags are turned into new line characters). 1083ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki int end = length; 1093ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki for (int i = builder.length() - 1; i >= 0; i--) { 1103ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki if (builder.charAt(i) != '\n') { 1113ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki break; 1123ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki } 1133ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki end = i; 1143ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki } 1153ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki 1163ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // If there's no trailing newlines, just return it. 1173ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki if (end == length) { 1183ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki return builder; 1193ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki } 1203ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki 1213ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // Otherwise, Return a substring of the original {@link Spanned} text 1223ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // from the start index (inclusive) to the end index (exclusive). 1233ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki return new SpannableStringBuilder(builder, 0, end); 1241b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1251b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 1261b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov /** 1271b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov * Replaces one span with the other. 1281b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov */ 1291b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov private static void replaceSpan(SpannableStringBuilder builder, Object originalSpan, 1301b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov Object newSpan) { 1311b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov builder.setSpan(newSpan, 1321b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov builder.getSpanStart(originalSpan), 1331b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov builder.getSpanEnd(originalSpan), 1341b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov builder.getSpanFlags(originalSpan)); 1351b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov builder.removeSpan(originalSpan); 1361b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1371b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 1381b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov public static class StreamItemQuoteSpan extends QuoteSpan { 1391b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov private final int mWidth; 1401b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 1411b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov public StreamItemQuoteSpan(int color, int width) { 1421b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov super(color); 1431b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov this.mWidth = width; 1441b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1451b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 1461b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov /** 1471b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov * {@inheritDoc} 1481b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov */ 1491b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov @Override 1501b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov public int getLeadingMargin(boolean first) { 1511b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov return mWidth; 1521b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1531b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1541b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov} 155