1/*
2 * Copyright (C) 2011 The Android Open Source Project
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
17package com.android.contacts.util;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.text.Html;
22import android.text.Html.ImageGetter;
23import android.text.Html.TagHandler;
24import android.text.SpannableStringBuilder;
25import android.text.Spanned;
26import android.text.TextUtils;
27import android.text.style.ImageSpan;
28import android.text.style.QuoteSpan;
29
30import com.android.contacts.R;
31import com.google.common.annotations.VisibleForTesting;
32
33/**
34 * Provides static functions to perform custom HTML to text conversions.
35 * Specifically, it adjusts the color and padding of the vertical
36 * stripe on block quotes and alignment of inlined images.
37 */
38@VisibleForTesting
39public class HtmlUtils {
40
41    /**
42     * Converts HTML string to a {@link Spanned} text, adjusting formatting. Any extra new line
43     * characters at the end of the text will be trimmed.
44     */
45    public static Spanned fromHtml(Context context, String text) {
46        if (TextUtils.isEmpty(text)) {
47            return null;
48        }
49        Spanned spanned = Html.fromHtml(text);
50        return postprocess(context, spanned);
51    }
52
53    /**
54     * Converts HTML string to a {@link Spanned} text, adjusting formatting and using a custom
55     * image getter. Any extra new line characters at the end of the text will be trimmed.
56     */
57    public static CharSequence fromHtml(Context context, String text, ImageGetter imageGetter,
58            TagHandler tagHandler) {
59        if (TextUtils.isEmpty(text)) {
60            return null;
61        }
62        return postprocess(context, Html.fromHtml(text, imageGetter, tagHandler));
63    }
64
65    /**
66     * Replaces some spans with custom versions of those. Any extra new line characters at the end
67     * of the text will be trimmed.
68     */
69    @VisibleForTesting
70    static Spanned postprocess(Context context, Spanned original) {
71        if (original == null) {
72            return null;
73        }
74        final int length = original.length();
75        if (length == 0) {
76            return original; // Bail early.
77        }
78
79        // If it's a SpannableStringBuilder, just use it.  Otherwise, create a new
80        // SpannableStringBuilder based on the passed Spanned.
81        final SpannableStringBuilder builder;
82        if (original instanceof SpannableStringBuilder) {
83            builder = (SpannableStringBuilder) original;
84        } else {
85            builder = new SpannableStringBuilder(original);
86        }
87
88        final QuoteSpan[] quoteSpans = builder.getSpans(0, length, QuoteSpan.class);
89        if (quoteSpans != null && quoteSpans.length != 0) {
90            Resources resources = context.getResources();
91            int color = resources.getColor(R.color.stream_item_stripe_color);
92            int width = resources.getDimensionPixelSize(R.dimen.stream_item_stripe_width);
93            for (int i = 0; i < quoteSpans.length; i++) {
94                replaceSpan(builder, quoteSpans[i], new StreamItemQuoteSpan(color, width));
95            }
96        }
97
98        final ImageSpan[] imageSpans = builder.getSpans(0, length, ImageSpan.class);
99        if (imageSpans != null) {
100            for (int i = 0; i < imageSpans.length; i++) {
101                ImageSpan span = imageSpans[i];
102                replaceSpan(builder, span, new ImageSpan(span.getDrawable(),
103                        ImageSpan.ALIGN_BASELINE));
104            }
105        }
106
107        // Trim the trailing new line characters at the end of the text (which can be added
108        // when HTML block quote tags are turned into new line characters).
109        int end = length;
110        for (int i = builder.length() - 1; i >= 0; i--) {
111            if (builder.charAt(i) != '\n') {
112                break;
113            }
114            end = i;
115        }
116
117        // If there's no trailing newlines, just return it.
118        if (end == length) {
119            return builder;
120        }
121
122        // Otherwise, Return a substring of the original {@link Spanned} text
123        // from the start index (inclusive) to the end index (exclusive).
124        return new SpannableStringBuilder(builder, 0, end);
125    }
126
127    /**
128     * Replaces one span with the other.
129     */
130    private static void replaceSpan(SpannableStringBuilder builder, Object originalSpan,
131            Object newSpan) {
132        builder.setSpan(newSpan,
133                builder.getSpanStart(originalSpan),
134                builder.getSpanEnd(originalSpan),
135                builder.getSpanFlags(originalSpan));
136        builder.removeSpan(originalSpan);
137    }
138
139    public static class StreamItemQuoteSpan extends QuoteSpan {
140        private final int mWidth;
141
142        public StreamItemQuoteSpan(int color, int width) {
143            super(color);
144            this.mWidth = width;
145        }
146
147        /**
148         * {@inheritDoc}
149         */
150        @Override
151        public int getLeadingMargin(boolean first) {
152            return mWidth;
153        }
154    }
155}
156