From 40317776063b55c52a6e86f36b1ad417ca623fec Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Mon, 1 Feb 2021 16:40:54 +0100 Subject: [PATCH 01/20] Open recognized timestamps in the description of contents in the popup player This commit adds support of opening recognized timestamps in the popup player instead of starting an intent which opens the YouTube website with the video timestamp. --- .../util/CommentTextOnTouchListener.java | 70 +---------- .../schabi/newpipe/util/TextLinkifier.java | 4 +- .../org/schabi/newpipe/util/URLHandler.java | 113 ++++++++++++++++++ 3 files changed, 118 insertions(+), 69 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/util/URLHandler.java diff --git a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java index d26116139..f48d39da0 100644 --- a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java +++ b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.util; -import android.content.Context; import android.text.Layout; import android.text.Selection; import android.text.Spannable; @@ -11,27 +10,9 @@ import android.view.MotionEvent; import android.view.View; import android.widget.TextView; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.exceptions.ParsingException; -import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.player.playqueue.PlayQueue; -import org.schabi.newpipe.player.playqueue.SinglePlayQueue; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.schedulers.Schedulers; - public class CommentTextOnTouchListener implements View.OnTouchListener { public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener(); - private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("(.*)#timestamp=(\\d+)"); - @Override public boolean onTouch(final View v, final MotionEvent event) { if (!(v instanceof TextView)) { @@ -66,7 +47,8 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { if (action == MotionEvent.ACTION_UP) { boolean handled = false; if (link[0] instanceof URLSpan) { - handled = handleUrl(v.getContext(), (URLSpan) link[0]); + handled = URLHandler.handleUrl(v.getContext(), + ((URLSpan) link[0]).getURL(), 1); } if (!handled) { ShareUtils.openUrlInBrowser(v.getContext(), @@ -83,52 +65,4 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { } return false; } - - private boolean handleUrl(final Context context, final URLSpan urlSpan) { - String url = urlSpan.getURL(); - int seconds = -1; - final Matcher matcher = TIMESTAMP_PATTERN.matcher(url); - if (matcher.matches()) { - url = matcher.group(1); - seconds = Integer.parseInt(matcher.group(2)); - } - final StreamingService service; - final StreamingService.LinkType linkType; - try { - service = NewPipe.getServiceByUrl(url); - linkType = service.getLinkTypeByUrl(url); - } catch (final ExtractionException e) { - return false; - } - if (linkType == StreamingService.LinkType.NONE) { - return false; - } - if (linkType == StreamingService.LinkType.STREAM && seconds != -1) { - return playOnPopup(context, url, service, seconds); - } else { - NavigationHelper.openRouterActivity(context, url); - return true; - } - } - - private boolean playOnPopup(final Context context, final String url, - final StreamingService service, final int seconds) { - final LinkHandlerFactory factory = service.getStreamLHFactory(); - final String cleanUrl; - try { - cleanUrl = factory.getUrl(factory.getId(url)); - } catch (final ParsingException e) { - return false; - } - final Single single - = ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false); - single.subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(info -> { - final PlayQueue playQueue - = new SinglePlayQueue((StreamInfo) info, seconds * 1000); - NavigationHelper.playOnPopupPlayer(context, playQueue, false); - }); - return true; - } } diff --git a/app/src/main/java/org/schabi/newpipe/util/TextLinkifier.java b/app/src/main/java/org/schabi/newpipe/util/TextLinkifier.java index 087677333..4fc3608bf 100644 --- a/app/src/main/java/org/schabi/newpipe/util/TextLinkifier.java +++ b/app/src/main/java/org/schabi/newpipe/util/TextLinkifier.java @@ -115,7 +115,9 @@ public final class TextLinkifier { for (final URLSpan span : urls) { final ClickableSpan clickableSpan = new ClickableSpan() { public void onClick(@NonNull final View view) { - ShareUtils.openUrlInBrowser(context, span.getURL(), false); + if (!URLHandler.handleUrl(context, span.getURL(), 0)) { + ShareUtils.openUrlInBrowser(context, span.getURL(), false); + } } }; diff --git a/app/src/main/java/org/schabi/newpipe/util/URLHandler.java b/app/src/main/java/org/schabi/newpipe/util/URLHandler.java new file mode 100644 index 000000000..761a91e50 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/URLHandler.java @@ -0,0 +1,113 @@ +package org.schabi.newpipe.util; + +import android.content.Context; + +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.SinglePlayQueue; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public final class URLHandler { + + private URLHandler() { + } + + /** + * Check if an URL can be handled in NewPipe. + *
+ * This method will check if the provided url can be handled in NewPipe or not. If this is a + * service URL with a timestamp, the popup player will be opened. + *
+ * The timestamp param accepts two integers, corresponding to two timestamps types: + * 0 for {@code &t=} (used for timestamps in descriptions), + * 1 for {@code #timestamp=} (used for timestamps in comments). + * Any other value of this integer will return false. + * + * @param context the context to be used + * @param url the URL to check if it can be handled + * @param timestampType the type of timestamp + * @return true if the URL can be handled by NewPipe, false if it cannot + */ + public static boolean handleUrl(final Context context, final String url, final int timestampType) { + String matchedUrl = ""; + int seconds = -1; + final Pattern TIMESTAMP_PATTERN; + + if (timestampType == 0) { + TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)"); + } else if (timestampType == 1) { + TIMESTAMP_PATTERN = Pattern.compile("(.*)#timestamp=(\\d+)"); + } else { + return false; + } + + final Matcher matcher = TIMESTAMP_PATTERN.matcher(url); + if (matcher.matches()) { + matchedUrl = matcher.group(1); + seconds = Integer.parseInt(matcher.group(2)); + } + + final StreamingService service; + final StreamingService.LinkType linkType; + + try { + service = NewPipe.getServiceByUrl(matchedUrl); + linkType = service.getLinkTypeByUrl(matchedUrl); + } catch (final ExtractionException e) { + return false; + } + + if (linkType == StreamingService.LinkType.NONE) { + return false; + } + if (linkType == StreamingService.LinkType.STREAM && seconds != -1) { + return playOnPopup(context, matchedUrl, service, seconds); + } else { + NavigationHelper.openRouterActivity(context, matchedUrl); + return true; + } + } + + /** + * Play a content in the floating player. + * + * @param context the context to be used + * @param url the URL of the content + * @param service the service of the content + * @param seconds the position in seconds at which the floating player will start + * @return true if the playback of the content has successfully started or false if not + */ + private static boolean playOnPopup(final Context context, final String url, + final StreamingService service, final int seconds) { + final LinkHandlerFactory factory = service.getStreamLHFactory(); + final String cleanUrl; + + try { + cleanUrl = factory.getUrl(factory.getId(url)); + } catch (final ParsingException e) { + return false; + } + + final Single single + = ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false); + single.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(info -> { + final PlayQueue playQueue + = new SinglePlayQueue((StreamInfo) info, seconds * 1000); + NavigationHelper.playOnPopupPlayer(context, playQueue, false); + }); + return true; + } +} From ae9349e36c05f0f0a3c368dce453344a25e68578 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sat, 13 Mar 2021 12:20:45 +0100 Subject: [PATCH 02/20] Initial work: add support for opening timestamps in plain text descriptions This commit adds support for opening plain text timestamps by parsing the description text using a regular expression, add a click listener for each timestamp which opens the popup player at the indicated time in the timestamp. In order to do this, playOnPopup method of the URLHandler class. Also, handleUrl method of this class has been renamed to canHandleUrl. --- .../fragments/detail/DescriptionFragment.java | 14 +- .../util/CommentTextOnTouchListener.java | 2 +- .../schabi/newpipe/util/ExtractorHelper.java | 3 +- .../schabi/newpipe/util/TextLinkifier.java | 149 ++++++++++++++---- .../org/schabi/newpipe/util/URLHandler.java | 22 ++- 5 files changed, 150 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java index 5f1cbc365..49ac3ef01 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java @@ -19,6 +19,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.FragmentDescriptionBinding; import org.schabi.newpipe.databinding.ItemMetadataBinding; import org.schabi.newpipe.databinding.ItemMetadataTagsBinding; +import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.util.Localization; @@ -131,19 +132,24 @@ public class DescriptionFragment extends BaseFragment { private void loadDescriptionContent() { final Description description = streamInfo.getDescription(); + final String contentUrl = streamInfo.getUrl(); + final StreamingService service = streamInfo.getService(); + switch (description.getType()) { case Description.HTML: descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock(requireContext(), description.getContent(), binding.detailDescriptionView, - HtmlCompat.FROM_HTML_MODE_LEGACY); + service, contentUrl, HtmlCompat.FROM_HTML_MODE_LEGACY); break; case Description.MARKDOWN: descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(), - description.getContent(), binding.detailDescriptionView); + description.getContent(), binding.detailDescriptionView, + service, contentUrl); break; case Description.PLAIN_TEXT: default: descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(), - description.getContent(), binding.detailDescriptionView); + description.getContent(), binding.detailDescriptionView, + service, contentUrl); break; } } @@ -199,7 +205,7 @@ public class DescriptionFragment extends BaseFragment { if (linkifyContent) { TextLinkifier.createLinksFromPlainText(requireContext(), - content, itemBinding.metadataContentView); + content, itemBinding.metadataContentView, null, null); } else { itemBinding.metadataContentView.setText(content); } diff --git a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java index f48d39da0..51d8539f4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java +++ b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java @@ -47,7 +47,7 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { if (action == MotionEvent.ACTION_UP) { boolean handled = false; if (link[0] instanceof URLSpan) { - handled = URLHandler.handleUrl(v.getContext(), + handled = URLHandler.canHandleUrl(v.getContext(), ((URLSpan) link[0]).getURL(), 1); } if (!handled) { diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index af7cafc15..0b8dbb751 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -311,7 +311,8 @@ public final class ExtractorHelper { metaInfoSeparator.setVisibility(View.VISIBLE); return TextLinkifier.createLinksFromHtmlBlock(context, stringBuilder.toString(), - metaInfoTextView, HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING); + metaInfoTextView, null, null, + HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/TextLinkifier.java b/app/src/main/java/org/schabi/newpipe/util/TextLinkifier.java index 4fc3608bf..37bc80f72 100644 --- a/app/src/main/java/org/schabi/newpipe/util/TextLinkifier.java +++ b/app/src/main/java/org/schabi/newpipe/util/TextLinkifier.java @@ -13,6 +13,11 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.text.HtmlCompat; +import org.schabi.newpipe.extractor.StreamingService; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import io.noties.markwon.Markwon; import io.noties.markwon.linkify.LinkifyPlugin; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; @@ -20,6 +25,8 @@ import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; +import static org.schabi.newpipe.util.URLHandler.playOnPopup; + public final class TextLinkifier { public static final String TAG = TextLinkifier.class.getSimpleName(); @@ -30,92 +37,173 @@ public final class TextLinkifier { * Create web links for contents with an HTML description. *
* This will call - * {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView)} + * {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView, + * StreamingService, String)} * after having linked the URLs with {@link HtmlCompat#fromHtml(String, int)}. * - * @param context the context to use - * @param htmlBlock the htmlBlock to be linked - * @param textView the TextView to set the htmlBlock linked - * @param htmlCompatFlag the int flag to be set when {@link HtmlCompat#fromHtml(String, int)} - * will be called + * @param context the context to use + * @param htmlBlock the htmlBlock to be linked + * @param textView the TextView to set the htmlBlock linked + * @param streamingService the {@link StreamingService} of the content + * @param contentUrl the URL of the content + * @param htmlCompatFlag the int flag to be set when {@link HtmlCompat#fromHtml(String, int)} + * will be called * @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed */ public static Disposable createLinksFromHtmlBlock(final Context context, final String htmlBlock, final TextView textView, + final StreamingService streamingService, + final String contentUrl, final int htmlCompatFlag) { return changeIntentsOfDescriptionLinks(context, - HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), textView); + HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), textView, streamingService, + contentUrl); } /** * Create web links for contents with a plain text description. *
* This will call - * {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView)} + * {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView, + * StreamingService, String)} * after having linked the URLs with {@link TextView#setAutoLinkMask(int)} and * {@link TextView#setText(CharSequence, TextView.BufferType)}. * - * @param context the context to use - * @param plainTextBlock the block of plain text to be linked - * @param textView the TextView to set the plain text block linked + * @param context the context to use + * @param plainTextBlock the block of plain text to be linked + * @param textView the TextView to set the plain text block linked + * @param streamingService the {@link StreamingService} of the content + * @param contentUrl the URL of the content * @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed */ public static Disposable createLinksFromPlainText(final Context context, final String plainTextBlock, - final TextView textView) { + final TextView textView, + final StreamingService streamingService, + final String contentUrl) { textView.setAutoLinkMask(Linkify.WEB_URLS); textView.setText(plainTextBlock, TextView.BufferType.SPANNABLE); - return changeIntentsOfDescriptionLinks(context, textView.getText(), textView); + return changeIntentsOfDescriptionLinks(context, textView.getText(), textView, + streamingService, contentUrl); } /** * Create web links for contents with a markdown description. *
* This will call - * {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView)} + * {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView, + * StreamingService, String)} * after creating an {@link Markwon} object and using * {@link Markwon#setMarkdown(TextView, String)}. * - * @param context the context to use - * @param markdownBlock the block of markdown text to be linked - * @param textView the TextView to set the plain text block linked + * @param context the context to use + * @param markdownBlock the block of markdown text to be linked + * @param textView the TextView to set the plain text block linked + * @param streamingService the {@link StreamingService} of the content + * @param contentUrl the URL of the content * @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed */ public static Disposable createLinksFromMarkdownText(final Context context, final String markdownBlock, - final TextView textView) { + final TextView textView, + final StreamingService streamingService, + final String contentUrl) { final Markwon markwon = Markwon.builder(context).usePlugin(LinkifyPlugin.create()).build(); markwon.setMarkdown(textView, markdownBlock); - return changeIntentsOfDescriptionLinks(context, textView.getText(), textView); + return changeIntentsOfDescriptionLinks(context, textView.getText(), textView, + streamingService, contentUrl); + } + + private static final Pattern TIMESTAMPS_PATTERN_IN_PLAIN_TEXT = + Pattern.compile("(?:([0-5]?[0-9]):)?([0-5]?[0-9]):([0-5][0-9])"); + + /** + * Add click listeners which opens the popup player on timestamps in a plain text. + *
+ * This method finds all timestamps in the {@link SpannableStringBuilder} of the description + * using a regular expression, adds for each a {@link ClickableSpan} which opens the popup + * player at the time indicated in the timestamps. + * + * @param context the context to use + * @param spannableDescription the SpannableStringBuilder with the text of the + * content description + * @param contentUrl the URL of the content + * @param streamingService the {@link StreamingService} of the content + */ + private static void addClickListenersOnTimestamps(final Context context, + final SpannableStringBuilder + spannableDescription, + final String contentUrl, + final StreamingService streamingService) { + final String descriptionText = spannableDescription.toString(); + final Matcher timestampMatches = TIMESTAMPS_PATTERN_IN_PLAIN_TEXT.matcher(descriptionText); + + while (timestampMatches.find()) { + final int timestampStart = timestampMatches.start(0); + final int timestampEnd = timestampMatches.end(0); + final String parsedTimestamp = descriptionText.substring(timestampStart, timestampEnd); + final String[] timestampParts = parsedTimestamp.split(":"); + final int seconds; + if (timestampParts.length == 3) { // timestamp format: XX:XX:XX + seconds = Integer.parseInt(timestampParts[0]) * 3600 + Integer.parseInt( + timestampParts[1]) * 60 + Integer.parseInt(timestampParts[2]); + spannableDescription.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull final View view) { + playOnPopup(context, contentUrl, streamingService, seconds); + } + }, timestampStart, timestampEnd, 0); + } else if (timestampParts.length == 2) { // timestamp format: XX:XX + seconds = Integer.parseInt(timestampParts[0]) * 60 + Integer.parseInt( + timestampParts[1]); + spannableDescription.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull final View view) { + playOnPopup(context, contentUrl, streamingService, seconds); + } + }, timestampStart, timestampEnd, 0); + } + } } /** - * 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 + * and add click listeners on timestamps in this description. *
- * Instead of using an {@link android.content.Intent#ACTION_VIEW} intent in the description of a - * content, this method will parse the {@link CharSequence} and replace all current web links + * Instead of using an {@link android.content.Intent#ACTION_VIEW} intent in the description of + * a content, this method will parse the {@link CharSequence} and replace all current web links * with {@link ShareUtils#openUrlInBrowser(Context, String, boolean)}. + * This method will also add click listeners on timestamps in this description, which will play + * the content in the popup player at the time indicated in the timestamp, by using + * {@link TextLinkifier#addClickListenersOnTimestamps(Context, SpannableStringBuilder, String, + * StreamingService)} method. *
* This method is required in order to intercept links and e.g. show a confirmation dialog * before opening a web link. * - * @param context the context to use - * @param chars the CharSequence to be parsed - * @param textView the TextView in which the converted CharSequence will be applied + * @param context the context to use + * @param chars the CharSequence to be parsed + * @param textView the TextView in which the converted CharSequence will be applied + * @param streamingService the {@link StreamingService} of the content + * @param contentUrl the URL of the content * @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed */ private static Disposable changeIntentsOfDescriptionLinks(final Context context, final CharSequence chars, - final TextView textView) { + final TextView textView, + final StreamingService + streamingService, + final String contentUrl) { return Single.fromCallable(() -> { + // add custom click actions on web links final SpannableStringBuilder textBlockLinked = new SpannableStringBuilder(chars); final URLSpan[] urls = textBlockLinked.getSpans(0, chars.length(), URLSpan.class); for (final URLSpan span : urls) { final ClickableSpan clickableSpan = new ClickableSpan() { public void onClick(@NonNull final View view) { - if (!URLHandler.handleUrl(context, span.getURL(), 0)) { + if (!URLHandler.canHandleUrl(context, span.getURL(), 0)) { ShareUtils.openUrlInBrowser(context, span.getURL(), false); } } @@ -126,6 +214,13 @@ public final class TextLinkifier { textBlockLinked.removeSpan(span); } + // add click actions on plain text timestamps only for description of contents, + // unneeded for metainfo TextViews + if (contentUrl != null || streamingService != null) { + addClickListenersOnTimestamps(context, textBlockLinked, contentUrl, + streamingService); + } + return textBlockLinked; }).subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/org/schabi/newpipe/util/URLHandler.java b/app/src/main/java/org/schabi/newpipe/util/URLHandler.java index 761a91e50..17555f0f9 100644 --- a/app/src/main/java/org/schabi/newpipe/util/URLHandler.java +++ b/app/src/main/java/org/schabi/newpipe/util/URLHandler.java @@ -39,25 +39,31 @@ public final class URLHandler { * @param timestampType the type of timestamp * @return true if the URL can be handled by NewPipe, false if it cannot */ - public static boolean handleUrl(final Context context, final String url, final int timestampType) { + public static boolean canHandleUrl(final Context context, + final String url, + final int timestampType) { String matchedUrl = ""; int seconds = -1; - final Pattern TIMESTAMP_PATTERN; + final Pattern timestampPattern; if (timestampType == 0) { - TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)"); + timestampPattern = Pattern.compile("(.*)&t=(\\d+)"); } else if (timestampType == 1) { - TIMESTAMP_PATTERN = Pattern.compile("(.*)#timestamp=(\\d+)"); + timestampPattern = Pattern.compile("(.*)#timestamp=(\\d+)"); } else { return false; } - final Matcher matcher = TIMESTAMP_PATTERN.matcher(url); + final Matcher matcher = timestampPattern.matcher(url); if (matcher.matches()) { matchedUrl = matcher.group(1); seconds = Integer.parseInt(matcher.group(2)); } + if (matchedUrl == null || matchedUrl.isEmpty()) { + return false; + } + final StreamingService service; final StreamingService.LinkType linkType; @@ -88,8 +94,10 @@ public final class URLHandler { * @param seconds the position in seconds at which the floating player will start * @return true if the playback of the content has successfully started or false if not */ - private static boolean playOnPopup(final Context context, final String url, - final StreamingService service, final int seconds) { + public static boolean playOnPopup(final Context context, + final String url, + final StreamingService service, + final int seconds) { final LinkHandlerFactory factory = service.getStreamLHFactory(); final String cleanUrl; From 9e9d1a04e46fba9bf5c597dac5d39a924d1be308 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sat, 13 Mar 2021 12:37:54 +0100 Subject: [PATCH 03/20] Fix toast shown when falling back to Google Play Store URL and the action of Open with Kodi button in the player Add a boolean param, showToast, in ShareUtils.openIntentInApp and only show toast "No app on your device can open this" if this boolean is true. Fix the action of play with Kodi button by applying the fix provided in #5599 (adding the flag Intent.FLAG_ACTIVITY_NEW_TASK to the intent in NavigationHelper.playWithKore method). Do also some cleanup in viewWithFileProvider and shareFile methods of MissionAdapter class. --- .../schabi/newpipe/error/ErrorActivity.java | 5 +- .../schabi/newpipe/util/NavigationHelper.java | 2 +- .../org/schabi/newpipe/util/ShareUtils.java | 65 ++++++++++++------- .../giga/ui/adapter/MissionAdapter.java | 18 ++--- 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java index 106a86cfa..5fc9a72ca 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java @@ -220,13 +220,10 @@ public class ErrorActivity extends AppCompatActivity { + getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME) .putExtra(Intent.EXTRA_TEXT, buildJson()); - if (i.resolveActivity(getPackageManager()) != null) { - ShareUtils.openIntentInApp(context, i); - } + ShareUtils.openIntentInApp(context, i, true); } else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false); } - }) .setNegativeButton(R.string.decline, (dialog, which) -> { // do nothing diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 106399735..2f1851efe 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -252,7 +252,7 @@ public final class NavigationHelper { public static void resolveActivityOrAskToInstall(final Context context, final Intent intent) { if (intent.resolveActivity(context.getPackageManager()) != null) { - ShareUtils.openIntentInApp(context, intent); + ShareUtils.openIntentInApp(context, intent, false); } else { if (context instanceof Activity) { new AlertDialog.Builder(context) diff --git a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java index 45ec1d015..18b2aa5d0 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java @@ -25,9 +25,9 @@ public final class ShareUtils { * second param (a system chooser will be opened if there are multiple markets and no default) * and falls back to Google Play Store web URL if no app to handle the market scheme was found. *
- * It uses {@link ShareUtils#openIntentInApp(Context, Intent)} to open market scheme and - * {@link ShareUtils#openUrlInBrowser(Context, String, boolean)} to open Google Play Store web - * URL with false for the boolean param. + * It uses {@link ShareUtils#openIntentInApp(Context, Intent, boolean)} to open market scheme + * and {@link ShareUtils#openUrlInBrowser(Context, String, boolean)} to open Google Play Store + * web URL with false for the boolean param. * * @param context the context to use * @param packageId the package id of the app to be installed @@ -36,7 +36,7 @@ public final class ShareUtils { // Try market:// scheme final boolean marketSchemeResult = openIntentInApp(context, new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + packageId)) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), false); if (!marketSchemeResult) { // Fall back to Google Play Store Web URL (F-Droid can handle it) openUrlInBrowser(context, @@ -48,7 +48,7 @@ public final class ShareUtils { * Open the url with the system default browser. *
* If no browser is set as default, fallbacks to - * {@link ShareUtils#openAppChooser(Context, Intent, String)} + * {@link ShareUtils#openAppChooser(Context, Intent, boolean)} * * @param context the context to use * @param url the url to browse @@ -71,7 +71,7 @@ public final class ShareUtils { if (defaultPackageName.equals("android")) { // No browser set as default (doesn't work on some devices) - openAppChooser(context, intent, context.getString(R.string.open_with)); + openAppChooser(context, intent, true); } else { if (defaultPackageName.isEmpty()) { // No app installed to open a web url @@ -84,7 +84,7 @@ public final class ShareUtils { } catch (final ActivityNotFoundException e) { // Not a browser but an app chooser because of OEMs changes intent.setPackage(null); - openAppChooser(context, intent, context.getString(R.string.open_with)); + openAppChooser(context, intent, true); } } } @@ -96,7 +96,7 @@ public final class ShareUtils { * Open the url with the system default browser. *
* If no browser is set as default, fallbacks to - * {@link ShareUtils#openAppChooser(Context, Intent, String)} + * {@link ShareUtils#openAppChooser(Context, Intent, boolean)} *
* This calls {@link ShareUtils#openUrlInBrowser(Context, String, boolean)} with true * for the boolean parameter @@ -116,22 +116,29 @@ public final class ShareUtils { * {@link ShareUtils#openUrlInBrowser(Context, String, boolean)} should be used. *
* If no app is set as default, fallbacks to - * {@link ShareUtils#openAppChooser(Context, Intent, String)} + * {@link ShareUtils#openAppChooser(Context, Intent, boolean)}. + *
* - * @param context the context to use - * @param intent the intent to open + * @param context the context to use + * @param intent the intent to open + * @param showToast the boolean to set if a toast is displayed to user when no app is installed + * to open the intent (true) or not (false) * @return true if the intent can be opened or false if it cannot be */ - public static boolean openIntentInApp(final Context context, final Intent intent) { + public static boolean openIntentInApp(final Context context, final Intent intent, + final boolean showToast) { final String defaultPackageName = getDefaultAppPackageName(context, intent); if (defaultPackageName.equals("android")) { // No app set as default (doesn't work on some devices) - openAppChooser(context, intent, context.getString(R.string.open_with)); + openAppChooser(context, intent, true); } else { if (defaultPackageName.isEmpty()) { // No app installed to open the intent - Toast.makeText(context, R.string.no_app_to_open_intent, Toast.LENGTH_LONG).show(); + if (showToast) { + Toast.makeText(context, R.string.no_app_to_open_intent, Toast.LENGTH_LONG) + .show(); + } return false; } else { try { @@ -140,7 +147,7 @@ public final class ShareUtils { } catch (final ActivityNotFoundException e) { // Not an app to open the intent but an app chooser because of OEMs changes intent.setPackage(null); - openAppChooser(context, intent, context.getString(R.string.open_with)); + openAppChooser(context, intent, true); } } } @@ -152,18 +159,25 @@ public final class ShareUtils { * Open the system chooser to launch an intent. *
* This method opens an {@link android.content.Intent#ACTION_CHOOSER} of the intent putted
- * as the viewIntent param. A string for the chooser's title must be passed as the last param.
+ * as the intent param. If the setTitleChooser boolean is true, the string "Open with" will be
+ * set as the title of the system chooser.
+ * For Android P and higher, title for {@link android.content.Intent#ACTION_SEND} system
+ * choosers must be set on this intent, not on the
+ * {@link android.content.Intent#ACTION_CHOOSER} intent.
*
- * @param context the context to use
- * @param intent the intent to open
- * @param chooserStringTitle the string of chooser's title
+ * @param context the context to use
+ * @param intent the intent to open
+ * @param setTitleChooser set the title "Open with" to the chooser if true, else not
*/
- private static void openAppChooser(final Context context, final Intent intent,
- final String chooserStringTitle) {
+ private static void openAppChooser(final Context context,
+ final Intent intent,
+ final boolean setTitleChooser) {
final Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
chooserIntent.putExtra(Intent.EXTRA_INTENT, intent);
- chooserIntent.putExtra(Intent.EXTRA_TITLE, chooserStringTitle);
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ if (setTitleChooser) {
+ chooserIntent.putExtra(Intent.EXTRA_TITLE, context.getString(R.string.open_with));
+ }
context.startActivity(chooserIntent);
}
@@ -201,10 +215,13 @@ public final class ShareUtils {
public static void shareText(final Context context, final String subject, final String url) {
final Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
- shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
+ if (!subject.isEmpty()) {
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
+ }
shareIntent.putExtra(Intent.EXTRA_TEXT, url);
+ shareIntent.putExtra(Intent.EXTRA_TITLE, context.getString(R.string.share_dialog_title));
- openAppChooser(context, shareIntent, context.getString(R.string.share_dialog_title));
+ openAppChooser(context, shareIntent, false);
}
/**
diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
index 45ee290f6..b31933dfd 100644
--- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
+++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
@@ -348,10 +348,8 @@ public class MissionAdapter extends Adapter
@@ -125,11 +124,11 @@ public final class TextLinkifier {
* using a regular expression, adds for each a {@link ClickableSpan} which opens the popup
* player at the time indicated in the timestamps.
*
- * @param context the context to use
- * @param spannableDescription the SpannableStringBuilder with the text of the
- * content description
- * @param contentUrl the URL of the content
- * @param streamingService the {@link StreamingService} of the content
+ * @param context the context to use
+ * @param spannableDescription the SpannableStringBuilder with the text of the
+ * content description
+ * @param contentUrl the URL of the content
+ * @param streamingService the {@link StreamingService} of the content
*/
private static void addClickListenersOnTimestamps(final Context context,
final SpannableStringBuilder
@@ -144,23 +143,24 @@ public final class TextLinkifier {
final int timestampEnd = timestampMatches.end(0);
final String parsedTimestamp = descriptionText.substring(timestampStart, timestampEnd);
final String[] timestampParts = parsedTimestamp.split(":");
- final int seconds;
+ final int time;
if (timestampParts.length == 3) { // timestamp format: XX:XX:XX
- seconds = Integer.parseInt(timestampParts[0]) * 3600 + Integer.parseInt(
- timestampParts[1]) * 60 + Integer.parseInt(timestampParts[2]);
+ time = Integer.parseInt(timestampParts[0]) * 3600 // hours
+ + Integer.parseInt(timestampParts[1]) * 60 // minutes
+ + Integer.parseInt(timestampParts[2]); // seconds
spannableDescription.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull final View view) {
- playOnPopup(context, contentUrl, streamingService, seconds);
+ playOnPopup(context, contentUrl, streamingService, time);
}
}, timestampStart, timestampEnd, 0);
} else if (timestampParts.length == 2) { // timestamp format: XX:XX
- seconds = Integer.parseInt(timestampParts[0]) * 60 + Integer.parseInt(
- timestampParts[1]);
+ time = Integer.parseInt(timestampParts[0]) * 60 // minutes
+ + Integer.parseInt(timestampParts[1]); // seconds
spannableDescription.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull final View view) {
- playOnPopup(context, contentUrl, streamingService, seconds);
+ playOnPopup(context, contentUrl, streamingService, time);
}
}, timestampStart, timestampEnd, 0);
}
@@ -203,7 +203,7 @@ public final class TextLinkifier {
for (final URLSpan span : urls) {
final ClickableSpan clickableSpan = new ClickableSpan() {
public void onClick(@NonNull final View view) {
- if (!URLHandler.canHandleUrl(context, span.getURL(), 0)) {
+ if (!URLHandler.handleUrl(context, span.getURL(), 0)) {
ShareUtils.openUrlInBrowser(context, span.getURL(), false);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/URLHandler.java b/app/src/main/java/org/schabi/newpipe/util/URLHandler.java
index 17555f0f9..6c5c574e8 100644
--- a/app/src/main/java/org/schabi/newpipe/util/URLHandler.java
+++ b/app/src/main/java/org/schabi/newpipe/util/URLHandler.java
@@ -19,12 +19,15 @@ import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class URLHandler {
+ private static final Pattern AMPERSAND_TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)");
+ private static final Pattern HASHTAG_TIMESTAMP_PATTERN =
+ Pattern.compile("(.*)#timestamp=(\\d+)");
private URLHandler() {
}
/**
- * Check if an URL can be handled in NewPipe.
+ * Handle an URL in NewPipe.
*
* This method will check if the provided url can be handled in NewPipe or not. If this is a
* service URL with a timestamp, the popup player will be opened.
@@ -39,17 +42,17 @@ public final class URLHandler {
* @param timestampType the type of timestamp
* @return true if the URL can be handled by NewPipe, false if it cannot
*/
- public static boolean canHandleUrl(final Context context,
- final String url,
- final int timestampType) {
+ public static boolean handleUrl(final Context context,
+ final String url,
+ final int timestampType) {
String matchedUrl = "";
int seconds = -1;
final Pattern timestampPattern;
if (timestampType == 0) {
- timestampPattern = Pattern.compile("(.*)&t=(\\d+)");
+ timestampPattern = AMPERSAND_TIMESTAMP_PATTERN;
} else if (timestampType == 1) {
- timestampPattern = Pattern.compile("(.*)#timestamp=(\\d+)");
+ timestampPattern = HASHTAG_TIMESTAMP_PATTERN;
} else {
return false;
}
@@ -107,13 +110,13 @@ public final class URLHandler {
return false;
}
- final Single single
+ final Single
- * It uses {@link ShareUtils#openIntentInApp(Context, Intent, boolean)} to open market scheme
- * and {@link ShareUtils#openUrlInBrowser(Context, String, boolean)} to open Google Play Store
+ * It uses {@link #openIntentInApp(Context, Intent, boolean)} to open market scheme
+ * and {@link #openUrlInBrowser(Context, String, boolean)} to open Google Play Store
* web URL with false for the boolean param.
*
* @param context the context to use
* @param packageId the package id of the app to be installed
*/
public static void installApp(final Context context, final String packageId) {
- // Try market:// scheme
+ // Try market scheme
final boolean marketSchemeResult = openIntentInApp(context, new Intent(Intent.ACTION_VIEW,
Uri.parse("market://details?id=" + packageId))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), false);
@@ -48,7 +49,7 @@ public final class ShareUtils {
* Open the url with the system default browser.
*
* If no browser is set as default, fallbacks to
- * {@link ShareUtils#openAppChooser(Context, Intent, boolean)}
+ * {@link #openAppChooser(Context, Intent, boolean)}
*
* @param context the context to use
* @param url the url to browse
@@ -56,7 +57,8 @@ public final class ShareUtils {
* for HTTP protocol or for the created intent
* @return true if the URL can be opened or false if it cannot
*/
- public static boolean openUrlInBrowser(final Context context, final String url,
+ public static boolean openUrlInBrowser(final Context context,
+ final String url,
final boolean httpDefaultBrowserTest) {
final String defaultPackageName;
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url))
@@ -96,9 +98,9 @@ public final class ShareUtils {
* Open the url with the system default browser.
*
* If no browser is set as default, fallbacks to
- * {@link ShareUtils#openAppChooser(Context, Intent, boolean)}
+ * {@link #openAppChooser(Context, Intent, boolean)}
*
- * This calls {@link ShareUtils#openUrlInBrowser(Context, String, boolean)} with true
+ * This calls {@link #openUrlInBrowser(Context, String, boolean)} with true
* for the boolean parameter
*
* @param context the context to use
@@ -113,19 +115,20 @@ public final class ShareUtils {
* Open an intent with the system default app.
*
* The intent can be of every type, excepted a web intent for which
- * {@link ShareUtils#openUrlInBrowser(Context, String, boolean)} should be used.
+ * {@link #openUrlInBrowser(Context, String, boolean)} should be used.
*
* If no app is set as default, fallbacks to
- * {@link ShareUtils#openAppChooser(Context, Intent, boolean)}.
+ * {@link #openAppChooser(Context, Intent, boolean)}.
*
*
* @param context the context to use
* @param intent the intent to open
- * @param showToast the boolean to set if a toast is displayed to user when no app is installed
+ * @param showToast a boolean to set if a toast is displayed to user when no app is installed
* to open the intent (true) or not (false)
* @return true if the intent can be opened or false if it cannot be
*/
- public static boolean openIntentInApp(final Context context, final Intent intent,
+ public static boolean openIntentInApp(final Context context,
+ final Intent intent,
final boolean showToast) {
final String defaultPackageName = getDefaultAppPackageName(context, intent);
@@ -178,6 +181,36 @@ public final class ShareUtils {
if (setTitleChooser) {
chooserIntent.putExtra(Intent.EXTRA_TITLE, context.getString(R.string.open_with));
}
+
+ // Migrate any clip data and flags from the original intent.
+ final int permFlags;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ permFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+ } else {
+ permFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
+ }
+ if (permFlags != 0) {
+ ClipData targetClipData = intent.getClipData();
+ if (targetClipData == null && intent.getData() != null) {
+ final ClipData.Item item = new ClipData.Item(intent.getData());
+ final String[] mimeTypes;
+ if (intent.getType() != null) {
+ mimeTypes = new String[] {intent.getType()};
+ } else {
+ mimeTypes = new String[] {};
+ }
+ targetClipData = new ClipData(null, mimeTypes, item);
+ }
+ if (targetClipData != null) {
+ chooserIntent.setClipData(targetClipData);
+ chooserIntent.addFlags(permFlags);
+ }
+ }
context.startActivity(chooserIntent);
}
@@ -208,24 +241,45 @@ public final class ShareUtils {
/**
* Open the android share menu to share the current url.
*
- * @param context the context to use
- * @param subject the url subject, typically the title
- * @param url the url to share
+ * @param context the context to use
+ * @param subject the url subject, typically the title
+ * @param url the url to share
+ * @param imagePreviewUrl the image of the subject
*/
- public static void shareText(final Context context, final String subject, final String url) {
- shareText(context, subject, url, true);
- }
-
-
public static void shareText(final Context context,
final String subject,
final String url,
+ final String imagePreviewUrl) {
+ shareText(context, subject, url, imagePreviewUrl, true);
+ }
+
+ /**
+ * Open the android share sheet to share the current url.
+ *
+ * For Android 10+ users, a content preview is shown, which includes the title of the shared
+ * content.
+ * Support sharing the image of the content needs to done, if possible.
+ *
+ * @param context the context to use
+ * @param subject the url subject, typically the title
+ * @param url the url to share
+ * @param imagePreviewUrl the image of the subject
+ * @param showPreviewText show the subject as an extra title of the Android share sheet if true
+ */
+ public static void shareText(final Context context,
+ final String subject,
+ final String url,
+ final String imagePreviewUrl,
final boolean showPreviewText) {
final Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
- if (!subject.isEmpty() && showPreviewText) {
- shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
+ if (!imagePreviewUrl.isEmpty() && !subject.isEmpty() && showPreviewText) {
shareIntent.putExtra(Intent.EXTRA_TITLE, subject);
+ /* TODO: add the image of the content to Android share sheet with setClipData after
+ generating a content URI of this image, then use ClipData.newUri(the content
+ resolver, null, the content URI) and set the ClipData to the share intent with
+ shareIntent.setClipData(generated ClipData).*/
+ //shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
shareIntent.putExtra(Intent.EXTRA_TEXT, url);
diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java
index 610f9f852..a8ee1ebaa 100644
--- a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java
+++ b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java
@@ -88,7 +88,8 @@ public enum StreamDialogEntry {
}),
share(R.string.share, (fragment, item) ->
- ShareUtils.shareText(fragment.getContext(), item.getName(), item.getUrl())),
+ ShareUtils.shareText(fragment.getContext(), item.getName(), item.getUrl(),
+ item.getThumbnailUrl())),
open_in_browser(R.string.open_in_browser, (fragment, item) ->
ShareUtils.openUrlInBrowser(fragment.getContext(), item.getUrl()));
diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
index b31933dfd..44c20b541 100644
--- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
+++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
@@ -376,8 +376,11 @@ public class MissionAdapter extends Adapter
+ * This method finds all timestamps in the {@link SpannableStringBuilder} of the description
+ * using a regular expression, adds for each a {@link ClickableSpan} which opens
+ * {@link NavigationHelper#openSearch(Context, int, String)} and makes a search on the hashtag,
+ * in the service of the content.
+ *
+ * @param context the context to use
+ * @param spannableDescription the SpannableStringBuilder with the text of the
+ * content description
+ * @param streamingService the {@link StreamingService} of the content
+ */
+ private static void addClickListenersOnHashtags(final Context context,
+ final SpannableStringBuilder
+ spannableDescription,
+ final StreamingService streamingService) {
+ final String descriptionText = spannableDescription.toString();
+ final Matcher hashtagsMatches = HASHTAGS_PATTERN.matcher(descriptionText);
+
+ while (hashtagsMatches.find()) {
+ final int hashtagStart = hashtagsMatches.start(1);
+ final int hashtagEnd = hashtagsMatches.end(1);
+ final String parsedHashtag = descriptionText.substring(hashtagStart, hashtagEnd);
+
+ spannableDescription.setSpan(new ClickableSpan() {
+ @Override
+ public void onClick(@NonNull final View view) {
+ NavigationHelper.openSearch(context, streamingService.getServiceId(),
+ parsedHashtag);
+ }
+ }, hashtagStart, hashtagEnd, 0);
+ }
+ }
+
/**
* Add click listeners which opens the popup player on timestamps in a plain text.
*
@@ -137,11 +173,11 @@ public final class TextLinkifier {
final String contentUrl,
final StreamingService streamingService) {
final String descriptionText = spannableDescription.toString();
- final Matcher timestampMatches = TIMESTAMPS_PATTERN_IN_PLAIN_TEXT.matcher(descriptionText);
+ final Matcher timestampsMatches = TIMESTAMPS_PATTERN.matcher(descriptionText);
- while (timestampMatches.find()) {
- final int timestampStart = timestampMatches.start(2);
- final int timestampEnd = timestampMatches.end(3);
+ while (timestampsMatches.find()) {
+ final int timestampStart = timestampsMatches.start(2);
+ final int timestampEnd = timestampsMatches.end(3);
final String parsedTimestamp = descriptionText.substring(timestampStart, timestampEnd);
final String[] timestampParts = parsedTimestamp.split(":");
final int time;
@@ -178,7 +214,8 @@ public final class TextLinkifier {
* This method will also add click listeners on timestamps in this description, which will play
* the content in the popup player at the time indicated in the timestamp, by using
* {@link TextLinkifier#addClickListenersOnTimestamps(Context, SpannableStringBuilder, String,
- * StreamingService)} method.
+ * StreamingService)} method and click listeners on hashtags, which will open a search
+ * on the current service with the hashtag.
*
* This method is required in order to intercept links and e.g. show a confirmation dialog
* before opening a web link.
@@ -220,6 +257,7 @@ public final class TextLinkifier {
if (contentUrl != null || streamingService != null) {
addClickListenersOnTimestamps(context, textBlockLinked, contentUrl,
streamingService);
+ addClickListenersOnHashtags(context, textBlockLinked, streamingService);
}
return textBlockLinked;
From 2702700d10222c272e05832f2dc8da7b6e109c73 Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Fri, 2 Apr 2021 18:48:58 +0200
Subject: [PATCH 11/20] Don't use a chooser for other intents than opening a
content in a browser or sharing a content to other apps Use an ACTION_CHOOSER
intent has a negative impact for user experience, because user cannot set as
default an activity for an intent
---
.../schabi/newpipe/CheckForNewAppVersion.java | 6 +---
.../schabi/newpipe/about/LicenseFragment.kt | 1 -
.../external_communication/ShareUtils.java | 35 ++++++-------------
3 files changed, 12 insertions(+), 30 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
index 7bddb1e95..37ca0e400 100644
--- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
+++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
@@ -129,11 +129,7 @@ public final class CheckForNewAppVersion {
if (BuildConfig.VERSION_CODE < versionCode) {
// A pending intent to open the apk location url in the browser.
- final Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
- viewIntent.putExtra(Intent.EXTRA_TITLE, R.string.open_with);
-
- final Intent intent = new Intent(Intent.ACTION_CHOOSER);
- intent.putExtra(Intent.EXTRA_INTENT, viewIntent);
+ final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final PendingIntent pendingIntent
= PendingIntent.getActivity(application, 0, intent, 0);
diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
index 249955968..ba0c04eb0 100644
--- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
@@ -19,7 +19,6 @@ import java.util.Objects
*/
class LicenseFragment : Fragment() {
private lateinit var softwareComponents: Array
- * If no app is set as default, fallbacks to
- * {@link #openAppChooser(Context, Intent, boolean)}.
- *
+ * If no app can open the intent, a toast with the message {@code No app on your device can
+ * open this} is shown.
*
* @param context the context to use
* @param intent the intent to open
@@ -132,27 +131,15 @@ public final class ShareUtils {
final boolean showToast) {
final String defaultPackageName = getDefaultAppPackageName(context, intent);
- if (defaultPackageName.equals("android")) {
- // No app set as default (doesn't work on some devices)
- openAppChooser(context, intent, true);
- } else {
- if (defaultPackageName.isEmpty()) {
- // No app installed to open the intent
- if (showToast) {
- Toast.makeText(context, R.string.no_app_to_open_intent, Toast.LENGTH_LONG)
- .show();
- }
- return false;
- } else {
- try {
- intent.setPackage(defaultPackageName);
- context.startActivity(intent);
- } catch (final ActivityNotFoundException e) {
- // Not an app to open the intent but an app chooser because of OEMs changes
- intent.setPackage(null);
- openAppChooser(context, intent, true);
- }
+ if (defaultPackageName.isEmpty()) {
+ // No app installed to open the intent
+ if (showToast) {
+ Toast.makeText(context, R.string.no_app_to_open_intent, Toast.LENGTH_LONG)
+ .show();
}
+ return false;
+ } else {
+ context.startActivity(intent);
}
return true;
@@ -256,6 +243,7 @@ public final class ShareUtils {
final String imagePreviewUrl) {
final Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
+ shareIntent.putExtra(Intent.EXTRA_TEXT, content);
if (!title.isEmpty()) {
shareIntent.putExtra(Intent.EXTRA_TITLE, title);
}
@@ -267,7 +255,6 @@ public final class ShareUtils {
if (!imagePreviewUrl.isEmpty()) {
//shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}*/
- shareIntent.putExtra(Intent.EXTRA_TEXT, content);
openAppChooser(context, shareIntent, false);
}
From a79badd783c72f06afca7eaae6d5b0297beab4ed Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Sat, 3 Apr 2021 14:46:30 +0200
Subject: [PATCH 12/20] Adress requested changes and try some cleanup in
handleUrl method of InternalUrlsHandler class
---
.../util/CommentTextOnTouchListener.java | 11 ++---
.../InternalUrlsHandler.java | 9 ++--
.../external_communication/TextLinkifier.java | 46 ++++++++++---------
.../giga/ui/adapter/MissionAdapter.java | 2 +
4 files changed, 34 insertions(+), 34 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java
index 8fe59b036..d0edc4bc4 100644
--- a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java
+++ b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java
@@ -48,14 +48,11 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
- boolean handled = false;
if (link[0] instanceof URLSpan) {
- handled = InternalUrlsHandler.handleUrl(v.getContext(),
- ((URLSpan) link[0]).getURL(), 1);
- }
- if (!handled) {
- ShareUtils.openUrlInBrowser(v.getContext(),
- ((URLSpan) link[0]).getURL(), false);
+ final String url = ((URLSpan) link[0]).getURL();
+ if (!InternalUrlsHandler.handleUrl(v.getContext(), url, 1)) {
+ ShareUtils.openUrlInBrowser(v.getContext(), url, false);
+ }
}
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java
index 7b81538a3..a234f6203 100644
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java
+++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java
@@ -49,22 +49,20 @@ public final class InternalUrlsHandler {
final int timestampType) {
String matchedUrl = "";
int seconds = -1;
- final Pattern timestampPattern;
+ final Matcher matcher;
if (timestampType == 0) {
- timestampPattern = AMPERSAND_TIMESTAMP_PATTERN;
+ matcher = AMPERSAND_TIMESTAMP_PATTERN.matcher(url);
} else if (timestampType == 1) {
- timestampPattern = HASHTAG_TIMESTAMP_PATTERN;
+ matcher = HASHTAG_TIMESTAMP_PATTERN.matcher(url);
} else {
return false;
}
- final Matcher matcher = timestampPattern.matcher(url);
if (matcher.matches()) {
matchedUrl = matcher.group(1);
seconds = Integer.parseInt(matcher.group(2));
}
-
if (matchedUrl == null || matchedUrl.isEmpty()) {
return false;
}
@@ -78,7 +76,6 @@ public final class InternalUrlsHandler {
} catch (final ExtractionException e) {
return false;
}
-
if (linkType == StreamingService.LinkType.NONE) {
return false;
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
index 1859d42d8..d63b8c5bb 100644
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
+++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
@@ -144,13 +144,18 @@ public final class TextLinkifier {
final int hashtagEnd = hashtagsMatches.end(1);
final String parsedHashtag = descriptionText.substring(hashtagStart, hashtagEnd);
- spannableDescription.setSpan(new ClickableSpan() {
- @Override
- public void onClick(@NonNull final View view) {
- NavigationHelper.openSearch(context, streamingService.getServiceId(),
- parsedHashtag);
- }
- }, hashtagStart, hashtagEnd, 0);
+ // don't add a ClickableSpan if there is already one, which should be a part of an URL,
+ // already parsed before
+ if (spannableDescription.getSpans(hashtagStart, hashtagEnd,
+ ClickableSpan.class).length == 0) {
+ spannableDescription.setSpan(new ClickableSpan() {
+ @Override
+ public void onClick(@NonNull final View view) {
+ NavigationHelper.openSearch(context, streamingService.getServiceId(),
+ parsedHashtag);
+ }
+ }, hashtagStart, hashtagEnd, 0);
+ }
}
}
@@ -181,26 +186,24 @@ public final class TextLinkifier {
final String parsedTimestamp = descriptionText.substring(timestampStart, timestampEnd);
final String[] timestampParts = parsedTimestamp.split(":");
final int time;
+
if (timestampParts.length == 3) { // timestamp format: XX:XX:XX
time = Integer.parseInt(timestampParts[0]) * 3600 // hours
+ Integer.parseInt(timestampParts[1]) * 60 // minutes
+ Integer.parseInt(timestampParts[2]); // seconds
- spannableDescription.setSpan(new ClickableSpan() {
- @Override
- public void onClick(@NonNull final View view) {
- playOnPopup(context, contentUrl, streamingService, time);
- }
- }, timestampStart, timestampEnd, 0);
} else if (timestampParts.length == 2) { // timestamp format: XX:XX
time = Integer.parseInt(timestampParts[0]) * 60 // minutes
+ Integer.parseInt(timestampParts[1]); // seconds
- spannableDescription.setSpan(new ClickableSpan() {
- @Override
- public void onClick(@NonNull final View view) {
- playOnPopup(context, contentUrl, streamingService, time);
- }
- }, timestampStart, timestampEnd, 0);
+ } else {
+ continue;
}
+
+ spannableDescription.setSpan(new ClickableSpan() {
+ @Override
+ public void onClick(@NonNull final View view) {
+ playOnPopup(context, contentUrl, streamingService, time);
+ }
+ }, timestampStart, timestampEnd, 0);
}
}
@@ -239,10 +242,11 @@ public final class TextLinkifier {
final URLSpan[] urls = textBlockLinked.getSpans(0, chars.length(), URLSpan.class);
for (final URLSpan span : urls) {
+ final String url = span.getURL();
final ClickableSpan clickableSpan = new ClickableSpan() {
public void onClick(@NonNull final View view) {
- if (!InternalUrlsHandler.handleUrl(context, span.getURL(), 0)) {
- ShareUtils.openUrlInBrowser(context, span.getURL(), false);
+ if (!InternalUrlsHandler.handleUrl(context, url, 0)) {
+ ShareUtils.openUrlInBrowser(context, url, false);
}
}
};
diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
index d77afde97..e06485fdf 100644
--- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
+++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
@@ -375,6 +375,8 @@ public class MissionAdapter extends Adapter
+ * This method will check if the provided url is a YouTube comment description URL ({@code
+ * https://www.youtube.com/watch?v=}video_id{@code #timestamp=}time_in_seconds). If yes, the
+ * popup player will be opened when the user will click on the timestamp in the comment,
+ * at the time and for the video indicated in the timestamp.
+ *
+ * @param context the context to use
+ * @param url the URL to check if it can be handled
+ * @return true if the URL can be handled by NewPipe, false if it cannot
+ */
+ public static boolean handleUrlCommentsTimestamp(final Context context, final String url) {
+ return handleUrl(context, url, HASHTAG_TIMESTAMP_PATTERN);
+ }
+
+ /**
+ * Handle a YouTube timestamp description URL in NewPipe.
+ *
+ * This method will check if the provided url is a YouTube timestamp description URL ({@code
+ * https://www.youtube.com/watch?v=}video_id{@code &t=}time_in_seconds). If yes, the popup
+ * player will be opened when the user will click on the timestamp in the video description,
+ * at the time and for the video indicated in the timestamp.
+ *
+ * @param context the context to use
+ * @param url the URL to check if it can be handled
+ * @return true if the URL can be handled by NewPipe, false if it cannot
+ */
+ public static boolean handleUrlDescriptionTimestamp(final Context context, final String url) {
+ return handleUrl(context, url, AMPERSAND_TIMESTAMP_PATTERN);
+ }
+
/**
* Handle an URL in NewPipe.
*
* This method will check if the provided url can be handled in NewPipe or not. If this is a
- * service URL with a timestamp, the popup player will be opened.
- *
- * The timestamp param accepts two integers, corresponding to two timestamps types:
- * 0 for {@code &t=} (used for timestamps in descriptions),
- * 1 for {@code #timestamp=} (used for timestamps in comments).
- * Any other value of this integer will return false.
+ * service URL with a timestamp, the popup player will be opened and true will be returned;
+ * else, false will be returned.
*
- * @param context the context to be used
+ * @param context the context to use
* @param url the URL to check if it can be handled
- * @param timestampType the type of timestamp
+ * @param pattern the pattern
* @return true if the URL can be handled by NewPipe, false if it cannot
*/
- public static boolean handleUrl(final Context context,
- final String url,
- final int timestampType) {
- String matchedUrl = "";
- int seconds = -1;
- final Matcher matcher;
-
- if (timestampType == 0) {
- matcher = AMPERSAND_TIMESTAMP_PATTERN.matcher(url);
- } else if (timestampType == 1) {
- matcher = HASHTAG_TIMESTAMP_PATTERN.matcher(url);
+ private static boolean handleUrl(final Context context,
+ final String url,
+ final Pattern pattern) {
+ final String matchedUrl;
+ final StreamingService service;
+ final StreamingService.LinkType linkType;
+ final int seconds;
+ final Matcher matcher = pattern.matcher(url);
+ if (matcher.matches()) {
+ matchedUrl = matcher.group(1);
+ seconds = Integer.parseInt(matcher.group(2));
} else {
return false;
}
- if (matcher.matches()) {
- matchedUrl = matcher.group(1);
- seconds = Integer.parseInt(matcher.group(2));
- }
- if (matchedUrl == null || matchedUrl.isEmpty()) {
+ if (isNullOrEmpty(matchedUrl)) {
return false;
}
-
- final StreamingService service;
- final StreamingService.LinkType linkType;
-
try {
service = NewPipe.getServiceByUrl(matchedUrl);
linkType = service.getLinkTypeByUrl(matchedUrl);
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
index d63b8c5bb..50a453e35 100644
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
+++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
@@ -245,7 +245,7 @@ public final class TextLinkifier {
final String url = span.getURL();
final ClickableSpan clickableSpan = new ClickableSpan() {
public void onClick(@NonNull final View view) {
- if (!InternalUrlsHandler.handleUrl(context, url, 0)) {
+ if (!InternalUrlsHandler.handleUrlDescriptionTimestamp(context, url)) {
ShareUtils.openUrlInBrowser(context, url, false);
}
}
From da4d379b227534245aa5cd0a4fdc95ca781edf51 Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Sun, 4 Apr 2021 16:37:09 +0200
Subject: [PATCH 14/20] Initial work: use disposables for timestamps parsing in
YouTube video descriptions and YouTube comments
---
.../util/CommentTextOnTouchListener.java | 6 ++-
.../InternalUrlsHandler.java | 51 +++++++++++--------
.../external_communication/TextLinkifier.java | 7 ++-
3 files changed, 40 insertions(+), 24 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java
index dfe5e8ad0..7c87e664b 100644
--- a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java
+++ b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java
@@ -13,6 +13,8 @@ import android.widget.TextView;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.external_communication.InternalUrlsHandler;
+import io.reactivex.rxjava3.disposables.CompositeDisposable;
+
public class CommentTextOnTouchListener implements View.OnTouchListener {
public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener();
@@ -50,8 +52,8 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
if (action == MotionEvent.ACTION_UP) {
if (link[0] instanceof URLSpan) {
final String url = ((URLSpan) link[0]).getURL();
- if (!InternalUrlsHandler.handleUrlCommentsTimestamp(v.getContext(),
- url)) {
+ if (!InternalUrlsHandler.handleUrlCommentsTimestamp(
+ new CompositeDisposable(), v.getContext(), url)) {
ShareUtils.openUrlInBrowser(v.getContext(), url, false);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java
index 086ba0015..58a06089d 100644
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java
+++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java
@@ -18,6 +18,7 @@ import java.util.regex.Pattern;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
+import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
@@ -38,12 +39,15 @@ public final class InternalUrlsHandler {
* popup player will be opened when the user will click on the timestamp in the comment,
* at the time and for the video indicated in the timestamp.
*
- * @param context the context to use
- * @param url the URL to check if it can be handled
+ * @param disposables a field of the Activity/Fragment class that calls this method
+ * @param context the context to use
+ * @param url the URL to check if it can be handled
* @return true if the URL can be handled by NewPipe, false if it cannot
*/
- public static boolean handleUrlCommentsTimestamp(final Context context, final String url) {
- return handleUrl(context, url, HASHTAG_TIMESTAMP_PATTERN);
+ public static boolean handleUrlCommentsTimestamp(final CompositeDisposable disposables,
+ final Context context,
+ final String url) {
+ return handleUrl(disposables, context, url, HASHTAG_TIMESTAMP_PATTERN);
}
/**
@@ -54,12 +58,15 @@ public final class InternalUrlsHandler {
* player will be opened when the user will click on the timestamp in the video description,
* at the time and for the video indicated in the timestamp.
*
- * @param context the context to use
- * @param url the URL to check if it can be handled
+ * @param disposables a field of the Activity/Fragment class that calls this method
+ * @param context the context to use
+ * @param url the URL to check if it can be handled
* @return true if the URL can be handled by NewPipe, false if it cannot
*/
- public static boolean handleUrlDescriptionTimestamp(final Context context, final String url) {
- return handleUrl(context, url, AMPERSAND_TIMESTAMP_PATTERN);
+ public static boolean handleUrlDescriptionTimestamp(final CompositeDisposable disposables,
+ final Context context,
+ final String url) {
+ return handleUrl(disposables, context, url, AMPERSAND_TIMESTAMP_PATTERN);
}
/**
@@ -69,12 +76,14 @@ public final class InternalUrlsHandler {
* service URL with a timestamp, the popup player will be opened and true will be returned;
* else, false will be returned.
*
- * @param context the context to use
- * @param url the URL to check if it can be handled
- * @param pattern the pattern
+ * @param disposables a field of the Activity/Fragment class that calls this method
+ * @param context the context to use
+ * @param url the URL to check if it can be handled
+ * @param pattern the pattern to use
* @return true if the URL can be handled by NewPipe, false if it cannot
*/
- private static boolean handleUrl(final Context context,
+ private static boolean handleUrl(final CompositeDisposable disposables,
+ final Context context,
final String url,
final Pattern pattern) {
final String matchedUrl;
@@ -102,7 +111,7 @@ public final class InternalUrlsHandler {
return false;
}
if (linkType == StreamingService.LinkType.STREAM && seconds != -1) {
- return playOnPopup(context, matchedUrl, service, seconds);
+ return playOnPopup(disposables, context, matchedUrl, service, seconds);
} else {
NavigationHelper.openRouterActivity(context, matchedUrl);
return true;
@@ -112,13 +121,15 @@ public final class InternalUrlsHandler {
/**
* Play a content in the floating player.
*
- * @param context the context to be used
- * @param url the URL of the content
- * @param service the service of the content
- * @param seconds the position in seconds at which the floating player will start
+ * @param disposables a field of the Activity/Fragment class that calls this method
+ * @param context the context to be used
+ * @param url the URL of the content
+ * @param service the service of the content
+ * @param seconds the position in seconds at which the floating player will start
* @return true if the playback of the content has successfully started or false if not
*/
- public static boolean playOnPopup(final Context context,
+ public static boolean playOnPopup(final CompositeDisposable disposables,
+ final Context context,
final String url,
final StreamingService service,
final int seconds) {
@@ -133,13 +144,13 @@ public final class InternalUrlsHandler {
final Single
+ * This calls {@link #shareText(Context, String, String, String)} with an empty string for the
+ * imagePreviewUrl parameter.
+ *
+ * @param context the context to use
+ * @param title the title of the content
+ * @param content the content to share
+ */
+ public static void shareText(final Context context, final String title, final String content) {
+ shareText(context, title, content, "");
+ }
+
/**
* Copy the text to clipboard, and indicate to the user whether the operation was completed
* successfully using a Toast.
From 218f25c171a4e361c072348469baac152b83a6da Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Fri, 21 May 2021 14:52:21 +0200
Subject: [PATCH 16/20] Annotate params and methods with NonNull
---
.../InternalUrlsHandler.java | 22 +++++++++------
.../external_communication/KoreUtils.java | 6 ++--
.../external_communication/ShareUtils.java | 28 +++++++++++--------
.../external_communication/TextLinkifier.java | 12 +++++---
4 files changed, 41 insertions(+), 27 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java
index 58a06089d..69c846dbc 100644
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java
+++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java
@@ -2,6 +2,8 @@ package org.schabi.newpipe.util.external_communication;
import android.content.Context;
+import androidx.annotation.NonNull;
+
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@@ -44,9 +46,10 @@ public final class InternalUrlsHandler {
* @param url the URL to check if it can be handled
* @return true if the URL can be handled by NewPipe, false if it cannot
*/
- public static boolean handleUrlCommentsTimestamp(final CompositeDisposable disposables,
+ public static boolean handleUrlCommentsTimestamp(@NonNull final CompositeDisposable
+ disposables,
final Context context,
- final String url) {
+ @NonNull final String url) {
return handleUrl(disposables, context, url, HASHTAG_TIMESTAMP_PATTERN);
}
@@ -63,9 +66,10 @@ public final class InternalUrlsHandler {
* @param url the URL to check if it can be handled
* @return true if the URL can be handled by NewPipe, false if it cannot
*/
- public static boolean handleUrlDescriptionTimestamp(final CompositeDisposable disposables,
+ public static boolean handleUrlDescriptionTimestamp(@NonNull final CompositeDisposable
+ disposables,
final Context context,
- final String url) {
+ @NonNull final String url) {
return handleUrl(disposables, context, url, AMPERSAND_TIMESTAMP_PATTERN);
}
@@ -82,10 +86,10 @@ public final class InternalUrlsHandler {
* @param pattern the pattern to use
* @return true if the URL can be handled by NewPipe, false if it cannot
*/
- private static boolean handleUrl(final CompositeDisposable disposables,
+ private static boolean handleUrl(@NonNull final CompositeDisposable disposables,
final Context context,
- final String url,
- final Pattern pattern) {
+ @NonNull final String url,
+ @NonNull final Pattern pattern) {
final String matchedUrl;
final StreamingService service;
final StreamingService.LinkType linkType;
@@ -128,10 +132,10 @@ public final class InternalUrlsHandler {
* @param seconds the position in seconds at which the floating player will start
* @return true if the playback of the content has successfully started or false if not
*/
- public static boolean playOnPopup(final CompositeDisposable disposables,
+ public static boolean playOnPopup(@NonNull final CompositeDisposable disposables,
final Context context,
final String url,
- final StreamingService service,
+ @NonNull final StreamingService service,
final int seconds) {
final LinkHandlerFactory factory = service.getStreamLHFactory();
final String cleanUrl;
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/KoreUtils.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/KoreUtils.java
index 5844a0c6c..6801f24ef 100644
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/KoreUtils.java
+++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/KoreUtils.java
@@ -2,6 +2,7 @@ package org.schabi.newpipe.util.external_communication;
import android.content.Context;
+import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.PreferenceManager;
@@ -17,13 +18,14 @@ public final class KoreUtils {
|| serviceId == ServiceList.SoundCloud.getServiceId());
}
- public static boolean shouldShowPlayWithKodi(final Context context, final int serviceId) {
+ public static boolean shouldShowPlayWithKodi(@NonNull final Context context,
+ final int serviceId) {
return isServiceSupportedByKore(serviceId)
&& PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_play_with_kodi_key), false);
}
- public static void showInstallKoreDialog(final Context context) {
+ public static void showInstallKoreDialog(@NonNull final Context context) {
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(R.string.kore_not_found)
.setPositiveButton(R.string.install, (dialog, which) ->
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java
index b9f96746f..e49cd6ea2 100644
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java
+++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java
@@ -11,6 +11,7 @@ import android.net.Uri;
import android.os.Build;
import android.widget.Toast;
+import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R;
@@ -33,7 +34,7 @@ public final class ShareUtils {
* @param context the context to use
* @param packageId the package id of the app to be installed
*/
- public static void installApp(final Context context, final String packageId) {
+ public static void installApp(@NonNull final Context context, final String packageId) {
// Try market scheme
final boolean marketSchemeResult = openIntentInApp(context, new Intent(Intent.ACTION_VIEW,
Uri.parse("market://details?id=" + packageId))
@@ -57,7 +58,7 @@ public final class ShareUtils {
* for HTTP protocol or for the created intent
* @return true if the URL can be opened or false if it cannot
*/
- public static boolean openUrlInBrowser(final Context context,
+ public static boolean openUrlInBrowser(@NonNull final Context context,
final String url,
final boolean httpDefaultBrowserTest) {
final String defaultPackageName;
@@ -107,7 +108,7 @@ public final class ShareUtils {
* @param url the url to browse
* @return true if the URL can be opened or false if it cannot be
**/
- public static boolean openUrlInBrowser(final Context context, final String url) {
+ public static boolean openUrlInBrowser(@NonNull final Context context, final String url) {
return openUrlInBrowser(context, url, true);
}
@@ -126,8 +127,8 @@ public final class ShareUtils {
* to open the intent (true) or not (false)
* @return true if the intent can be opened or false if it cannot be
*/
- public static boolean openIntentInApp(final Context context,
- final Intent intent,
+ public static boolean openIntentInApp(@NonNull final Context context,
+ @NonNull final Intent intent,
final boolean showToast) {
final String defaultPackageName = getDefaultAppPackageName(context, intent);
@@ -159,8 +160,8 @@ public final class ShareUtils {
* @param intent the intent to open
* @param setTitleChooser set the title "Open with" to the chooser if true, else not
*/
- private static void openAppChooser(final Context context,
- final Intent intent,
+ private static void openAppChooser(@NonNull final Context context,
+ @NonNull final Intent intent,
final boolean setTitleChooser) {
final Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
chooserIntent.putExtra(Intent.EXTRA_INTENT, intent);
@@ -214,7 +215,8 @@ public final class ShareUtils {
* @return the package name of the default app, an empty string if there's no app installed to
* handle the intent or the app chooser if there's no default
*/
- private static String getDefaultAppPackageName(final Context context, final Intent intent) {
+ private static String getDefaultAppPackageName(@NonNull final Context context,
+ @NonNull final Intent intent) {
final ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent,
PackageManager.MATCH_DEFAULT_ONLY);
@@ -237,8 +239,8 @@ public final class ShareUtils {
* @param content the content to share
* @param imagePreviewUrl the image of the subject
*/
- public static void shareText(final Context context,
- final String title,
+ public static void shareText(@NonNull final Context context,
+ @NonNull final String title,
final String content,
final String imagePreviewUrl) {
final Intent shareIntent = new Intent(Intent.ACTION_SEND);
@@ -272,7 +274,9 @@ public final class ShareUtils {
* @param title the title of the content
* @param content the content to share
*/
- public static void shareText(final Context context, final String title, final String content) {
+ public static void shareText(@NonNull final Context context,
+ @NonNull final String title,
+ final String content) {
shareText(context, title, content, "");
}
@@ -283,7 +287,7 @@ public final class ShareUtils {
* @param context the context to use
* @param text the text to copy
*/
- public static void copyToClipboard(final Context context, final String text) {
+ public static void copyToClipboard(@NonNull final Context context, final String text) {
final ClipboardManager clipboardManager =
ContextCompat.getSystemService(context, ClipboardManager.class);
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
index 6c5cfb499..d9d9875ab 100644
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
+++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
@@ -55,6 +55,7 @@ public final class TextLinkifier {
* will be called
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
*/
+ @NonNull
public static Disposable createLinksFromHtmlBlock(final Context context,
final String htmlBlock,
final TextView textView,
@@ -82,9 +83,10 @@ public final class TextLinkifier {
* @param contentUrl the URL of the content
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
*/
+ @NonNull
public static Disposable createLinksFromPlainText(final Context context,
final String plainTextBlock,
- final TextView textView,
+ @NonNull final TextView textView,
final StreamingService streamingService,
final String contentUrl) {
textView.setAutoLinkMask(Linkify.WEB_URLS);
@@ -109,6 +111,7 @@ public final class TextLinkifier {
* @param contentUrl the URL of the content
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
*/
+ @NonNull
public static Disposable createLinksFromMarkdownText(final Context context,
final String markdownBlock,
final TextView textView,
@@ -134,7 +137,7 @@ public final class TextLinkifier {
* @param streamingService the {@link StreamingService} of the content
*/
private static void addClickListenersOnHashtags(final Context context,
- final SpannableStringBuilder
+ @NonNull final SpannableStringBuilder
spannableDescription,
final StreamingService streamingService) {
final String descriptionText = spannableDescription.toString();
@@ -174,7 +177,7 @@ public final class TextLinkifier {
* @param streamingService the {@link StreamingService} of the content
*/
private static void addClickListenersOnTimestamps(final Context context,
- final SpannableStringBuilder
+ @NonNull final SpannableStringBuilder
spannableDescription,
final String contentUrl,
final StreamingService streamingService) {
@@ -232,6 +235,7 @@ public final class TextLinkifier {
* @param contentUrl the URL of the content
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
*/
+ @NonNull
private static Disposable changeIntentsOfDescriptionLinks(final Context context,
final CharSequence chars,
final TextView textView,
@@ -279,7 +283,7 @@ public final class TextLinkifier {
});
}
- private static void setTextViewCharSequence(final TextView textView,
+ private static void setTextViewCharSequence(@NonNull final TextView textView,
final CharSequence charSequence) {
textView.setText(charSequence);
textView.setMovementMethod(LinkMovementMethod.getInstance());
From eef418a757795abae2255fcba236927b5784677a Mon Sep 17 00:00:00 2001
From: Stypox
* This will call
- * {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView,
- * StreamingService, String)}
+ * {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence, Info)}
* after having linked the URLs with {@link HtmlCompat#fromHtml(String, int)}.
*
- * @param context the context to use
- * @param htmlBlock the htmlBlock to be linked
- * @param textView the TextView to set the htmlBlock linked
- * @param streamingService the {@link StreamingService} of the content
- * @param contentUrl the URL of the content
- * @param htmlCompatFlag the int flag to be set when {@link HtmlCompat#fromHtml(String, int)}
- * will be called
+ * @param textView the TextView to set the htmlBlock linked
+ * @param htmlBlock the htmlBlock to be linked
+ * @param htmlCompatFlag the int flag to be set when {@link HtmlCompat#fromHtml(String, int)}
+ * will be called
+ * @param relatedInfo if given, handle timestamps to open the stream in the popup player at
+ * the specific time, and hashtags to search for the term in the correct
+ * service
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
*/
@NonNull
- public static Disposable createLinksFromHtmlBlock(final Context context,
+ public static Disposable createLinksFromHtmlBlock(@NonNull final TextView textView,
final String htmlBlock,
- final TextView textView,
- final StreamingService streamingService,
- final String contentUrl,
- final int htmlCompatFlag) {
- return changeIntentsOfDescriptionLinks(context,
- HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), textView, streamingService,
- contentUrl);
+ final int htmlCompatFlag,
+ @Nullable final Info relatedInfo) {
+ return changeIntentsOfDescriptionLinks(
+ textView, HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), relatedInfo);
}
/**
* Create web links for contents with a plain text description.
*
* This will call
- * {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView,
- * StreamingService, String)}
+ * {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence, Info)}
* after having linked the URLs with {@link TextView#setAutoLinkMask(int)} and
* {@link TextView#setText(CharSequence, TextView.BufferType)}.
*
- * @param context the context to use
- * @param plainTextBlock the block of plain text to be linked
- * @param textView the TextView to set the plain text block linked
- * @param streamingService the {@link StreamingService} of the content
- * @param contentUrl the URL of the content
+ * @param textView the TextView to set the plain text block linked
+ * @param plainTextBlock the block of plain text to be linked
+ * @param relatedInfo if given, handle timestamps to open the stream in the popup player at
+ * the specific time, and hashtags to search for the term in the correct
+ * service
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
*/
@NonNull
- public static Disposable createLinksFromPlainText(final Context context,
+ public static Disposable createLinksFromPlainText(@NonNull final TextView textView,
final String plainTextBlock,
- @NonNull final TextView textView,
- final StreamingService streamingService,
- final String contentUrl) {
+ @Nullable final Info relatedInfo) {
textView.setAutoLinkMask(Linkify.WEB_URLS);
textView.setText(plainTextBlock, TextView.BufferType.SPANNABLE);
- return changeIntentsOfDescriptionLinks(context, textView.getText(), textView,
- streamingService, contentUrl);
+ return changeIntentsOfDescriptionLinks(textView, textView.getText(), relatedInfo);
}
/**
* Create web links for contents with a markdown description.
*
* This will call
- * {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView,
- * StreamingService, String)}
+ * {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence, Info)}
* after creating an {@link Markwon} object and using
* {@link Markwon#setMarkdown(TextView, String)}.
*
- * @param context the context to use
- * @param markdownBlock the block of markdown text to be linked
- * @param textView the TextView to set the plain text block linked
- * @param streamingService the {@link StreamingService} of the content
- * @param contentUrl the URL of the content
+ * @param textView the TextView to set the plain text block linked
+ * @param markdownBlock the block of markdown text to be linked
+ * @param relatedInfo if given, handle timestamps to open the stream in the popup player at
+ * the specific time, and hashtags to search for the term in the correct
+ * service
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
*/
@NonNull
- public static Disposable createLinksFromMarkdownText(final Context context,
+ public static Disposable createLinksFromMarkdownText(@NonNull final TextView textView,
final String markdownBlock,
- final TextView textView,
- final StreamingService streamingService,
- final String contentUrl) {
- final Markwon markwon = Markwon.builder(context).usePlugin(LinkifyPlugin.create()).build();
+ @Nullable final Info relatedInfo) {
+ final Markwon markwon = Markwon.builder(textView.getContext())
+ .usePlugin(LinkifyPlugin.create()).build();
markwon.setMarkdown(textView, markdownBlock);
- return changeIntentsOfDescriptionLinks(context, textView.getText(), textView,
- streamingService, contentUrl);
+ return changeIntentsOfDescriptionLinks(textView, textView.getText(), relatedInfo);
}
/**
@@ -134,12 +124,12 @@ public final class TextLinkifier {
* @param context the context to use
* @param spannableDescription the SpannableStringBuilder with the text of the
* content description
- * @param streamingService the {@link StreamingService} of the content
+ * @param relatedInfo used to search for the term in the correct service
*/
private static void addClickListenersOnHashtags(final Context context,
@NonNull final SpannableStringBuilder
spannableDescription,
- final StreamingService streamingService) {
+ final Info relatedInfo) {
final String descriptionText = spannableDescription.toString();
final Matcher hashtagsMatches = HASHTAGS_PATTERN.matcher(descriptionText);
@@ -155,7 +145,7 @@ public final class TextLinkifier {
spannableDescription.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull final View view) {
- NavigationHelper.openSearch(context, streamingService.getServiceId(),
+ NavigationHelper.openSearch(context, relatedInfo.getServiceId(),
parsedHashtag);
}
}, hashtagStart, hashtagEnd, 0);
@@ -173,14 +163,12 @@ public final class TextLinkifier {
* @param context the context to use
* @param spannableDescription the SpannableStringBuilder with the text of the
* content description
- * @param contentUrl the URL of the content
- * @param streamingService the {@link StreamingService} of the content
+ * @param relatedInfo what to open in the popup player when timestamps are clicked
*/
private static void addClickListenersOnTimestamps(final Context context,
@NonNull final SpannableStringBuilder
spannableDescription,
- final String contentUrl,
- final StreamingService streamingService) {
+ final Info relatedInfo) {
final String descriptionText = spannableDescription.toString();
final Matcher timestampsMatches = TIMESTAMPS_PATTERN.matcher(descriptionText);
@@ -189,14 +177,14 @@ public final class TextLinkifier {
final int timestampEnd = timestampsMatches.end(3);
final String parsedTimestamp = descriptionText.substring(timestampStart, timestampEnd);
final String[] timestampParts = parsedTimestamp.split(":");
- final int time;
+ final int seconds;
if (timestampParts.length == 3) { // timestamp format: XX:XX:XX
- time = Integer.parseInt(timestampParts[0]) * 3600 // hours
+ seconds = Integer.parseInt(timestampParts[0]) * 3600 // hours
+ Integer.parseInt(timestampParts[1]) * 60 // minutes
+ Integer.parseInt(timestampParts[2]); // seconds
} else if (timestampParts.length == 2) { // timestamp format: XX:XX
- time = Integer.parseInt(timestampParts[0]) * 60 // minutes
+ seconds = Integer.parseInt(timestampParts[0]) * 60 // minutes
+ Integer.parseInt(timestampParts[1]); // seconds
} else {
continue;
@@ -205,8 +193,8 @@ public final class TextLinkifier {
spannableDescription.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull final View view) {
- playOnPopup(new CompositeDisposable(), context, contentUrl, streamingService,
- time);
+ playOnPopup(new CompositeDisposable(), context, relatedInfo.getUrl(),
+ relatedInfo.getService(), seconds);
}
}, timestampStart, timestampEnd, 0);
}
@@ -221,28 +209,28 @@ public final class TextLinkifier {
* with {@link ShareUtils#openUrlInBrowser(Context, String, boolean)}.
* This method will also add click listeners on timestamps in this description, which will play
* the content in the popup player at the time indicated in the timestamp, by using
- * {@link TextLinkifier#addClickListenersOnTimestamps(Context, SpannableStringBuilder, String,
- * StreamingService)} method and click listeners on hashtags, which will open a search
- * on the current service with the hashtag.
+ * {@link TextLinkifier#addClickListenersOnTimestamps(Context, SpannableStringBuilder, Info)}
+ * method and click listeners on hashtags, by using
+ * {@link TextLinkifier#addClickListenersOnHashtags(Context, SpannableStringBuilder, Info)},
+ * which will open a search on the current service with the hashtag.
*
* This method is required in order to intercept links and e.g. show a confirmation dialog
* before opening a web link.
*
- * @param context the context to use
- * @param chars the CharSequence to be parsed
- * @param textView the TextView in which the converted CharSequence will be applied
- * @param streamingService the {@link StreamingService} of the content
- * @param contentUrl the URL of the content
+ * @param textView the TextView in which the converted CharSequence will be applied
+ * @param chars the CharSequence to be parsed
+ * @param relatedInfo if given, handle timestamps to open the stream in the popup player at
+ * the specific time, and hashtags to search for the term in the correct
+ * service
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
*/
@NonNull
- private static Disposable changeIntentsOfDescriptionLinks(final Context context,
+ private static Disposable changeIntentsOfDescriptionLinks(final TextView textView,
final CharSequence chars,
- final TextView textView,
- final StreamingService
- streamingService,
- final String contentUrl) {
+ @Nullable final Info relatedInfo) {
return Single.fromCallable(() -> {
+ final Context context = textView.getContext();
+
// add custom click actions on web links
final SpannableStringBuilder textBlockLinked = new SpannableStringBuilder(chars);
final URLSpan[] urls = textBlockLinked.getSpans(0, chars.length(), URLSpan.class);
@@ -264,11 +252,10 @@ public final class TextLinkifier {
}
// add click actions on plain text timestamps only for description of contents,
- // unneeded for metainfo TextViews
- if (contentUrl != null || streamingService != null) {
- addClickListenersOnTimestamps(context, textBlockLinked, contentUrl,
- streamingService);
- addClickListenersOnHashtags(context, textBlockLinked, streamingService);
+ // unneeded for meta-info or other TextViews
+ if (relatedInfo != null) {
+ addClickListenersOnTimestamps(context, textBlockLinked, relatedInfo);
+ addClickListenersOnHashtags(context, textBlockLinked, relatedInfo);
}
return textBlockLinked;
From edfe0f9c3060c30e208f96d6c043ffc26feb2e2d Mon Sep 17 00:00:00 2001
From: Stypox
- * This will call
- * {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence, Info)}
- * after having linked the URLs with {@link HtmlCompat#fromHtml(String, int)}.
+ * This will call {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence,
+ * Info, CompositeDisposable)} after having linked the URLs with
+ * {@link HtmlCompat#fromHtml(String, int)}.
*
* @param textView the TextView to set the htmlBlock linked
* @param htmlBlock the htmlBlock to be linked
@@ -53,23 +52,24 @@ public final class TextLinkifier {
* @param relatedInfo if given, handle timestamps to open the stream in the popup player at
* the specific time, and hashtags to search for the term in the correct
* service
- * @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
+ * @param disposables disposables created by the method are added here and their lifecycle
+ * should be handled by the calling class
*/
- @NonNull
- public static Disposable createLinksFromHtmlBlock(@NonNull final TextView textView,
- final String htmlBlock,
- final int htmlCompatFlag,
- @Nullable final Info relatedInfo) {
- return changeIntentsOfDescriptionLinks(
- textView, HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), relatedInfo);
+ public static void createLinksFromHtmlBlock(@NonNull final TextView textView,
+ final String htmlBlock,
+ final int htmlCompatFlag,
+ @Nullable final Info relatedInfo,
+ final CompositeDisposable disposables) {
+ changeIntentsOfDescriptionLinks(
+ textView, HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), relatedInfo, disposables);
}
/**
* Create web links for contents with a plain text description.
*
- * This will call
- * {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence, Info)}
- * after having linked the URLs with {@link TextView#setAutoLinkMask(int)} and
+ * This will call {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence,
+ * Info, CompositeDisposable)} after having linked the URLs with
+ * {@link TextView#setAutoLinkMask(int)} and
* {@link TextView#setText(CharSequence, TextView.BufferType)}.
*
* @param textView the TextView to set the plain text block linked
@@ -77,40 +77,40 @@ public final class TextLinkifier {
* @param relatedInfo if given, handle timestamps to open the stream in the popup player at
* the specific time, and hashtags to search for the term in the correct
* service
- * @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
+ * @param disposables disposables created by the method are added here and their lifecycle
+ * should be handled by the calling class
*/
- @NonNull
- public static Disposable createLinksFromPlainText(@NonNull final TextView textView,
- final String plainTextBlock,
- @Nullable final Info relatedInfo) {
+ public static void createLinksFromPlainText(@NonNull final TextView textView,
+ final String plainTextBlock,
+ @Nullable final Info relatedInfo,
+ final CompositeDisposable disposables) {
textView.setAutoLinkMask(Linkify.WEB_URLS);
textView.setText(plainTextBlock, TextView.BufferType.SPANNABLE);
- return changeIntentsOfDescriptionLinks(textView, textView.getText(), relatedInfo);
+ changeIntentsOfDescriptionLinks(textView, textView.getText(), relatedInfo, disposables);
}
/**
* Create web links for contents with a markdown description.
*
- * This will call
- * {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence, Info)}
- * after creating an {@link Markwon} object and using
+ * This will call {@link TextLinkifier#changeIntentsOfDescriptionLinks(TextView, CharSequence,
+ * Info, CompositeDisposable)} after creating an {@link Markwon} object and using
* {@link Markwon#setMarkdown(TextView, String)}.
*
* @param textView the TextView to set the plain text block linked
* @param markdownBlock the block of markdown text to be linked
* @param relatedInfo if given, handle timestamps to open the stream in the popup player at
* the specific time, and hashtags to search for the term in the correct
- * service
- * @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
+ * @param disposables disposables created by the method are added here and their lifecycle
+ * should be handled by the calling class
*/
- @NonNull
- public static Disposable createLinksFromMarkdownText(@NonNull final TextView textView,
- final String markdownBlock,
- @Nullable final Info relatedInfo) {
+ public static void createLinksFromMarkdownText(@NonNull final TextView textView,
+ final String markdownBlock,
+ @Nullable final Info relatedInfo,
+ final CompositeDisposable disposables) {
final Markwon markwon = Markwon.builder(textView.getContext())
.usePlugin(LinkifyPlugin.create()).build();
- markwon.setMarkdown(textView, markdownBlock);
- return changeIntentsOfDescriptionLinks(textView, textView.getText(), relatedInfo);
+ changeIntentsOfDescriptionLinks(textView, markwon.toMarkdown(markdownBlock), relatedInfo,
+ disposables);
}
/**
@@ -164,11 +164,14 @@ public final class TextLinkifier {
* @param spannableDescription the SpannableStringBuilder with the text of the
* content description
* @param relatedInfo what to open in the popup player when timestamps are clicked
+ * @param disposables disposables created by the method are added here and their
+ * lifecycle should be handled by the calling class
*/
private static void addClickListenersOnTimestamps(final Context context,
@NonNull final SpannableStringBuilder
spannableDescription,
- final Info relatedInfo) {
+ final Info relatedInfo,
+ final CompositeDisposable disposables) {
final String descriptionText = spannableDescription.toString();
final Matcher timestampsMatches = TIMESTAMPS_PATTERN.matcher(descriptionText);
@@ -193,8 +196,8 @@ public final class TextLinkifier {
spannableDescription.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull final View view) {
- playOnPopup(new CompositeDisposable(), context, relatedInfo.getUrl(),
- relatedInfo.getService(), seconds);
+ playOnPopup(context, relatedInfo.getUrl(), relatedInfo.getService(), seconds,
+ disposables);
}
}, timestampStart, timestampEnd, 0);
}
@@ -209,8 +212,8 @@ public final class TextLinkifier {
* with {@link ShareUtils#openUrlInBrowser(Context, String, boolean)}.
* This method will also add click listeners on timestamps in this description, which will play
* the content in the popup player at the time indicated in the timestamp, by using
- * {@link TextLinkifier#addClickListenersOnTimestamps(Context, SpannableStringBuilder, Info)}
- * method and click listeners on hashtags, by using
+ * {@link TextLinkifier#addClickListenersOnTimestamps(Context, SpannableStringBuilder, Info,
+ * CompositeDisposable)} method and click listeners on hashtags, by using
* {@link TextLinkifier#addClickListenersOnHashtags(Context, SpannableStringBuilder, Info)},
* which will open a search on the current service with the hashtag.
*
@@ -222,13 +225,14 @@ public final class TextLinkifier {
* @param relatedInfo if given, handle timestamps to open the stream in the popup player at
* the specific time, and hashtags to search for the term in the correct
* service
- * @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
+ * @param disposables disposables created by the method are added here and their lifecycle
+ * should be handled by the calling class
*/
- @NonNull
- private static Disposable changeIntentsOfDescriptionLinks(final TextView textView,
- final CharSequence chars,
- @Nullable final Info relatedInfo) {
- return Single.fromCallable(() -> {
+ private static void changeIntentsOfDescriptionLinks(final TextView textView,
+ final CharSequence chars,
+ @Nullable final Info relatedInfo,
+ final CompositeDisposable disposables) {
+ disposables.add(Single.fromCallable(() -> {
final Context context = textView.getContext();
// add custom click actions on web links
@@ -254,7 +258,7 @@ public final class TextLinkifier {
// add click actions on plain text timestamps only for description of contents,
// unneeded for meta-info or other TextViews
if (relatedInfo != null) {
- addClickListenersOnTimestamps(context, textBlockLinked, relatedInfo);
+ addClickListenersOnTimestamps(context, textBlockLinked, relatedInfo, disposables);
addClickListenersOnHashtags(context, textBlockLinked, relatedInfo);
}
@@ -267,7 +271,7 @@ public final class TextLinkifier {
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(@NonNull final TextView textView,
From 32b0bdb98c2a6ae7902e2c6c5d3c891a8b712ad9 Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Fri, 11 Jun 2021 12:19:29 +0200
Subject: [PATCH 19/20] Fix the compile error
---
.../main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt
index f24116e23..7617ef451 100644
--- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt
+++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt
@@ -10,8 +10,8 @@ import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.R
import org.schabi.newpipe.util.Localization
-import org.schabi.newpipe.util.ShareUtils
import org.schabi.newpipe.util.ThemeHelper
+import org.schabi.newpipe.util.external_communication.ShareUtils
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
From 4eef498d241bf395415dac6b1b49114101de94ad Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Fri, 11 Jun 2021 14:08:08 +0200
Subject: [PATCH 20/20] Only call addClickListenersOnTimestamps if relatedInfo
is instance of StreamInfo
---
.../newpipe/util/external_communication/TextLinkifier.java | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
index f4b423e41..76da09609 100644
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
+++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
@@ -15,6 +15,7 @@ import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import org.schabi.newpipe.extractor.Info;
+import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.regex.Matcher;
@@ -258,7 +259,10 @@ public final class TextLinkifier {
// add click actions on plain text timestamps only for description of contents,
// unneeded for meta-info or other TextViews
if (relatedInfo != null) {
- addClickListenersOnTimestamps(context, textBlockLinked, relatedInfo, disposables);
+ if (relatedInfo instanceof StreamInfo) {
+ addClickListenersOnTimestamps(context, textBlockLinked, relatedInfo,
+ disposables);
+ }
addClickListenersOnHashtags(context, textBlockLinked, relatedInfo);
}
Date: Fri, 26 Mar 2021 13:28:11 +0100
Subject: [PATCH 09/20] Move some classes to a new subpackage and adress
requested changes Rename URLHandler and KoreUtil classes to
InternalUrlsHandler and KoreUtils. Move InternalUrlsHandler, KoreUtils,
TextLinkfier, ShareUtils classes to external_communication subpackage. Remove
unused param showPreviewText in shareText method of ShareUtils class. Add
initial work to be able to display an image preview of the content shared
(not for downloads). Use a better regular expression to parse timestamps in
plain text descriptions.
---
app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
index 2f015a049..d40bb209e 100644
--- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
+++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
@@ -16,8 +16,8 @@ import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ActivityAboutBinding
import org.schabi.newpipe.databinding.FragmentAboutBinding
+import org.schabi.newpipe.util.external_communication.ShareUtils
import org.schabi.newpipe.util.Localization
-import org.schabi.newpipe.util.ShareUtils
import org.schabi.newpipe.util.ThemeHelper
class AboutActivity : AppCompatActivity() {
From 267686fd3772c69cf06f417cbd81165ae5741040 Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Sat, 27 Mar 2021 18:45:05 +0100
Subject: [PATCH 10/20] Initial work: add support for opening hashtags in plain
text descriptions This commit adds supports for opening hashtags in plain
text descriptions, using the same logic as timestamps. Every hashtag opens a
search on the current service with the text in the hashtag. Also use a better
regular expression for parsing timestamps.
---
.../external_communication/TextLinkifier.java | 54 ++++++++++++++++---
1 file changed, 46 insertions(+), 8 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
index 2b205a586..1859d42d8 100644
--- a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
+++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java
@@ -14,6 +14,7 @@ import androidx.annotation.NonNull;
import androidx.core.text.HtmlCompat;
import org.schabi.newpipe.extractor.StreamingService;
+import org.schabi.newpipe.util.NavigationHelper;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -29,9 +30,9 @@ import static org.schabi.newpipe.util.external_communication.InternalUrlsHandler
public final class TextLinkifier {
public static final String TAG = TextLinkifier.class.getSimpleName();
- private static final Pattern TIMESTAMPS_PATTERN_IN_PLAIN_TEXT =
- Pattern.compile("(?:^|(?![:])\\W)(?:([0-5]?[0-9]):)?([0-5]?[0-9]):"
- + "([0-5][0-9])(?=$|(?![:])\\W)");
+ private static final Pattern HASHTAGS_PATTERN = Pattern.compile("(#[A-Za-z0-9_]+)");
+ private static final Pattern TIMESTAMPS_PATTERN = Pattern.compile(
+ "(?:^|(?!:)\\W)(?:([0-5]?[0-9]):)?([0-5]?[0-9]):([0-5][0-9])(?=$|(?!:)\\W)");
private TextLinkifier() {
}
@@ -118,6 +119,41 @@ public final class TextLinkifier {
streamingService, contentUrl);
}
+ /**
+ * Add click listeners which opens a search on hashtags in a plain text.
+ *