From 2533d5dbe29ca0a00f259b97f0850bd502e279cb Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 23 Nov 2024 21:36:45 +0100 Subject: [PATCH] Add Html-TextView source code instead of referencing a library due to unavailability of JCenter under Apache License 2.0. --- app/build.gradle | 1 - .../htmltextview/ClickableTableSpan.java | 40 ++ .../htmltextview/DesignQuoteSpan.java | 64 +++ .../htmltextview/DrawTableLinkSpan.java | 93 ++++ .../htmltextview/HtmlAssetsImageGetter.java | 63 +++ .../htmltextview/HtmlFormatter.java | 80 ++++ .../htmltextview/HtmlFormatterBuilder.java | 91 ++++ .../htmltextview/HtmlHttpImageGetter.java | 265 +++++++++++ .../htmltextview/HtmlResImageGetter.java | 55 +++ .../htmltextview/HtmlTagHandler.java | 438 ++++++++++++++++++ .../htmltextview/HtmlTextView.java | 181 ++++++++ .../JellyBeanSpanFixTextView.java | 214 +++++++++ .../sufficientlysecure/htmltextview/LICENSE | 202 ++++++++ .../htmltextview/LocalLinkMovementMethod.java | 80 ++++ .../htmltextview/NumberSpan.java | 88 ++++ .../htmltextview/OnClickATagListener.java | 33 ++ .../sufficientlysecure/htmltextview/README.md | 228 +++++++++ .../htmltextview/WrapperContentHandler.java | 104 +++++ .../htmltextview/WrapperTagHandler.java | 25 + build.gradle | 1 - 20 files changed, 2344 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/ClickableTableSpan.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/DesignQuoteSpan.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/DrawTableLinkSpan.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/HtmlAssetsImageGetter.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/HtmlFormatter.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/HtmlFormatterBuilder.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/HtmlHttpImageGetter.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/HtmlResImageGetter.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/HtmlTagHandler.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/HtmlTextView.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/JellyBeanSpanFixTextView.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/LICENSE create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/LocalLinkMovementMethod.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/NumberSpan.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/OnClickATagListener.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/README.md create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/WrapperContentHandler.java create mode 100644 app/src/main/java/org/sufficientlysecure/htmltextview/WrapperTagHandler.java 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