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 */ 38cd321f65f1e50409812976380ad1f0fdb3fa35cbYorke Lee@VisibleForTesting 391b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikovpublic class HtmlUtils { 401b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 411b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov /** 423ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki * Converts HTML string to a {@link Spanned} text, adjusting formatting. Any extra new line 433ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki * characters at the end of the text will be trimmed. 441b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov */ 451b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov public static Spanned fromHtml(Context context, String text) { 46be7a9d511eed5a549226b2e1bc2ebd6f65018c4cKatherine Kuan if (TextUtils.isEmpty(text)) { 47be7a9d511eed5a549226b2e1bc2ebd6f65018c4cKatherine Kuan return null; 48be7a9d511eed5a549226b2e1bc2ebd6f65018c4cKatherine Kuan } 491b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov Spanned spanned = Html.fromHtml(text); 503ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki return postprocess(context, spanned); 511b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 521b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 531b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov /** 541b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov * Converts HTML string to a {@link Spanned} text, adjusting formatting and using a custom 553ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki * image getter. Any extra new line characters at the end of the text will be trimmed. 561b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov */ 571b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov public static CharSequence fromHtml(Context context, String text, ImageGetter imageGetter, 581b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov TagHandler tagHandler) { 59be7a9d511eed5a549226b2e1bc2ebd6f65018c4cKatherine Kuan if (TextUtils.isEmpty(text)) { 60be7a9d511eed5a549226b2e1bc2ebd6f65018c4cKatherine Kuan return null; 61be7a9d511eed5a549226b2e1bc2ebd6f65018c4cKatherine Kuan } 623ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki return postprocess(context, Html.fromHtml(text, imageGetter, tagHandler)); 631b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 641b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 651b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov /** 663ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki * Replaces some spans with custom versions of those. Any extra new line characters at the end 673ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki * of the text will be trimmed. 681b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov */ 693ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki @VisibleForTesting 703ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki static Spanned postprocess(Context context, Spanned original) { 713ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki if (original == null) { 723ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki return null; 733ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki } 743ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki final int length = original.length(); 753ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki if (length == 0) { 763ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki return original; // Bail early. 771b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 781b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 793ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // If it's a SpannableStringBuilder, just use it. Otherwise, create a new 803ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // SpannableStringBuilder based on the passed Spanned. 813ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki final SpannableStringBuilder builder; 823ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki if (original instanceof SpannableStringBuilder) { 833ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki builder = (SpannableStringBuilder) original; 843ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki } else { 853ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki builder = new SpannableStringBuilder(original); 863ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki } 871b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 883ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki final QuoteSpan[] quoteSpans = builder.getSpans(0, length, QuoteSpan.class); 891b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov if (quoteSpans != null && quoteSpans.length != 0) { 901b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov Resources resources = context.getResources(); 911b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov int color = resources.getColor(R.color.stream_item_stripe_color); 921b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov int width = resources.getDimensionPixelSize(R.dimen.stream_item_stripe_width); 931b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov for (int i = 0; i < quoteSpans.length; i++) { 941b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov replaceSpan(builder, quoteSpans[i], new StreamItemQuoteSpan(color, width)); 951b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 961b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 971b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 983ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki final ImageSpan[] imageSpans = builder.getSpans(0, length, ImageSpan.class); 991b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov if (imageSpans != null) { 1001b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov for (int i = 0; i < imageSpans.length; i++) { 1011b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov ImageSpan span = imageSpans[i]; 1021b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov replaceSpan(builder, span, new ImageSpan(span.getDrawable(), 1031b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov ImageSpan.ALIGN_BASELINE)); 1041b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1051b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1063ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki 1073ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // Trim the trailing new line characters at the end of the text (which can be added 1083ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // when HTML block quote tags are turned into new line characters). 1093ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki int end = length; 1103ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki for (int i = builder.length() - 1; i >= 0; i--) { 1113ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki if (builder.charAt(i) != '\n') { 1123ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki break; 1133ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki } 1143ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki end = i; 1153ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki } 1163ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki 1173ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // If there's no trailing newlines, just return it. 1183ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki if (end == length) { 1193ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki return builder; 1203ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki } 1213ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki 1223ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // Otherwise, Return a substring of the original {@link Spanned} text 1233ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki // from the start index (inclusive) to the end index (exclusive). 1243ab9a63014a2d5e097d221b424d6d4809295abd0Makoto Onuki return new SpannableStringBuilder(builder, 0, end); 1251b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1261b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 1271b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov /** 1281b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov * Replaces one span with the other. 1291b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov */ 1301b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov private static void replaceSpan(SpannableStringBuilder builder, Object originalSpan, 1311b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov Object newSpan) { 1321b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov builder.setSpan(newSpan, 1331b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov builder.getSpanStart(originalSpan), 1341b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov builder.getSpanEnd(originalSpan), 1351b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov builder.getSpanFlags(originalSpan)); 1361b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov builder.removeSpan(originalSpan); 1371b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1381b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 1391b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov public static class StreamItemQuoteSpan extends QuoteSpan { 1401b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov private final int mWidth; 1411b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 1421b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov public StreamItemQuoteSpan(int color, int width) { 1431b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov super(color); 1441b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov this.mWidth = width; 1451b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1461b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov 1471b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov /** 1481b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov * {@inheritDoc} 1491b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov */ 1501b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov @Override 1511b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov public int getLeadingMargin(boolean first) { 1521b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov return mWidth; 1531b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1541b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov } 1551b4572ba0ba449a4ed0cc896f5f694f33b43f073Dmitri Plotnikov} 156