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