diff --git a/app/build.gradle b/app/build.gradle
index 73a0d43a..a02fd18f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -119,7 +119,6 @@ dependencies {
implementation "ch.acra:acra-http:$acraVersion"
implementation "ch.acra:acra-dialog:$acraVersion"
implementation "ch.acra:acra-limiter:$acraVersion"
- implementation 'org.sufficientlysecure:html-textview:4.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation "com.github.permissions-dispatcher:permissionsdispatcher:$permissionsDispatcherVersion"
annotationProcessor "com.github.permissions-dispatcher:permissionsdispatcher-processor:$permissionsDispatcherVersion"
diff --git a/app/src/main/java/org/sufficientlysecure/htmltextview/ClickableTableSpan.java b/app/src/main/java/org/sufficientlysecure/htmltextview/ClickableTableSpan.java
new file mode 100644
index 00000000..8cf72887
--- /dev/null
+++ b/app/src/main/java/org/sufficientlysecure/htmltextview/ClickableTableSpan.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 Richard Thai
+ *
+ * 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.text.style.ClickableSpan;
+
+/**
+ * This span defines what should happen if a table is clicked. This abstract class is defined so
+ * that applications can access the raw table HTML and do whatever they'd like to render it (e.g.
+ * show it in a WebView).
+ */
+public abstract class ClickableTableSpan extends ClickableSpan {
+ protected String tableHtml;
+
+ // This sucks, but we need this so that each table can get its own ClickableTableSpan.
+ // Otherwise, we end up removing the clicking from earlier tables.
+ public abstract ClickableTableSpan newInstance();
+
+ public void setTableHtml(String tableHtml) {
+ this.tableHtml = tableHtml;
+ }
+
+ public String getTableHtml() {
+ return tableHtml;
+ }
+}
diff --git a/app/src/main/java/org/sufficientlysecure/htmltextview/DesignQuoteSpan.java b/app/src/main/java/org/sufficientlysecure/htmltextview/DesignQuoteSpan.java
new file mode 100644
index 00000000..85db91e3
--- /dev/null
+++ b/app/src/main/java/org/sufficientlysecure/htmltextview/DesignQuoteSpan.java
@@ -0,0 +1,64 @@
+/*
+ * 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.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.Layout;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.LineBackgroundSpan;
+
+import androidx.annotation.NonNull;
+
+public class DesignQuoteSpan implements LeadingMarginSpan, LineBackgroundSpan {
+
+ private int backgroundColor, stripColor;
+ private float stripeWidth, gap;
+
+ DesignQuoteSpan(int backgroundColor, int stripColor, float stripWidth, float gap) {
+ this.backgroundColor = backgroundColor;
+ this.stripColor = stripColor;
+ this.stripeWidth = stripWidth;
+ this.gap = gap;
+ }
+
+ @Override
+ public int getLeadingMargin(boolean first) {
+ return (int) (stripeWidth + gap);
+ }
+
+ @Override
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline,
+ int bottom, CharSequence text, int start, int end, boolean first,
+ Layout layout) {
+ Paint.Style style = p.getStyle();
+ int paintColor = p.getColor();
+ p.setStyle(Paint.Style.FILL);
+ p.setColor(stripColor);
+ c.drawRect((float) x, (float) top, x + dir * stripeWidth, (float) bottom, p);
+ p.setStyle(style);
+ p.setColor(paintColor);
+ }
+
+ @Override
+ public void drawBackground(@NonNull Canvas canvas, @NonNull Paint paint,
+ int left, int right, int top, int baseline, int bottom,
+ @NonNull CharSequence text, int start, int end, int lineNumber) {
+ int paintColor = paint.getColor();
+ paint.setColor(backgroundColor);
+ canvas.drawRect((float) left, (float) top, (float) right, (float) bottom, paint);
+ paint.setColor(paintColor);
+ }
+}
diff --git a/app/src/main/java/org/sufficientlysecure/htmltextview/DrawTableLinkSpan.java b/app/src/main/java/org/sufficientlysecure/htmltextview/DrawTableLinkSpan.java
new file mode 100644
index 00000000..be5769a3
--- /dev/null
+++ b/app/src/main/java/org/sufficientlysecure/htmltextview/DrawTableLinkSpan.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2016 Richard Thai
+ *
+ * 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.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.text.style.ReplacementSpan;
+
+/**
+ * This span defines how a table should be rendered in the HtmlTextView. The default implementation
+ * is a cop-out which replaces the HTML table with some text ("[tap for table]" is the default).
+ *
+ * This is to be used in conjunction with the ClickableTableSpan which will redirect a click to the
+ * text some application-defined action (i.e. render the raw HTML in a WebView).
+ */
+public class DrawTableLinkSpan extends ReplacementSpan {
+
+ private static final String DEFAULT_TABLE_LINK_TEXT = "";
+ private static float DEFAULT_TEXT_SIZE = 80f;
+ private static int DEFAULT_TEXT_COLOR = Color.BLUE;
+
+ protected String mTableLinkText = DEFAULT_TABLE_LINK_TEXT;
+ protected float mTextSize = DEFAULT_TEXT_SIZE;
+ protected int mTextColor = DEFAULT_TEXT_COLOR;
+
+ // This sucks, but we need this so that each table can get drawn.
+ // Otherwise, we end up with the default table link text (nothing) for earlier tables.
+ public DrawTableLinkSpan newInstance() {
+ final DrawTableLinkSpan drawTableLinkSpan = new DrawTableLinkSpan();
+ drawTableLinkSpan.setTableLinkText(mTableLinkText);
+ drawTableLinkSpan.setTextSize(mTextSize);
+ drawTableLinkSpan.setTextColor(mTextColor);
+
+ return drawTableLinkSpan;
+ }
+
+ @Override
+ public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
+ int width = (int) paint.measureText(mTableLinkText, 0, mTableLinkText.length());
+ mTextSize = paint.getTextSize();
+ return width;
+ }
+
+ @Override
+ public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
+ final Paint paint2 = new Paint();
+ paint2.setStyle(Paint.Style.STROKE);
+ paint2.setColor(mTextColor);
+ paint2.setAntiAlias(true);
+ paint2.setTextSize(mTextSize);
+
+ canvas.drawText(mTableLinkText, x, bottom, paint2);
+ }
+
+ public void setTableLinkText(String tableLinkText) {
+ this.mTableLinkText = tableLinkText;
+ }
+
+ public void setTextSize(float textSize) {
+ this.mTextSize = textSize;
+ }
+
+ public void setTextColor(int textColor) {
+ this.mTextColor = textColor;
+ }
+
+ public String getTableLinkText() {
+ return mTableLinkText;
+ }
+
+ public float getTextSize() {
+ return mTextSize;
+ }
+
+ public int getTextColor() {
+ return mTextColor;
+ }
+}
diff --git a/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlAssetsImageGetter.java b/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlAssetsImageGetter.java
new file mode 100644
index 00000000..7df6766a
--- /dev/null
+++ b/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlAssetsImageGetter.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 Daniel Passos
+ *
+ * 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.drawable.Drawable;
+import android.text.Html;
+import android.util.Log;
+import android.widget.TextView;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Assets Image Getter
+ *
+ * Load image from assets folder
+ *
+ * @author Daniel Passos
+ */
+public class HtmlAssetsImageGetter implements Html.ImageGetter {
+
+ private final Context context;
+
+ public HtmlAssetsImageGetter(Context context) {
+ this.context = context;
+ }
+
+ public HtmlAssetsImageGetter(TextView textView) {
+ this.context = textView.getContext();
+ }
+
+ @Override
+ public Drawable getDrawable(String source) {
+
+ try {
+ InputStream inputStream = context.getAssets().open(source);
+ Drawable d = Drawable.createFromStream(inputStream, null);
+ d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
+ return d;
+ } catch (IOException e) {
+ // prevent a crash if the resource still can't be found
+ Log.e(HtmlTextView.TAG, "source could not be found: " + source);
+ return null;
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlFormatter.java b/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlFormatter.java
new file mode 100644
index 00000000..5a7c2099
--- /dev/null
+++ b/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlFormatter.java
@@ -0,0 +1,80 @@
+/*
+ * 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.text.Html;
+import android.text.Html.ImageGetter;
+import android.text.Spanned;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class HtmlFormatter {
+
+ private HtmlFormatter() {
+ }
+
+ public static Spanned formatHtml(@NonNull final HtmlFormatterBuilder builder) {
+ return formatHtml(
+ builder.getHtml(), builder.getImageGetter(), builder.getClickableTableSpan(),
+ builder.getDrawTableLinkSpan(), new TagClickListenerProvider() {
+ @Override public OnClickATagListener provideTagClickListener() {
+ return builder.getOnClickATagListener();
+ }
+ }, builder.getIndent(),
+ builder.isRemoveTrailingWhiteSpace()
+ );
+ }
+
+ interface TagClickListenerProvider {
+ OnClickATagListener provideTagClickListener();
+ }
+
+ public static Spanned formatHtml(@Nullable String html, ImageGetter imageGetter, ClickableTableSpan clickableTableSpan, DrawTableLinkSpan drawTableLinkSpan, TagClickListenerProvider tagClickListenerProvider, float indent, boolean removeTrailingWhiteSpace) {
+ final HtmlTagHandler htmlTagHandler = new HtmlTagHandler();
+ htmlTagHandler.setClickableTableSpan(clickableTableSpan);
+ htmlTagHandler.setDrawTableLinkSpan(drawTableLinkSpan);
+ htmlTagHandler.setOnClickATagListenerProvider(tagClickListenerProvider);
+ htmlTagHandler.setListIndentPx(indent);
+
+ html = htmlTagHandler.overrideTags(html);
+
+ Spanned formattedHtml;
+ if (removeTrailingWhiteSpace) {
+ formattedHtml = removeHtmlBottomPadding(Html.fromHtml(html, imageGetter, new WrapperContentHandler(htmlTagHandler)));
+ } else {
+ formattedHtml = Html.fromHtml(html, imageGetter, new WrapperContentHandler(htmlTagHandler));
+ }
+
+ return formattedHtml;
+ }
+
+ /**
+ * Html.fromHtml sometimes adds extra space at the bottom.
+ * This methods removes this space again.
+ * See https://github.com/SufficientlySecure/html-textview/issues/19
+ */
+ @Nullable
+ private static Spanned removeHtmlBottomPadding(@Nullable Spanned text) {
+ if (text == null) {
+ return null;
+ }
+
+ while (text.length() > 0 && text.charAt(text.length() - 1) == '\n') {
+ text = (Spanned) text.subSequence(0, text.length() - 1);
+ }
+ return text;
+ }
+}
diff --git a/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlFormatterBuilder.java b/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlFormatterBuilder.java
new file mode 100644
index 00000000..d98affb6
--- /dev/null
+++ b/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlFormatterBuilder.java
@@ -0,0 +1,91 @@
+/*
+ * 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.text.Html.ImageGetter;
+import androidx.annotation.Nullable;
+
+public class HtmlFormatterBuilder {
+
+ private String html;
+ private ImageGetter imageGetter;
+ private ClickableTableSpan clickableTableSpan;
+ private DrawTableLinkSpan drawTableLinkSpan;
+ private OnClickATagListener onClickATagListener;
+ private float indent = 24.0f;
+ private boolean removeTrailingWhiteSpace = true;
+
+ public String getHtml() {
+ return html;
+ }
+
+ public ImageGetter getImageGetter() {
+ return imageGetter;
+ }
+
+ public ClickableTableSpan getClickableTableSpan() {
+ return clickableTableSpan;
+ }
+
+ public DrawTableLinkSpan getDrawTableLinkSpan() {
+ return drawTableLinkSpan;
+ }
+
+ public OnClickATagListener getOnClickATagListener() {
+ return onClickATagListener;
+ }
+
+ public float getIndent() {
+ return indent;
+ }
+
+ public boolean isRemoveTrailingWhiteSpace() {
+ return removeTrailingWhiteSpace;
+ }
+
+ public HtmlFormatterBuilder setHtml(@Nullable final String html) {
+ this.html = html;
+ return this;
+ }
+
+ public HtmlFormatterBuilder setImageGetter(@Nullable final ImageGetter imageGetter) {
+ this.imageGetter = imageGetter;
+ return this;
+ }
+
+ public HtmlFormatterBuilder setClickableTableSpan(@Nullable final ClickableTableSpan clickableTableSpan) {
+ this.clickableTableSpan = clickableTableSpan;
+ return this;
+ }
+
+ public HtmlFormatterBuilder setDrawTableLinkSpan(@Nullable final DrawTableLinkSpan drawTableLinkSpan) {
+ this.drawTableLinkSpan = drawTableLinkSpan;
+ return this;
+ }
+
+ public void setOnClickATagListener(OnClickATagListener onClickATagListener) {
+ this.onClickATagListener = onClickATagListener;
+ }
+
+ public HtmlFormatterBuilder setIndent(final float indent) {
+ this.indent = indent;
+ return this;
+ }
+
+ public HtmlFormatterBuilder setRemoveTrailingWhiteSpace(final boolean removeTrailingWhiteSpace) {
+ this.removeTrailingWhiteSpace = removeTrailingWhiteSpace;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlHttpImageGetter.java b/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlHttpImageGetter.java
new file mode 100644
index 00000000..31de5fba
--- /dev/null
+++ b/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlHttpImageGetter.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2014-2016 Dominik Schürmann
+ * 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.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+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.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.net.URI;
+import java.net.URL;
+
+public class HtmlHttpImageGetter implements ImageGetter {
+ private TextView container;
+ private URI baseUri;
+ private boolean matchParentWidth;
+ private int placeHolder;
+
+ private boolean compressImage = false;
+ private int qualityImage = 50;
+
+ public HtmlHttpImageGetter(TextView textView) {
+ this.container = textView;
+ this.matchParentWidth = false;
+ }
+
+ public HtmlHttpImageGetter(TextView textView, String baseUrl) {
+ this.container = textView;
+ if (baseUrl != null) {
+ this.baseUri = URI.create(baseUrl);
+ }
+ }
+
+ public HtmlHttpImageGetter(TextView textView, String baseUrl, boolean matchParentWidth) {
+ this(textView,baseUrl,0,matchParentWidth);
+ }
+
+ public HtmlHttpImageGetter(TextView textView, String baseUrl,int placeHolder, boolean matchParentWidth) {
+ this.container = textView;
+ this.placeHolder = placeHolder;
+ this.matchParentWidth = matchParentWidth;
+ if (baseUrl != null) {
+ this.baseUri = URI.create(baseUrl);
+ }
+ }
+
+ public void enableCompressImage(boolean enable) {
+ enableCompressImage(enable, 50);
+ }
+
+ public void enableCompressImage(boolean enable, int quality) {
+ compressImage = enable;
+ qualityImage = quality;
+ }
+
+ public Drawable getDrawable(String source) {
+ UrlDrawable urlDrawable = new UrlDrawable();
+ if (placeHolder != 0) {
+ Drawable placeDrawable = container.getContext().getResources().getDrawable(placeHolder);
+ placeDrawable.setBounds(0, 0, placeDrawable.getIntrinsicWidth(), placeDrawable.getIntrinsicHeight());
+ urlDrawable.setBounds(0, 0, placeDrawable.getIntrinsicWidth(), placeDrawable.getIntrinsicHeight());
+ urlDrawable.drawable = placeDrawable;
+ }
+ // get the actual source
+ ImageGetterAsyncTask asyncTask = new ImageGetterAsyncTask(urlDrawable, this, container,
+ matchParentWidth, compressImage, qualityImage);
+
+ asyncTask.execute(source);
+
+ // return reference to URLDrawable which will asynchronously load the image specified in the src tag
+ return urlDrawable;
+ }
+
+ /**
+ * Static inner {@link AsyncTask} that keeps a {@link WeakReference} to the {@link UrlDrawable}
+ * and {@link HtmlHttpImageGetter}.
+ *
+ * This way, if the AsyncTask has a longer life span than the UrlDrawable,
+ * we won't leak the UrlDrawable or the HtmlRemoteImageGetter.
+ */
+ private static class ImageGetterAsyncTask extends AsyncTask {
+ private final WeakReference drawableReference;
+ private final WeakReference imageGetterReference;
+ private final WeakReference containerReference;
+ private final WeakReference resources;
+ private String source;
+ private boolean matchParentWidth;
+ private float scale;
+
+ private boolean compressImage = false;
+ private int qualityImage = 50;
+
+ public ImageGetterAsyncTask(UrlDrawable d, HtmlHttpImageGetter imageGetter, View container,
+ boolean matchParentWidth, boolean compressImage, int qualityImage) {
+ this.drawableReference = new WeakReference<>(d);
+ this.imageGetterReference = new WeakReference<>(imageGetter);
+ this.containerReference = new WeakReference<>(container);
+ this.resources = new WeakReference<>(container.getResources());
+ this.matchParentWidth = matchParentWidth;
+ this.compressImage = compressImage;
+ this.qualityImage = qualityImage;
+ }
+
+ @Override
+ protected Drawable doInBackground(String... params) {
+ source = params[0];
+
+ if (resources.get() != null) {
+ if (compressImage) {
+ return fetchCompressedDrawable(resources.get(), source);
+ } else {
+ return fetchDrawable(resources.get(), source);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Drawable result) {
+ if (result == null) {
+ Log.w(HtmlTextView.TAG, "Drawable result is null! (source: " + source + ")");
+ return;
+ }
+ final UrlDrawable urlDrawable = drawableReference.get();
+ if (urlDrawable == null) {
+ return;
+ }
+ // set the correct bound according to the result from HTTP call
+ urlDrawable.setBounds(0, 0, (int) (result.getIntrinsicWidth() * scale), (int) (result.getIntrinsicHeight() * scale));
+
+ // change the reference of the current drawable to the result from the HTTP call
+ urlDrawable.drawable = result;
+
+ final HtmlHttpImageGetter imageGetter = imageGetterReference.get();
+ if (imageGetter == null) {
+ return;
+ }
+ // redraw the image by invalidating the container
+ imageGetter.container.invalidate();
+ // re-set text to fix images overlapping text
+ imageGetter.container.setText(imageGetter.container.getText());
+ }
+
+ /**
+ * Get the Drawable from URL
+ */
+ public Drawable fetchDrawable(Resources res, String urlString) {
+ try {
+ InputStream is = fetch(urlString);
+ Drawable drawable = new BitmapDrawable(res, is);
+ scale = getScale(drawable);
+ drawable.setBounds(0, 0, (int) (drawable.getIntrinsicWidth() * scale), (int) (drawable.getIntrinsicHeight() * scale));
+ return drawable;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Get the compressed image with specific quality from URL
+ */
+ public Drawable fetchCompressedDrawable(Resources res, String urlString) {
+ try {
+ InputStream is = fetch(urlString);
+ Bitmap original = new BitmapDrawable(res, is).getBitmap();
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ original.compress(Bitmap.CompressFormat.JPEG, qualityImage, out);
+ original.recycle();
+ is.close();
+
+ Bitmap decoded = BitmapFactory.decodeStream(new ByteArrayInputStream(out.toByteArray()));
+ out.close();
+
+ scale = getScale(decoded);
+ BitmapDrawable b = new BitmapDrawable(res, decoded);
+
+ b.setBounds(0, 0, (int) (b.getIntrinsicWidth() * scale), (int) (b.getIntrinsicHeight() * scale));
+ return b;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private float getScale(Bitmap bitmap) {
+ View container = containerReference.get();
+ if (container == null) {
+ return 1f;
+ }
+
+ float maxWidth = container.getWidth();
+ float originalDrawableWidth = bitmap.getWidth();
+
+ return maxWidth / originalDrawableWidth;
+ }
+
+ private float getScale(Drawable drawable) {
+ View container = containerReference.get();
+ if (!matchParentWidth || container == null) {
+ return 1f;
+ }
+
+ float maxWidth = container.getWidth();
+ float originalDrawableWidth = drawable.getIntrinsicWidth();
+
+ return maxWidth / originalDrawableWidth;
+ }
+
+ private InputStream fetch(String urlString) throws IOException {
+ URL url;
+ final HtmlHttpImageGetter imageGetter = imageGetterReference.get();
+ if (imageGetter == null) {
+ return null;
+ }
+ if (imageGetter.baseUri != null) {
+ url = imageGetter.baseUri.resolve(urlString).toURL();
+ } else {
+ url = URI.create(urlString).toURL();
+ }
+
+ return (InputStream) url.getContent();
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ public class UrlDrawable extends BitmapDrawable {
+ protected Drawable drawable;
+
+ @Override
+ public void draw(Canvas canvas) {
+ // override the draw to facilitate refresh function later
+ if (drawable != null) {
+ drawable.draw(canvas);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlResImageGetter.java b/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlResImageGetter.java
new file mode 100644
index 00000000..781d365a
--- /dev/null
+++ b/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlResImageGetter.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014-2016 Dominik Schürmann
+ * Copyright (C) 2014 drawk
+ *
+ * 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.drawable.Drawable;
+import android.text.Html;
+import android.util.Log;
+import androidx.annotation.NonNull;
+
+/**
+ * Copied from http://stackoverflow.com/a/22298833
+ */
+public class HtmlResImageGetter implements Html.ImageGetter {
+ private Context context;
+
+ public HtmlResImageGetter(@NonNull Context context) {
+ this.context = context;
+ }
+
+ public Drawable getDrawable(String source) {
+ int id = context.getResources().getIdentifier(source, "drawable", context.getPackageName());
+
+ if (id == 0) {
+ // the drawable resource wasn't found in our package, maybe it is a stock android drawable?
+ id = context.getResources().getIdentifier(source, "drawable", "android");
+ }
+
+ if (id == 0) {
+ // prevent a crash if the resource still can't be found
+ Log.e(HtmlTextView.TAG, "source could not be found: " + source);
+ return null;
+ } else {
+ Drawable d = context.getResources().getDrawable(id);
+ d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
+ return d;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlTagHandler.java b/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlTagHandler.java
new file mode 100644
index 00000000..25ef19cf
--- /dev/null
+++ b/app/src/main/java/org/sufficientlysecure/htmltextview/HtmlTagHandler.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright (C) 2013-2015 Dominik Schürmann
+ * Copyright (C) 2013-2015 Juha Kuitunen
+ * Copyright (C) 2013 Mohammed Lakkadshaw
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * 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.text.Editable;
+import android.text.Html;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.style.AlignmentSpan;
+import android.text.style.BulletSpan;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.URLSpan;
+import android.util.Log;
+import android.view.View;
+import androidx.annotation.Nullable;
+import java.util.Stack;
+import org.xml.sax.Attributes;
+
+/**
+ * Some parts of this code are based on android.text.Html
+ */
+public class HtmlTagHandler implements WrapperTagHandler {
+
+ public static final String UNORDERED_LIST = "HTML_TEXTVIEW_ESCAPED_UL_TAG";
+ public static final String ORDERED_LIST = "HTML_TEXTVIEW_ESCAPED_OL_TAG";
+ public static final String LIST_ITEM = "HTML_TEXTVIEW_ESCAPED_LI_TAG";
+ public static final String A_ITEM = "HTML_TEXTVIEW_ESCAPED_A_TAG";
+ public static final String PLACEHOLDER_ITEM = "HTML_TEXTVIEW_ESCAPED_PLACEHOLDER";
+
+ public HtmlTagHandler() {
+ }
+
+ /**
+ * Newer versions of the Android SDK's {@link Html.TagHandler} handles <ul> and <li>
+ * tags itself which means they never get delegated to this class. We want to handle the tags
+ * ourselves so before passing the string html into Html.fromHtml(), we can use this method to
+ * replace the <ul> and <li> tags with tags of our own.
+ *
+ * @param html String containing HTML, for example: "Hello world!"
+ * @return html with replaced