Move TextLinkifier computation out of main thread

This commit is contained in:
Stypox 2021-01-15 20:57:19 +01:00
parent 79e98db3bd
commit 594f0b10ba
No known key found for this signature in database
GPG key ID: 4BDF1B40A49FDD23
4 changed files with 92 additions and 63 deletions

View file

@ -1229,19 +1229,20 @@ public final class VideoDetailFragment
return; return;
} }
if (description.getType() == Description.HTML) { switch (description.getType()) {
TextLinkifier.createLinksFromHtmlBlock(requireContext(), description.getContent(), case Description.HTML:
videoDescriptionView, HtmlCompat.FROM_HTML_MODE_LEGACY); disposables.add(TextLinkifier.createLinksFromHtmlBlock(requireContext(),
videoDescriptionView.setVisibility(View.VISIBLE); description.getContent(), videoDescriptionView,
} else if (description.getType() == Description.MARKDOWN) { HtmlCompat.FROM_HTML_MODE_LEGACY));
TextLinkifier.createLinksFromMarkdownText(requireContext(), description.getContent(), break;
videoDescriptionView); case Description.MARKDOWN:
videoDescriptionView.setVisibility(View.VISIBLE); disposables.add(TextLinkifier.createLinksFromMarkdownText(requireContext(),
} else { description.getContent(), videoDescriptionView));
//== Description.PLAIN_TEXT break;
TextLinkifier.createLinksFromPlainText(requireContext(), description.getContent(), case Description.PLAIN_TEXT: default:
videoDescriptionView); disposables.add(TextLinkifier.createLinksFromPlainText(requireContext(),
videoDescriptionView.setVisibility(View.VISIBLE); description.getContent(), videoDescriptionView));
break;
} }
} }
@ -1557,8 +1558,8 @@ public final class VideoDetailFragment
prepareDescription(info.getDescription()); prepareDescription(info.getDescription());
updateProgressInfo(info); updateProgressInfo(info);
initThumbnailViews(info); initThumbnailViews(info);
showMetaInfoInTextView(info.getMetaInfo(), detailMetaInfoTextView, detailMetaInfoSeparator); disposables.add(showMetaInfoInTextView(info.getMetaInfo(), detailMetaInfoTextView,
detailMetaInfoSeparator));
if (player == null || player.isStopped()) { if (player == null || player.isStopped()) {
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl()); updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());

View file

@ -280,8 +280,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
handleSearchSuggestion(); handleSearchSuggestion();
showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo), disposables.add(showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
metaInfoTextView, metaInfoSeparator); metaInfoTextView, metaInfoSeparator));
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) { if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
initSuggestionObserver(); initSuggestionObserver();
@ -1002,11 +1002,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
// List<MetaInfo> cannot be bundled without creating some containers // List<MetaInfo> cannot be bundled without creating some containers
metaInfo = new MetaInfo[result.getMetaInfo().size()]; metaInfo = new MetaInfo[result.getMetaInfo().size()];
metaInfo = result.getMetaInfo().toArray(metaInfo); metaInfo = result.getMetaInfo().toArray(metaInfo);
disposables.add(showMetaInfoInTextView(result.getMetaInfo(), metaInfoTextView,
metaInfoSeparator));
handleSearchSuggestion(); handleSearchSuggestion();
showMetaInfoInTextView(result.getMetaInfo(), metaInfoTextView, metaInfoSeparator);
lastSearchedString = searchString; lastSearchedString = searchString;
nextPage = result.getNextPage(); nextPage = result.getNextPage();

View file

@ -22,7 +22,6 @@ package org.schabi.newpipe.util;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Handler; import android.os.Handler;
import android.text.method.LinkMovementMethod;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
@ -68,6 +67,7 @@ import java.util.List;
import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
@ -325,10 +325,11 @@ public final class ExtractorHelper {
* @param metaInfos a list of meta information, can be null or empty * @param metaInfos a list of meta information, can be null or empty
* @param metaInfoTextView the text view in which to show the formatted HTML * @param metaInfoTextView the text view in which to show the formatted HTML
* @param metaInfoSeparator another view to be shown or hidden accordingly to the text view * @param metaInfoSeparator another view to be shown or hidden accordingly to the text view
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
*/ */
public static void showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInfos, public static Disposable showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInfos,
final TextView metaInfoTextView, final TextView metaInfoTextView,
final View metaInfoSeparator) { final View metaInfoSeparator) {
final Context context = metaInfoTextView.getContext(); final Context context = metaInfoTextView.getContext();
final boolean showMetaInfo = PreferenceManager.getDefaultSharedPreferences(context) final boolean showMetaInfo = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_meta_info_key), true); .getBoolean(context.getString(R.string.show_meta_info_key), true);
@ -336,6 +337,7 @@ public final class ExtractorHelper {
if (!showMetaInfo || metaInfos == null || metaInfos.isEmpty()) { if (!showMetaInfo || metaInfos == null || metaInfos.isEmpty()) {
metaInfoTextView.setVisibility(View.GONE); metaInfoTextView.setVisibility(View.GONE);
metaInfoSeparator.setVisibility(View.GONE); metaInfoSeparator.setVisibility(View.GONE);
return Disposable.empty();
} else { } else {
final StringBuilder stringBuilder = new StringBuilder(); final StringBuilder stringBuilder = new StringBuilder();
@ -365,11 +367,9 @@ public final class ExtractorHelper {
} }
} }
TextLinkifier.createLinksFromHtmlBlock(context, stringBuilder.toString(),
metaInfoTextView, HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING);
metaInfoTextView.setMovementMethod(LinkMovementMethod.getInstance());
metaInfoTextView.setVisibility(View.VISIBLE);
metaInfoSeparator.setVisibility(View.VISIBLE); metaInfoSeparator.setVisibility(View.VISIBLE);
return TextLinkifier.createLinksFromHtmlBlock(context, stringBuilder.toString(),
metaInfoTextView, HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING);
} }
} }

View file

@ -6,15 +6,23 @@ import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan; import android.text.style.ClickableSpan;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.text.util.Linkify; import android.text.util.Linkify;
import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.text.HtmlCompat; import androidx.core.text.HtmlCompat;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.linkify.LinkifyPlugin; import io.noties.markwon.linkify.LinkifyPlugin;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class TextLinkifier { public final class TextLinkifier {
public static final String TAG = TextLinkifier.class.getSimpleName();
private TextLinkifier() { private TextLinkifier() {
} }
@ -23,20 +31,21 @@ public final class TextLinkifier {
* <p> * <p>
* This will call * This will call
* {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView)} * {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView)}
* after linked the URLs with {@link HtmlCompat#fromHtml(String, int)}. * after having linked the URLs with {@link HtmlCompat#fromHtml(String, int)}.
* *
* @param context the context to use * @param context the context to use
* @param htmlBlock the htmlBlock to be linked * @param htmlBlock the htmlBlock to be linked
* @param textView the TextView to set the htmlBlock linked * @param textView the TextView to set the htmlBlock linked
* @param htmlCompatFlag the int flag to be set when {@link HtmlCompat#fromHtml(String, int)} * @param htmlCompatFlag the int flag to be set when {@link HtmlCompat#fromHtml(String, int)}
* will be called * will be called
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
*/ */
public static void createLinksFromHtmlBlock(final Context context, public static Disposable createLinksFromHtmlBlock(final Context context,
final String htmlBlock, final String htmlBlock,
final TextView textView, final TextView textView,
final int htmlCompatFlag) { final int htmlCompatFlag) {
changeIntentsOfDescriptionLinks(context, HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), return changeIntentsOfDescriptionLinks(context,
textView); HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), textView);
} }
/** /**
@ -44,19 +53,20 @@ public final class TextLinkifier {
* <p> * <p>
* This will call * This will call
* {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView)} * {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView)}
* after linked the URLs with {@link TextView#setAutoLinkMask(int)} and * after having linked the URLs with {@link TextView#setAutoLinkMask(int)} and
* {@link TextView#setText(CharSequence, TextView.BufferType)}. * {@link TextView#setText(CharSequence, TextView.BufferType)}.
* *
* @param context the context to use * @param context the context to use
* @param plainTextBlock the block of plain text to be linked * @param plainTextBlock the block of plain text to be linked
* @param textView the TextView to set the plain text block linked * @param textView the TextView to set the plain text block linked
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
*/ */
public static void createLinksFromPlainText(final Context context, public static Disposable createLinksFromPlainText(final Context context,
final String plainTextBlock, final String plainTextBlock,
final TextView textView) { final TextView textView) {
textView.setAutoLinkMask(Linkify.WEB_URLS); textView.setAutoLinkMask(Linkify.WEB_URLS);
textView.setText(plainTextBlock, TextView.BufferType.SPANNABLE); textView.setText(plainTextBlock, TextView.BufferType.SPANNABLE);
changeIntentsOfDescriptionLinks(context, textView.getText(), textView); return changeIntentsOfDescriptionLinks(context, textView.getText(), textView);
} }
/** /**
@ -70,48 +80,66 @@ public final class TextLinkifier {
* @param context the context to use * @param context the context to use
* @param markdownBlock the block of markdown text to be linked * @param markdownBlock the block of markdown text to be linked
* @param textView the TextView to set the plain text block linked * @param textView the TextView to set the plain text block linked
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
*/ */
public static void createLinksFromMarkdownText(final Context context, public static Disposable createLinksFromMarkdownText(final Context context,
final String markdownBlock, final String markdownBlock,
final TextView textView) { final TextView textView) {
final Markwon markwon = Markwon.builder(context).usePlugin(LinkifyPlugin.create()).build(); final Markwon markwon = Markwon.builder(context).usePlugin(LinkifyPlugin.create()).build();
markwon.setMarkdown(textView, markdownBlock); markwon.setMarkdown(textView, markdownBlock);
changeIntentsOfDescriptionLinks(context, textView.getText(), textView); return changeIntentsOfDescriptionLinks(context, textView.getText(), textView);
} }
/** /**
* Change links generated by libraries in the description of a content to a custom link action. * Change links generated by libraries in the description of a content to a custom link action.
* <p> * <p>
* Instead of using an ACTION_VIEW intent in the description of a content, this method will * Instead of using an {@link android.content.Intent#ACTION_VIEW} intent in the description of a
* parse the CharSequence and replace all current web links with * content, this method will parse the {@link CharSequence} and replace all current web links
* {@link ShareUtils#openUrlInBrowser(Context, String, boolean)}. * with {@link ShareUtils#openUrlInBrowser(Context, String, boolean)}.
* <p> * <p>
* This method is required in order to intercept links and maybe, show a confirmation dialog * This method is required in order to intercept links and e.g. show a confirmation dialog
* before opening a web link. * before opening a web link.
* *
* @param context the context to use * @param context the context to use
* @param chars the CharSequence to be parsed * @param chars the CharSequence to be parsed
* @param textView the TextView in which the converted CharSequence will be applied * @param textView the TextView in which the converted CharSequence will be applied
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
*/ */
private static void changeIntentsOfDescriptionLinks(final Context context, private static Disposable changeIntentsOfDescriptionLinks(final Context context,
final CharSequence chars, final CharSequence chars,
final TextView textView) { final TextView textView) {
final SpannableStringBuilder textBlockLinked = new SpannableStringBuilder(chars); return Single.fromCallable(() -> {
final URLSpan[] urls = textBlockLinked.getSpans(0, chars.length(), URLSpan.class); final SpannableStringBuilder textBlockLinked = new SpannableStringBuilder(chars);
final URLSpan[] urls = textBlockLinked.getSpans(0, chars.length(), URLSpan.class);
for (final URLSpan span : urls) { for (final URLSpan span : urls) {
final ClickableSpan clickableSpan = new ClickableSpan() { final ClickableSpan clickableSpan = new ClickableSpan() {
public void onClick(final View view) { public void onClick(@NonNull final View view) {
ShareUtils.openUrlInBrowser(context, span.getURL(), false); ShareUtils.openUrlInBrowser(context, span.getURL(), false);
} }
}; };
textBlockLinked.setSpan(clickableSpan, textBlockLinked.getSpanStart(span), textBlockLinked.setSpan(clickableSpan, textBlockLinked.getSpanStart(span),
textBlockLinked.getSpanEnd(span), textBlockLinked.getSpanFlags(span)); textBlockLinked.getSpanEnd(span), textBlockLinked.getSpanFlags(span));
textBlockLinked.removeSpan(span); textBlockLinked.removeSpan(span);
} }
textView.setText(textBlockLinked); return textBlockLinked;
}).subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
textBlockLinked -> setTextViewCharSequence(textView, textBlockLinked),
throwable -> {
Log.e(TAG, "Unable to linkify text", throwable);
// this should never happen, but if it does, just fallback to it
setTextViewCharSequence(textView, chars);
});
}
private static void setTextViewCharSequence(final TextView textView,
final CharSequence charSequence) {
textView.setText(charSequence);
textView.setMovementMethod(LinkMovementMethod.getInstance()); textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setVisibility(View.VISIBLE);
} }
} }