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 implements Handler.Callb if (BuildConfig.DEBUG) Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); - final Uri uri = resolveShareableUri(mission); - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(uri, mimeType); + intent.setDataAndType(resolveShareableUri(mission), mimeType); intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @@ -361,10 +359,8 @@ public class MissionAdapter extends Adapter implements Handler.Callb intent.addFlags(FLAG_ACTIVITY_NEW_TASK); } - //mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - if (intent.resolveActivity(mContext.getPackageManager()) != null) { - ShareUtils.openIntentInApp(mContext, intent); + ShareUtils.openIntentInApp(mContext, intent, false); } else { Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG).show(); } @@ -377,19 +373,13 @@ public class MissionAdapter extends Adapter implements Handler.Callb shareIntent.setType(resolveMimeType(mission)); shareIntent.putExtra(Intent.EXTRA_STREAM, resolveShareableUri(mission)); shareIntent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); + final Intent intent = new Intent(Intent.ACTION_CHOOSER); intent.putExtra(Intent.EXTRA_INTENT, shareIntent); intent.putExtra(Intent.EXTRA_TITLE, mContext.getString(R.string.share_dialog_title)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { - intent.setPackage("android"); - mContext.startActivity(intent); - } catch (final ActivityNotFoundException e) { - // falling back to OEM chooser if Android's system chooser was removed by the OEM - intent.setPackage(null); - mContext.startActivity(intent); - } + mContext.startActivity(intent); } /** From 6abdd2a6d8f7f9078c0811ebacc34357a586150b Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sat, 13 Mar 2021 12:43:51 +0100 Subject: [PATCH 04/20] Try to change message of the system chooser for the update notification This commit tries to change the title of the system chooser shown, which is from Android System ("Open links with"), when no defaut browser is present, for the update notification. --- .../main/java/org/schabi/newpipe/CheckForNewAppVersion.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java index f84d986aa..7bddb1e95 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java @@ -130,12 +130,11 @@ 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); - intent.putExtra(Intent.EXTRA_TITLE, R.string.open_with); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final PendingIntent pendingIntent = PendingIntent.getActivity(application, 0, intent, 0); From c9729403380aa675af3333919b5457592f1c7ec2 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Thu, 18 Mar 2021 18:42:25 +0100 Subject: [PATCH 05/20] Apply suggested changes and fix some warnings --- .../util/CommentTextOnTouchListener.java | 2 +- .../schabi/newpipe/util/TextLinkifier.java | 32 +++++++++---------- .../org/schabi/newpipe/util/URLHandler.java | 19 ++++++----- 3 files changed, 28 insertions(+), 25 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 51d8539f4..f48d39da0 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.canHandleUrl(v.getContext(), + handled = URLHandler.handleUrl(v.getContext(), ((URLSpan) link[0]).getURL(), 1); } if (!handled) { 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 37bc80f72..56c69f47c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/TextLinkifier.java +++ b/app/src/main/java/org/schabi/newpipe/util/TextLinkifier.java @@ -29,6 +29,8 @@ import static org.schabi.newpipe.util.URLHandler.playOnPopup; public final class TextLinkifier { public static final String TAG = TextLinkifier.class.getSimpleName(); + private static final Pattern TIMESTAMPS_PATTERN_IN_PLAIN_TEXT = + Pattern.compile("(?:([0-5]?[0-9]):)?([0-5]?[0-9]):([0-5][0-9])"); private TextLinkifier() { } @@ -115,9 +117,6 @@ public final class TextLinkifier { 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. *

@@ -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 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); + = new SinglePlayQueue(info, seconds * 1000); NavigationHelper.playOnPopupPlayer(context, playQueue, false); }); return true; From 2fb86364ab452a4585d284d0af76cfc6b3ec65f0 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Thu, 18 Mar 2021 19:19:41 +0100 Subject: [PATCH 06/20] Fix title of the subject when sharing an URL --- .../main/java/org/schabi/newpipe/RouterActivity.java | 2 +- .../java/org/schabi/newpipe/util/ShareUtils.java | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 0d70a7181..b1045bd4e 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -234,7 +234,7 @@ public class RouterActivity extends AppCompatActivity { .setPositiveButton(R.string.open_in_browser, (dialog, which) -> ShareUtils.openUrlInBrowser(this, url)) .setNegativeButton(R.string.share, - (dialog, which) -> ShareUtils.shareText(this, "", url)) // no subject + (dialog, which) -> ShareUtils.shareText(this, "", url, false)) //no subject .setNeutralButton(R.string.cancel, null) .setOnDismissListener(dialog -> finish()) .show(); 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 18b2aa5d0..4474d1811 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java @@ -213,13 +213,21 @@ public final class ShareUtils { * @param url the url to share */ 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 boolean showPreviewText) { final Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); - if (!subject.isEmpty()) { + if (!subject.isEmpty() && showPreviewText) { shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); + shareIntent.putExtra(Intent.EXTRA_TITLE, subject); } shareIntent.putExtra(Intent.EXTRA_TEXT, url); - shareIntent.putExtra(Intent.EXTRA_TITLE, context.getString(R.string.share_dialog_title)); openAppChooser(context, shareIntent, false); } From d85afd6435ecfe2cbd213a84688b3e727e856667 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sat, 20 Mar 2021 16:35:14 +0100 Subject: [PATCH 07/20] Initial work to add the image of the content in the share sheet Also do some fixes when sharing a file in downloads and some improvements in JavaDocs of ShareUtils class. --- .../org/schabi/newpipe/RouterActivity.java | 2 +- .../fragments/detail/VideoDetailFragment.java | 4 +- .../list/channel/ChannelFragment.java | 3 +- .../list/playlist/PlaylistFragment.java | 2 +- .../subscription/SubscriptionFragment.kt | 3 +- .../newpipe/player/PlayQueueActivity.java | 3 +- .../org/schabi/newpipe/player/Player.java | 3 +- .../org/schabi/newpipe/util/ShareUtils.java | 96 +++++++++++++++---- .../newpipe/util/StreamDialogEntry.java | 3 +- .../giga/ui/adapter/MissionAdapter.java | 5 +- 10 files changed, 93 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index b1045bd4e..af6ca077c 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -234,7 +234,7 @@ public class RouterActivity extends AppCompatActivity { .setPositiveButton(R.string.open_in_browser, (dialog, which) -> ShareUtils.openUrlInBrowser(this, url)) .setNegativeButton(R.string.share, - (dialog, which) -> ShareUtils.shareText(this, "", url, false)) //no subject + (dialog, which) -> ShareUtils.shareText(this, "", url, "")) //no subject .setNeutralButton(R.string.cancel, null) .setOnDismissListener(dialog -> finish()) .show(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 784a1c3be..47257dd1f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -454,8 +454,8 @@ public final class VideoDetailFragment break; case R.id.detail_controls_share: if (currentInfo != null) { - ShareUtils.shareText(requireContext(), - currentInfo.getName(), currentInfo.getUrl()); + ShareUtils.shareText(requireContext(), currentInfo.getName(), + currentInfo.getUrl(), currentInfo.getThumbnailUrl()); } break; case R.id.detail_controls_open_in_browser: diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index e02e18a86..a94d60032 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -203,7 +203,8 @@ public class ChannelFragment extends BaseListInfoFragment break; case R.id.menu_item_share: if (currentInfo != null) { - ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl()); + ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(), + currentInfo.getAvatarUrl()); } break; default: diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 0e36d18c7..f7d3b34f8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -251,7 +251,7 @@ public class PlaylistFragment extends BaseListInfoFragment { ShareUtils.openUrlInBrowser(requireContext(), url); break; case R.id.menu_item_share: - ShareUtils.shareText(requireContext(), name, url); + ShareUtils.shareText(requireContext(), name, url, currentInfo.getThumbnailUrl()); break; case R.id.menu_item_bookmark: onBookmarkClicked(); diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index 095c8dbc7..060e5066b 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -293,7 +293,8 @@ class SubscriptionFragment : BaseStateFragment() { val actions = DialogInterface.OnClickListener { _, i -> when (i) { - 0 -> ShareUtils.shareText(requireContext(), selectedItem.name, selectedItem.url) + 0 -> ShareUtils.shareText(requireContext(), selectedItem.name, selectedItem.url, + selectedItem.thumbnailUrl) 1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url) 2 -> deleteChannel(selectedItem) } diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java index 29c9ac77b..403848dd6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java @@ -313,7 +313,8 @@ public final class PlayQueueActivity extends AppCompatActivity final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3, Menu.NONE, R.string.share); share.setOnMenuItemClickListener(menuItem -> { - shareText(getApplicationContext(), item.getTitle(), item.getUrl()); + shareText(getApplicationContext(), item.getTitle(), item.getUrl(), + item.getThumbnailUrl()); return true; }); diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 0c5dbbb6f..7600346ae 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -3593,7 +3593,8 @@ public final class Player implements } else if (v.getId() == binding.moreOptionsButton.getId()) { onMoreOptionsClicked(); } else if (v.getId() == binding.share.getId()) { - ShareUtils.shareText(context, getVideoTitle(), getVideoUrlAtCurrentTime()); + ShareUtils.shareText(context, getVideoTitle(), getVideoUrlAtCurrentTime(), + currentItem.getThumbnailUrl()); } else if (v.getId() == binding.playWithKodi.getId()) { onPlayWithKodiClicked(); } else if (v.getId() == binding.openInBrowser.getId()) { 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 4474d1811..682830741 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java @@ -8,6 +8,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; +import android.os.Build; import android.widget.Toast; import androidx.core.content.ContextCompat; @@ -25,15 +26,15 @@ 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, 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 implements Handler.Callb final Intent intent = new Intent(Intent.ACTION_CHOOSER); intent.putExtra(Intent.EXTRA_INTENT, shareIntent); - intent.putExtra(Intent.EXTRA_TITLE, mContext.getString(R.string.share_dialog_title)); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) { + intent.putExtra(Intent.EXTRA_TITLE, mContext.getString(R.string.share_dialog_title)); + } intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); mContext.startActivity(intent); } From d6decc05d735ce96323687646c2c2673819e7b36 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Fri, 26 Mar 2021 13:28:11 +0100 Subject: [PATCH 08/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. --- .../org/schabi/newpipe/RouterActivity.java | 2 +- .../schabi/newpipe/error/ErrorActivity.java | 2 +- .../fragments/detail/DescriptionFragment.java | 4 +- .../fragments/detail/VideoDetailFragment.java | 8 +-- .../fragments/list/BaseListFragment.java | 4 +- .../list/channel/ChannelFragment.java | 2 +- .../list/playlist/PlaylistFragment.java | 6 +-- .../holder/CommentsMiniInfoItemHolder.java | 2 +- .../history/StatisticsPlaylistFragment.java | 4 +- .../local/playlist/LocalPlaylistFragment.java | 4 +- .../subscription/SubscriptionFragment.kt | 2 +- .../newpipe/player/PlayQueueActivity.java | 2 +- .../org/schabi/newpipe/player/Player.java | 8 +-- .../util/CommentTextOnTouchListener.java | 5 +- .../schabi/newpipe/util/ExtractorHelper.java | 1 + .../schabi/newpipe/util/NavigationHelper.java | 3 +- .../newpipe/util/StreamDialogEntry.java | 4 +- .../InternalUrlsHandler.java} | 8 +-- .../KoreUtils.java} | 7 +-- .../ShareUtils.java | 50 +++++++------------ .../TextLinkifier.java | 13 ++--- .../giga/ui/adapter/MissionAdapter.java | 3 +- 22 files changed, 70 insertions(+), 74 deletions(-) rename app/src/main/java/org/schabi/newpipe/util/{URLHandler.java => external_communication/InternalUrlsHandler.java} (95%) rename app/src/main/java/org/schabi/newpipe/util/{KoreUtil.java => external_communication/KoreUtils.java} (88%) rename app/src/main/java/org/schabi/newpipe/util/{ => external_communication}/ShareUtils.java (87%) rename app/src/main/java/org/schabi/newpipe/util/{ => external_communication}/TextLinkifier.java (96%) diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index af6ca077c..222b6a3d9 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -69,7 +69,7 @@ import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.ShareUtils; +import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.urlfinder.UrlFinder; import org.schabi.newpipe.views.FocusOverlayView; 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 5fc9a72ca..0f9d6569c 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java @@ -27,7 +27,7 @@ import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.ActivityErrorBinding; import org.schabi.newpipe.util.Localization; -import org.schabi.newpipe.util.ShareUtils; +import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import java.time.LocalDateTime; 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 49ac3ef01..fb2f07eec 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 @@ -24,8 +24,8 @@ import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.ShareUtils; -import org.schabi.newpipe.util.TextLinkifier; +import org.schabi.newpipe.util.external_communication.ShareUtils; +import org.schabi.newpipe.util.external_communication.TextLinkifier; import java.util.ArrayList; import java.util.Collections; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 47257dd1f..c86d9596b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -91,12 +91,12 @@ import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; -import org.schabi.newpipe.util.KoreUtil; +import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.ShareUtils; +import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import java.util.ArrayList; @@ -472,7 +472,7 @@ public final class VideoDetailFragment if (DEBUG) { Log.i(TAG, "Failed to start kore", e); } - KoreUtil.showInstallKoreDialog(requireContext()); + KoreUtils.showInstallKoreDialog(requireContext()); } } break; @@ -631,7 +631,7 @@ public final class VideoDetailFragment binding.detailControlsShare.setOnClickListener(this); binding.detailControlsOpenInBrowser.setOnClickListener(this); binding.detailControlsPlayWithKodi.setOnClickListener(this); - binding.detailControlsPlayWithKodi.setVisibility(KoreUtil.shouldShowPlayWithKodi( + binding.detailControlsPlayWithKodi.setVisibility(KoreUtils.shouldShowPlayWithKodi( requireContext(), serviceId) ? View.VISIBLE : View.GONE); binding.overlayThumbnail.setOnClickListener(this); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 728558685..c28a41b7d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -33,7 +33,7 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.player.helper.PlayerHolder; -import org.schabi.newpipe.util.KoreUtil; +import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; @@ -371,7 +371,7 @@ public abstract class BaseListFragment extends BaseStateFragment )); } entries.add(StreamDialogEntry.open_in_browser); - if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) { + if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) { entries.add(StreamDialogEntry.play_with_kodi); } if (!isNullOrEmpty(item.getUploaderUrl())) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index a94d60032..fe07a8dc9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -43,7 +43,7 @@ import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.ShareUtils; +import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import java.util.ArrayList; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index f7d3b34f8..938cf9d66 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -42,10 +42,10 @@ import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; -import org.schabi.newpipe.util.KoreUtil; +import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.ShareUtils; +import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.StreamDialogEntry; import java.util.ArrayList; @@ -162,7 +162,7 @@ public class PlaylistFragment extends BaseListInfoFragment { )); } entries.add(StreamDialogEntry.open_in_browser); - if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) { + if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) { entries.add(StreamDialogEntry.play_with_kodi); } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java index ae7ddfd63..629240dc6 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -24,7 +24,7 @@ import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.ShareUtils; +import org.schabi.newpipe.util.external_communication.ShareUtils; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 10aa8aa68..2fe757dea 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -36,7 +36,7 @@ import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.settings.HistorySettingsFragment; -import org.schabi.newpipe.util.KoreUtil; +import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StreamDialogEntry; @@ -359,7 +359,7 @@ public class StatisticsPlaylistFragment )); } entries.add(StreamDialogEntry.open_in_browser); - if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { + if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { entries.add(StreamDialogEntry.play_with_kodi); } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index f79282641..768b0b862 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -44,7 +44,7 @@ import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; -import org.schabi.newpipe.util.KoreUtil; +import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; @@ -770,7 +770,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment 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. + *

+ * 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 - private var componentForContextMenu: SoftwareComponent? = null private var activeLicense: License? = null private val compositeDisposable = CompositeDisposable() 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 af6e49d72..ff60126ec 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 @@ -117,9 +117,8 @@ public final class ShareUtils { * The intent can be of every type, excepted a web intent for which * {@link #openUrlInBrowser(Context, String, boolean)} should be used. *

- * 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 implements Handler.Callb final Intent intent = new Intent(Intent.ACTION_CHOOSER); intent.putExtra(Intent.EXTRA_INTENT, shareIntent); + // unneeded to set a title to the chooser on Android P and higher because the system + // ignores this title on these versions if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) { intent.putExtra(Intent.EXTRA_TITLE, mContext.getString(R.string.share_dialog_title)); } From f13f4cc5d21397e92e59d2d609788d34ab5d1c8d Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sat, 3 Apr 2021 18:57:16 +0200 Subject: [PATCH 13/20] Split handleUrl method into two methods Split handleURL method, now private, into two methods: handleUrlCommentsTimestamp and handleUrlDescriptionTimestamp. Code is now more proper. --- .../util/CommentTextOnTouchListener.java | 3 +- .../InternalUrlsHandler.java | 78 ++++++++++++------- .../external_communication/TextLinkifier.java | 2 +- 3 files changed, 53 insertions(+), 30 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 d0edc4bc4..dfe5e8ad0 100644 --- a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java +++ b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java @@ -50,7 +50,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.handleUrl(v.getContext(), url, 1)) { + if (!InternalUrlsHandler.handleUrlCommentsTimestamp(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 a234f6203..086ba0015 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 @@ -20,6 +20,8 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + public final class InternalUrlsHandler { private static final Pattern AMPERSAND_TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)"); private static final Pattern HASHTAG_TIMESTAMP_PATTERN = @@ -28,48 +30,68 @@ public final class InternalUrlsHandler { private InternalUrlsHandler() { } + /** + * Handle a YouTube timestamp comment URL in NewPipe. + *

+ * 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 single = ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false); - single.subscribeOn(Schedulers.io()) + disposables.add(single.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(info -> { final PlayQueue playQueue = new SinglePlayQueue(info, seconds * 1000); NavigationHelper.playOnPopupPlayer(context, playQueue, false); - }); + })); return true; } } 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 50a453e35..6c5cfb499 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 @@ -23,6 +23,7 @@ import io.noties.markwon.Markwon; import io.noties.markwon.linkify.LinkifyPlugin; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; @@ -201,7 +202,8 @@ public final class TextLinkifier { spannableDescription.setSpan(new ClickableSpan() { @Override public void onClick(@NonNull final View view) { - playOnPopup(context, contentUrl, streamingService, time); + playOnPopup(new CompositeDisposable(), context, contentUrl, streamingService, + time); } }, timestampStart, timestampEnd, 0); } @@ -245,7 +247,8 @@ public final class TextLinkifier { final String url = span.getURL(); final ClickableSpan clickableSpan = new ClickableSpan() { public void onClick(@NonNull final View view) { - if (!InternalUrlsHandler.handleUrlDescriptionTimestamp(context, url)) { + if (!InternalUrlsHandler.handleUrlDescriptionTimestamp( + new CompositeDisposable(), context, url)) { ShareUtils.openUrlInBrowser(context, url, false); } } From f02df6d80c7e2f534ecd6497d4161e2c955aa63b Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sun, 25 Apr 2021 14:14:56 +0200 Subject: [PATCH 15/20] Fix an error and add a new method in the ShareUtils class Fix the error due to the rebase on the dev branch of this branch Add a shareText method in the ShareUtils class which has 3 parameters and calls the original shareText method with an empty string for the imagePreviewUrl param. --- .../java/org/schabi/newpipe/RouterActivity.java | 2 +- .../org/schabi/newpipe/error/ErrorActivity.java | 3 ++- .../util/external_communication/ShareUtils.java | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 222b6a3d9..55bb8424f 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -234,7 +234,7 @@ public class RouterActivity extends AppCompatActivity { .setPositiveButton(R.string.open_in_browser, (dialog, which) -> ShareUtils.openUrlInBrowser(this, url)) .setNegativeButton(R.string.share, - (dialog, which) -> ShareUtils.shareText(this, "", url, "")) //no subject + (dialog, which) -> ShareUtils.shareText(this, "", url)) // no subject .setNeutralButton(R.string.cancel, null) .setOnDismissListener(dialog -> finish()) .show(); 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 0f9d6569c..c0d88c8ec 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java @@ -195,7 +195,8 @@ public class ErrorActivity extends AppCompatActivity { onBackPressed(); return true; case R.id.menu_item_share_error: - ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson()); + ShareUtils.shareText(getApplicationContext(), + getString(R.string.error_report_title), buildJson()); return true; default: return false; 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 ff60126ec..b9f96746f 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 @@ -259,6 +259,23 @@ public final class ShareUtils { openAppChooser(context, shareIntent, false); } + /** + * Open the android share sheet to share a content. + * + * For Android 10+ users, a content preview is shown, which includes the title of the shared + * content. + *

+ * 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 Date: Sat, 5 Jun 2021 14:59:23 +0200 Subject: [PATCH 17/20] Improve text linkifier function parameters --- .../org/schabi/newpipe/about/AboutActivity.kt | 2 +- .../fragments/detail/DescriptionFragment.java | 23 +-- .../schabi/newpipe/util/ExtractorHelper.java | 6 +- .../external_communication/TextLinkifier.java | 137 ++++++++---------- 4 files changed, 74 insertions(+), 94 deletions(-) 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 d40bb209e..0199f30d8 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt @@ -16,9 +16,9 @@ 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.ThemeHelper +import org.schabi.newpipe.util.external_communication.ShareUtils class AboutActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { 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 fb2f07eec..fcc183dad 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,7 +19,6 @@ 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; @@ -132,24 +131,19 @@ 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, - service, contentUrl, HtmlCompat.FROM_HTML_MODE_LEGACY); + descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock( + binding.detailDescriptionView, description.getContent(), + HtmlCompat.FROM_HTML_MODE_LEGACY, streamInfo); break; case Description.MARKDOWN: - descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(), - description.getContent(), binding.detailDescriptionView, - service, contentUrl); + descriptionDisposable = TextLinkifier.createLinksFromMarkdownText( + binding.detailDescriptionView, description.getContent(), streamInfo); break; case Description.PLAIN_TEXT: default: - descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(), - description.getContent(), binding.detailDescriptionView, - service, contentUrl); + descriptionDisposable = TextLinkifier.createLinksFromPlainText( + binding.detailDescriptionView, description.getContent(), streamInfo); break; } } @@ -204,8 +198,7 @@ public class DescriptionFragment extends BaseFragment { }); if (linkifyContent) { - TextLinkifier.createLinksFromPlainText(requireContext(), - content, itemBinding.metadataContentView, null, null); + TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, null); } else { itemBinding.metadataContentView.setText(content); } 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 2d841b0c8..67e12a77f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -311,9 +311,9 @@ public final class ExtractorHelper { } metaInfoSeparator.setVisibility(View.VISIBLE); - return TextLinkifier.createLinksFromHtmlBlock(context, stringBuilder.toString(), - metaInfoTextView, null, null, - HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING); + return TextLinkifier.createLinksFromHtmlBlock(metaInfoTextView, + stringBuilder.toString(), HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING, + null); } } 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 d9d9875ab..203b68726 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 @@ -11,9 +11,10 @@ import android.view.View; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.text.HtmlCompat; -import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.util.NavigationHelper; import java.util.regex.Matcher; @@ -42,85 +43,74 @@ public final class TextLinkifier { * Create web links for contents with an HTML 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 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 Date: Sat, 5 Jun 2021 15:35:48 +0200 Subject: [PATCH 18/20] Fix disposables handling for text linkifier also use differently Markwon methods to convert plain text to markdown --- .../fragments/detail/DescriptionFragment.java | 26 +++-- .../fragments/detail/VideoDetailFragment.java | 4 +- .../fragments/list/search/SearchFragment.java | 11 ++- .../schabi/newpipe/util/ExtractorHelper.java | 18 ++-- .../InternalUrlsHandler.java | 52 +++++----- .../external_communication/TextLinkifier.java | 94 ++++++++++--------- 6 files changed, 101 insertions(+), 104 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 fcc183dad..92a571f37 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 @@ -31,7 +31,7 @@ import java.util.Collections; import java.util.List; import icepick.State; -import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.disposables.CompositeDisposable; import static android.text.TextUtils.isEmpty; import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; @@ -41,8 +41,7 @@ public class DescriptionFragment extends BaseFragment { @State StreamInfo streamInfo = null; - @Nullable - Disposable descriptionDisposable = null; + final CompositeDisposable descriptionDisposables = new CompositeDisposable(); FragmentDescriptionBinding binding; public DescriptionFragment() { @@ -67,10 +66,8 @@ public class DescriptionFragment extends BaseFragment { @Override public void onDestroy() { + descriptionDisposables.clear(); super.onDestroy(); - if (descriptionDisposable != null) { - descriptionDisposable.dispose(); - } } @@ -133,17 +130,17 @@ public class DescriptionFragment extends BaseFragment { final Description description = streamInfo.getDescription(); switch (description.getType()) { case Description.HTML: - descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock( - binding.detailDescriptionView, description.getContent(), - HtmlCompat.FROM_HTML_MODE_LEGACY, streamInfo); + TextLinkifier.createLinksFromHtmlBlock(binding.detailDescriptionView, + description.getContent(), HtmlCompat.FROM_HTML_MODE_LEGACY, streamInfo, + descriptionDisposables); break; case Description.MARKDOWN: - descriptionDisposable = TextLinkifier.createLinksFromMarkdownText( - binding.detailDescriptionView, description.getContent(), streamInfo); + TextLinkifier.createLinksFromMarkdownText(binding.detailDescriptionView, + description.getContent(), streamInfo, descriptionDisposables); break; case Description.PLAIN_TEXT: default: - descriptionDisposable = TextLinkifier.createLinksFromPlainText( - binding.detailDescriptionView, description.getContent(), streamInfo); + TextLinkifier.createLinksFromPlainText(binding.detailDescriptionView, + description.getContent(), streamInfo, descriptionDisposables); break; } } @@ -198,7 +195,8 @@ public class DescriptionFragment extends BaseFragment { }); if (linkifyContent) { - TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, null); + TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, null, + descriptionDisposables); } else { itemBinding.metadataContentView.setText(content); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index c86d9596b..fbd11283f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1546,8 +1546,8 @@ public final class VideoDetailFragment .getDefaultResolutionIndex(activity, sortedVideoStreams); updateProgressInfo(info); initThumbnailViews(info); - disposables.add(showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView, - binding.detailMetaInfoSeparator)); + showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView, + binding.detailMetaInfoSeparator, disposables); if (player == null || player.isStopped()) { updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl()); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 70fce1cb7..2810b9b76 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -278,8 +278,9 @@ public class SearchFragment extends BaseListFragment cannot be bundled without creating some containers metaInfo = new MetaInfo[result.getMetaInfo().size()]; metaInfo = result.getMetaInfo().toArray(metaInfo); - disposables.add(showMetaInfoInTextView(result.getMetaInfo(), - searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator)); + showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView, + searchBinding.searchMetaInfoSeparator, disposables); handleSearchSuggestion(); 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 67e12a77f..af94e3366 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -55,7 +55,7 @@ import java.util.List; import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.disposables.CompositeDisposable; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; @@ -269,18 +269,19 @@ public final class ExtractorHelper { * @param metaInfos a list of meta information, can be null or empty * @param metaInfoTextView the text view in which to show the formatted HTML * @param metaInfoSeparator another view to be shown or hidden accordingly to the text view - * @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed + * @param disposables disposables created by the method are added here and their lifecycle + * should be handled by the calling class */ - public static Disposable showMetaInfoInTextView(@Nullable final List metaInfos, - final TextView metaInfoTextView, - final View metaInfoSeparator) { + public static void showMetaInfoInTextView(@Nullable final List metaInfos, + final TextView metaInfoTextView, + final View metaInfoSeparator, + final CompositeDisposable disposables) { final Context context = metaInfoTextView.getContext(); if (metaInfos == null || metaInfos.isEmpty() || !PreferenceManager.getDefaultSharedPreferences(context).getBoolean( context.getString(R.string.show_meta_info_key), true)) { metaInfoTextView.setVisibility(View.GONE); metaInfoSeparator.setVisibility(View.GONE); - return Disposable.empty(); } else { final StringBuilder stringBuilder = new StringBuilder(); @@ -311,9 +312,8 @@ public final class ExtractorHelper { } metaInfoSeparator.setVisibility(View.VISIBLE); - return TextLinkifier.createLinksFromHtmlBlock(metaInfoTextView, - stringBuilder.toString(), HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING, - null); + TextLinkifier.createLinksFromHtmlBlock(metaInfoTextView, stringBuilder.toString(), + HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING, null, disposables); } } 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 69c846dbc..39ec51ce4 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 @@ -23,8 +23,6 @@ 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; - public final class InternalUrlsHandler { private static final Pattern AMPERSAND_TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)"); private static final Pattern HASHTAG_TIMESTAMP_PATTERN = @@ -50,7 +48,7 @@ public final class InternalUrlsHandler { disposables, final Context context, @NonNull final String url) { - return handleUrl(disposables, context, url, HASHTAG_TIMESTAMP_PATTERN); + return handleUrl(context, url, HASHTAG_TIMESTAMP_PATTERN, disposables); } /** @@ -70,7 +68,7 @@ public final class InternalUrlsHandler { disposables, final Context context, @NonNull final String url) { - return handleUrl(disposables, context, url, AMPERSAND_TIMESTAMP_PATTERN); + return handleUrl(context, url, AMPERSAND_TIMESTAMP_PATTERN, disposables); } /** @@ -80,42 +78,37 @@ 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 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 + * @param disposables a field of the Activity/Fragment class that calls this method * @return true if the URL can be handled by NewPipe, false if it cannot */ - private static boolean handleUrl(@NonNull final CompositeDisposable disposables, - final Context context, + private static boolean handleUrl(final Context context, @NonNull final String url, - @NonNull final Pattern pattern) { - final String matchedUrl; + @NonNull final Pattern pattern, + @NonNull final CompositeDisposable disposables) { + final Matcher matcher = pattern.matcher(url); + if (!matcher.matches()) { + return false; + } + final String matchedUrl = matcher.group(1); + final int seconds = Integer.parseInt(matcher.group(2)); + 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 (isNullOrEmpty(matchedUrl)) { - return false; - } try { service = NewPipe.getServiceByUrl(matchedUrl); linkType = service.getLinkTypeByUrl(matchedUrl); + if (linkType == StreamingService.LinkType.NONE) { + return false; + } } catch (final ExtractionException e) { return false; } - if (linkType == StreamingService.LinkType.NONE) { - return false; - } + if (linkType == StreamingService.LinkType.STREAM && seconds != -1) { - return playOnPopup(disposables, context, matchedUrl, service, seconds); + return playOnPopup(context, matchedUrl, service, seconds, disposables); } else { NavigationHelper.openRouterActivity(context, matchedUrl); return true; @@ -125,18 +118,19 @@ public final class InternalUrlsHandler { /** * Play a content in the floating player. * - * @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 + * @param disposables disposables created by the method are added here and their lifecycle + * should be handled by the calling class * @return true if the playback of the content has successfully started or false if not */ - public static boolean playOnPopup(@NonNull final CompositeDisposable disposables, - final Context context, + public static boolean playOnPopup(final Context context, final String url, @NonNull final StreamingService service, - final int seconds) { + final int seconds, + @NonNull final CompositeDisposable disposables) { final LinkHandlerFactory factory = service.getStreamLHFactory(); final String cleanUrl; 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 203b68726..f4b423e41 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 @@ -25,7 +25,6 @@ import io.noties.markwon.linkify.LinkifyPlugin; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; -import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; import static org.schabi.newpipe.util.external_communication.InternalUrlsHandler.playOnPopup; @@ -42,9 +41,9 @@ public final class TextLinkifier { /** * Create web links for contents with an HTML description. *

- * 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); }