diff options
Diffstat (limited to 'libraries/HtmlTextView/src')
4 files changed, 515 insertions, 0 deletions
| diff --git a/libraries/HtmlTextView/src/org/sufficientlysecure/htmltextview/HtmlTagHandler.java b/libraries/HtmlTextView/src/org/sufficientlysecure/htmltextview/HtmlTagHandler.java new file mode 100644 index 000000000..c40c8dec3 --- /dev/null +++ b/libraries/HtmlTextView/src/org/sufficientlysecure/htmltextview/HtmlTagHandler.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2013 Mohammed Lakkadshaw + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.htmltextview; + +import java.util.Vector; + +import org.xml.sax.XMLReader; + +import android.text.Editable; +import android.text.Html; +import android.text.Spannable; +import android.text.style.BulletSpan; +import android.text.style.LeadingMarginSpan; +import android.text.style.TypefaceSpan; +import android.util.Log; + +public class HtmlTagHandler implements Html.TagHandler { +    private int mListItemCount = 0; +    private Vector<String> mListParents = new Vector<String>(); + +    @Override +    public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) { + +        if (tag.equals("ul") || tag.equals("ol") || tag.equals("dd")) { +            if (opening) { +                mListParents.add(tag); +            } else mListParents.remove(tag); + +            mListItemCount = 0; +        } else if (tag.equals("li") && !opening) { +            handleListTag(output); +        } else if (tag.equalsIgnoreCase("code")) { +            if (opening) { +                output.setSpan(new TypefaceSpan("monospace"), output.length(), output.length(), Spannable.SPAN_MARK_MARK); +            } else { +                Log.d(HtmlTextView.TAG, "Code tag encountered"); +                Object obj = getLast(output, TypefaceSpan.class); +                int where = output.getSpanStart(obj); +                output.setSpan(new TypefaceSpan("monospace"), where, output.length(), 0); +            } +        } +    } + +    private Object getLast(Editable text, Class kind) { +        Object[] objs = text.getSpans(0, text.length(), kind); +        if (objs.length == 0) { +            return null; +        } else { +            for (int i = objs.length; i > 0; i--) { +                if (text.getSpanFlags(objs[i - 1]) == Spannable.SPAN_MARK_MARK) { +                    return objs[i - 1]; +                } +            } +            return null; +        } +    } + +    private void handleListTag(Editable output) { +        if (mListParents.lastElement().equals("ul")) { +            output.append("\n"); +            String[] split = output.toString().split("\n"); + +            int lastIndex = split.length - 1; +            int start = output.length() - split[lastIndex].length() - 1; +            output.setSpan(new BulletSpan(15 * mListParents.size()), start, output.length(), 0); +        } else if (mListParents.lastElement().equals("ol")) { +            mListItemCount++; + +            output.append("\n"); +            String[] split = output.toString().split("\n"); + +            int lastIndex = split.length - 1; +            int start = output.length() - split[lastIndex].length() - 1; +            output.insert(start, mListItemCount + ". "); +            output.setSpan(new LeadingMarginSpan.Standard(15 * mListParents.size()), start, output.length(), 0); +        } +    } +}  diff --git a/libraries/HtmlTextView/src/org/sufficientlysecure/htmltextview/HtmlTextView.java b/libraries/HtmlTextView/src/org/sufficientlysecure/htmltextview/HtmlTextView.java new file mode 100644 index 000000000..317c25aaf --- /dev/null +++ b/libraries/HtmlTextView/src/org/sufficientlysecure/htmltextview/HtmlTextView.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.htmltextview; + +import android.content.Context; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.util.AttributeSet; + +import java.io.InputStream; + +public class HtmlTextView extends JellyBeanSpanFixTextView { + +    public static final String TAG = "HtmlTextView"; + +    public HtmlTextView(Context context, AttributeSet attrs, int defStyle) { +        super(context, attrs, defStyle); +    } + +    public HtmlTextView(Context context, AttributeSet attrs) { +        super(context, attrs); +    } + +    public HtmlTextView(Context context) { +        super(context); +    } + +    /** +     * http://stackoverflow.com/questions/309424/read-convert-an-inputstream-to-a-string +     * +     * @param is +     * @return +     */ +    static private String convertStreamToString(java.io.InputStream is) { +        java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A"); +        return s.hasNext() ? s.next() : ""; +    } + +    /** +     * Loads HTML from a raw resource, i.e., a HTML file in res/raw/. +     * This allows translatable resource (e.g., res/raw-de/ for german). +     * The containing HTML is parsed to Android's Spannable format and then displayed. +     * +     * @param context +     * @param id      for example: R.raw.help +     */ +    public void setHtmlFromRawResource(Context context, int id) { +        // load html from html file from /res/raw +        InputStream inputStreamText = context.getResources().openRawResource(id); + +        setHtmlFromString(convertStreamToString(inputStreamText)); +    } + +    /** +     * Parses String containing HTML to Android's Spannable format and displays it in this TextView. +     * +     * @param html String containing HTML, for example: "<b>Hello world!</b>" +     */ +    public void setHtmlFromString(String html) { +        // this uses Android's Html class for basic parsing, and HtmlTagHandler +        setText(Html.fromHtml(html, new UrlImageGetter(this, getContext()), new HtmlTagHandler())); + +        // make links work +        setMovementMethod(LinkMovementMethod.getInstance()); + +        // no flickering when clicking textview for Android < 4 +//        text.setTextColor(getResources().getColor(android.R.color.secondary_text_dark_nodisable)); +    } +} diff --git a/libraries/HtmlTextView/src/org/sufficientlysecure/htmltextview/JellyBeanSpanFixTextView.java b/libraries/HtmlTextView/src/org/sufficientlysecure/htmltextview/JellyBeanSpanFixTextView.java new file mode 100644 index 000000000..94bf45849 --- /dev/null +++ b/libraries/HtmlTextView/src/org/sufficientlysecure/htmltextview/JellyBeanSpanFixTextView.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2012 Pierre-Yves Ricau <py.ricau@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.htmltextview; + +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.TextView; + +/** + * <p> + * A {@link android.widget.TextView} that insert spaces around its text spans where needed to prevent + * {@link IndexOutOfBoundsException} in {@link #onMeasure(int, int)} on Jelly Bean. + * <p> + * When {@link #onMeasure(int, int)} throws an exception, we try to fix the text by adding spaces + * around spans, until it works again. We then try removing some of the added spans, to minimize the + * insertions. + * <p> + * The fix is time consuming (a few ms, it depends on the size of your text), but it should only + * happen once per text change. + * <p> + * See http://code.google.com/p/android/issues/detail?id=35466 + *  + */ +public class JellyBeanSpanFixTextView extends TextView { + +    private static class FixingResult { +        public final boolean fixed; +        public final List<Object> spansWithSpacesBefore; +        public final List<Object> spansWithSpacesAfter; + +        public static FixingResult fixed(List<Object> spansWithSpacesBefore, +                List<Object> spansWithSpacesAfter) { +            return new FixingResult(true, spansWithSpacesBefore, spansWithSpacesAfter); +        } + +        public static FixingResult notFixed() { +            return new FixingResult(false, null, null); +        } + +        private FixingResult(boolean fixed, List<Object> spansWithSpacesBefore, +                List<Object> spansWithSpacesAfter) { +            this.fixed = fixed; +            this.spansWithSpacesBefore = spansWithSpacesBefore; +            this.spansWithSpacesAfter = spansWithSpacesAfter; +        } +    } + +    public JellyBeanSpanFixTextView(Context context, AttributeSet attrs, int defStyle) { +        super(context, attrs, defStyle); +    } + +    public JellyBeanSpanFixTextView(Context context, AttributeSet attrs) { +        super(context, attrs); +    } + +    public JellyBeanSpanFixTextView(Context context) { +        super(context); +    } + +    @Override +    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { +        try { +            super.onMeasure(widthMeasureSpec, heightMeasureSpec); +        } catch (IndexOutOfBoundsException e) { +            fixOnMeasure(widthMeasureSpec, heightMeasureSpec); +        } +    } + +    /** +     * If possible, fixes the Spanned text by adding spaces around spans when needed. +     */ +    private void fixOnMeasure(int widthMeasureSpec, int heightMeasureSpec) { +        CharSequence text = getText(); +        if (text instanceof Spanned) { +            SpannableStringBuilder builder = new SpannableStringBuilder(text); +            fixSpannedWithSpaces(builder, widthMeasureSpec, heightMeasureSpec); +        } else { +            if (BuildConfig.DEBUG) { +                Log.d(HtmlTextView.TAG, "The text isn't a Spanned"); +            } +            fallbackToString(widthMeasureSpec, heightMeasureSpec); +        } +    } + +    /** +     * Add spaces around spans until the text is fixed, and then removes the unneeded spaces +     */ +    private void fixSpannedWithSpaces(SpannableStringBuilder builder, int widthMeasureSpec, +            int heightMeasureSpec) { +        long startFix = System.currentTimeMillis(); + +        FixingResult result = addSpacesAroundSpansUntilFixed(builder, widthMeasureSpec, +                heightMeasureSpec); + +        if (result.fixed) { +            removeUnneededSpaces(widthMeasureSpec, heightMeasureSpec, builder, result); +        } else { +            fallbackToString(widthMeasureSpec, heightMeasureSpec); +        } + +        if (BuildConfig.DEBUG) { +            long fixDuration = System.currentTimeMillis() - startFix; +            Log.d(HtmlTextView.TAG, "fixSpannedWithSpaces() duration in ms: " + fixDuration); +        } +    } + +    private FixingResult addSpacesAroundSpansUntilFixed(SpannableStringBuilder builder, +            int widthMeasureSpec, int heightMeasureSpec) { + +        Object[] spans = builder.getSpans(0, builder.length(), Object.class); +        List<Object> spansWithSpacesBefore = new ArrayList<Object>(spans.length); +        List<Object> spansWithSpacesAfter = new ArrayList<Object>(spans.length); + +        for (Object span : spans) { +            int spanStart = builder.getSpanStart(span); +            if (isNotSpace(builder, spanStart - 1)) { +                builder.insert(spanStart, " "); +                spansWithSpacesBefore.add(span); +            } + +            int spanEnd = builder.getSpanEnd(span); +            if (isNotSpace(builder, spanEnd)) { +                builder.insert(spanEnd, " "); +                spansWithSpacesAfter.add(span); +            } + +            try { +                setTextAndMeasure(builder, widthMeasureSpec, heightMeasureSpec); +                return FixingResult.fixed(spansWithSpacesBefore, spansWithSpacesAfter); +            } catch (IndexOutOfBoundsException notFixed) { +            } +        } +        if (BuildConfig.DEBUG) { +            Log.d(HtmlTextView.TAG, "Could not fix the Spanned by adding spaces around spans"); +        } +        return FixingResult.notFixed(); +    } + +    private boolean isNotSpace(CharSequence text, int where) { +        if (where < 0) +            return true; +        return text.charAt(where) != ' '; +    } + +    private void setTextAndMeasure(CharSequence text, int widthMeasureSpec, int heightMeasureSpec) { +        setText(text); +        super.onMeasure(widthMeasureSpec, heightMeasureSpec); +    } + +    private void removeUnneededSpaces(int widthMeasureSpec, int heightMeasureSpec, +            SpannableStringBuilder builder, FixingResult result) { + +        for (Object span : result.spansWithSpacesAfter) { +            int spanEnd = builder.getSpanEnd(span); +            builder.delete(spanEnd, spanEnd + 1); +            try { +                setTextAndMeasure(builder, widthMeasureSpec, heightMeasureSpec); +            } catch (IndexOutOfBoundsException ignored) { +                builder.insert(spanEnd, " "); +            } +        } + +        boolean needReset = true; +        for (Object span : result.spansWithSpacesBefore) { +            int spanStart = builder.getSpanStart(span); +            builder.delete(spanStart - 1, spanStart); +            try { +                setTextAndMeasure(builder, widthMeasureSpec, heightMeasureSpec); +                needReset = false; +            } catch (IndexOutOfBoundsException ignored) { +                needReset = true; +                int newSpanStart = spanStart - 1; +                builder.insert(newSpanStart, " "); +            } +        } + +        if (needReset) { +            setText(builder); +            super.onMeasure(widthMeasureSpec, heightMeasureSpec); +        } +    } + +    private void fallbackToString(int widthMeasureSpec, int heightMeasureSpec) { +        if (BuildConfig.DEBUG) { +            Log.d(HtmlTextView.TAG, "Fallback to unspanned text"); +        } +        String fallbackText = getText().toString(); +        setTextAndMeasure(fallbackText, widthMeasureSpec, heightMeasureSpec); +    } + +}
\ No newline at end of file diff --git a/libraries/HtmlTextView/src/org/sufficientlysecure/htmltextview/UrlImageGetter.java b/libraries/HtmlTextView/src/org/sufficientlysecure/htmltextview/UrlImageGetter.java new file mode 100644 index 000000000..e4fc41c05 --- /dev/null +++ b/libraries/HtmlTextView/src/org/sufficientlysecure/htmltextview/UrlImageGetter.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2013 Antarix Tandon + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.htmltextview; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.text.Html.ImageGetter; +import android.view.View; + +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; + +public class UrlImageGetter implements ImageGetter { +    Context c; +    View container; + +    /** +     * Construct the URLImageParser which will execute AsyncTask and refresh the container +     * +     * @param t +     * @param c +     */ +    public UrlImageGetter(View t, Context c) { +        this.c = c; +        this.container = t; +    } + +    public Drawable getDrawable(String source) { +        UrlDrawable urlDrawable = new UrlDrawable(); + +        // get the actual source +        ImageGetterAsyncTask asyncTask = new ImageGetterAsyncTask(urlDrawable); + +        asyncTask.execute(source); + +        // return reference to URLDrawable where I will change with actual image from +        // the src tag +        return urlDrawable; +    } + +    public class ImageGetterAsyncTask extends AsyncTask<String, Void, Drawable> { +        UrlDrawable urlDrawable; + +        public ImageGetterAsyncTask(UrlDrawable d) { +            this.urlDrawable = d; +        } + +        @Override +        protected Drawable doInBackground(String... params) { +            String source = params[0]; +            return fetchDrawable(source); +        } + +        @Override +        protected void onPostExecute(Drawable result) { +            // set the correct bound according to the result from HTTP call +            urlDrawable.setBounds(0, 0, 0 + result.getIntrinsicWidth(), 0 + result.getIntrinsicHeight()); + +            // change the reference of the current drawable to the result +            // from the HTTP call +            urlDrawable.drawable = result; + +            // redraw the image by invalidating the container +            UrlImageGetter.this.container.invalidate(); +        } + +        /** +         * Get the Drawable from URL +         * +         * @param urlString +         * @return +         */ +        public Drawable fetchDrawable(String urlString) { +            try { +                InputStream is = fetch(urlString); +                Drawable drawable = Drawable.createFromStream(is, "src"); +                drawable.setBounds(0, 0, 0 + drawable.getIntrinsicWidth(), 0 + drawable.getIntrinsicHeight()); +                return drawable; +            } catch (Exception e) { +                return null; +            } +        } + +        private InputStream fetch(String urlString) throws MalformedURLException, IOException { +            DefaultHttpClient httpClient = new DefaultHttpClient(); +            HttpGet request = new HttpGet(urlString); +            HttpResponse response = httpClient.execute(request); +            return response.getEntity().getContent(); +        } +    } + +    @SuppressWarnings("deprecation") +    public class UrlDrawable extends BitmapDrawable { +        // the drawable that you need to set, you could set the initial drawing +        // with the loading image if you need to +        protected Drawable drawable; + +        @Override +        public void draw(Canvas canvas) { +            // override the draw to facilitate refresh function later +            if (drawable != null) { +                drawable.draw(canvas); +            } +        } +    } +}  | 
