Merge pull request #5523 from TiA4f8R/share-improvements

Recognize timestamps and hashtags in descriptions and do some sharing fixes and improvements
This commit is contained in:
Stypox 2021-06-15 20:56:37 +02:00 committed by GitHub
commit 92910eb227
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 851 additions and 548 deletions

View file

@ -129,13 +129,8 @@ public final class CheckForNewAppVersion {
if (BuildConfig.VERSION_CODE < versionCode) { if (BuildConfig.VERSION_CODE < versionCode) {
// A pending intent to open the apk location url in the browser. // A pending intent to open the apk location url in the browser.
final Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl)); final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
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); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final PendingIntent pendingIntent final PendingIntent pendingIntent
= PendingIntent.getActivity(application, 0, intent, 0); = PendingIntent.getActivity(application, 0, intent, 0);

View file

@ -69,7 +69,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper; 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.ThemeHelper;
import org.schabi.newpipe.util.urlfinder.UrlFinder; import org.schabi.newpipe.util.urlfinder.UrlFinder;
import org.schabi.newpipe.views.FocusOverlayView; import org.schabi.newpipe.views.FocusOverlayView;

View file

@ -17,8 +17,8 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ActivityAboutBinding import org.schabi.newpipe.databinding.ActivityAboutBinding
import org.schabi.newpipe.databinding.FragmentAboutBinding import org.schabi.newpipe.databinding.FragmentAboutBinding
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ShareUtils
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
class AboutActivity : AppCompatActivity() { class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View file

@ -19,7 +19,6 @@ import java.util.Objects
*/ */
class LicenseFragment : Fragment() { class LicenseFragment : Fragment() {
private lateinit var softwareComponents: Array<SoftwareComponent> private lateinit var softwareComponents: Array<SoftwareComponent>
private var componentForContextMenu: SoftwareComponent? = null
private var activeLicense: License? = null private var activeLicense: License? = null
private val compositeDisposable = CompositeDisposable() private val compositeDisposable = CompositeDisposable()

View file

@ -10,8 +10,8 @@ import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ShareUtils
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
import java.io.BufferedReader import java.io.BufferedReader
import java.io.IOException import java.io.IOException
import java.io.InputStreamReader import java.io.InputStreamReader

View file

@ -27,7 +27,7 @@ import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityErrorBinding; import org.schabi.newpipe.databinding.ActivityErrorBinding;
import org.schabi.newpipe.util.Localization; 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 org.schabi.newpipe.util.ThemeHelper;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -195,7 +195,8 @@ public class ErrorActivity extends AppCompatActivity {
onBackPressed(); onBackPressed();
return true; return true;
case R.id.menu_item_share_error: 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; return true;
default: default:
return false; return false;
@ -220,13 +221,10 @@ public class ErrorActivity extends AppCompatActivity {
+ getString(R.string.app_name) + " " + getString(R.string.app_name) + " "
+ BuildConfig.VERSION_NAME) + BuildConfig.VERSION_NAME)
.putExtra(Intent.EXTRA_TEXT, buildJson()); .putExtra(Intent.EXTRA_TEXT, buildJson());
if (i.resolveActivity(getPackageManager()) != null) { ShareUtils.openIntentInApp(context, i, true);
ShareUtils.openIntentInApp(context, i);
}
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub } else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false); ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false);
} }
}) })
.setNegativeButton(R.string.decline, (dialog, which) -> { .setNegativeButton(R.string.decline, (dialog, which) -> {
// do nothing // do nothing

View file

@ -23,15 +23,15 @@ import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; 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.TextLinkifier; import org.schabi.newpipe.util.external_communication.TextLinkifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import icepick.State; import icepick.State;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import static android.text.TextUtils.isEmpty; import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
@ -41,8 +41,7 @@ public class DescriptionFragment extends BaseFragment {
@State @State
StreamInfo streamInfo = null; StreamInfo streamInfo = null;
@Nullable final CompositeDisposable descriptionDisposables = new CompositeDisposable();
Disposable descriptionDisposable = null;
FragmentDescriptionBinding binding; FragmentDescriptionBinding binding;
public DescriptionFragment() { public DescriptionFragment() {
@ -67,10 +66,8 @@ public class DescriptionFragment extends BaseFragment {
@Override @Override
public void onDestroy() { public void onDestroy() {
descriptionDisposables.clear();
super.onDestroy(); super.onDestroy();
if (descriptionDisposable != null) {
descriptionDisposable.dispose();
}
} }
@ -133,17 +130,17 @@ public class DescriptionFragment extends BaseFragment {
final Description description = streamInfo.getDescription(); final Description description = streamInfo.getDescription();
switch (description.getType()) { switch (description.getType()) {
case Description.HTML: case Description.HTML:
descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock(requireContext(), TextLinkifier.createLinksFromHtmlBlock(binding.detailDescriptionView,
description.getContent(), binding.detailDescriptionView, description.getContent(), HtmlCompat.FROM_HTML_MODE_LEGACY, streamInfo,
HtmlCompat.FROM_HTML_MODE_LEGACY); descriptionDisposables);
break; break;
case Description.MARKDOWN: case Description.MARKDOWN:
descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(), TextLinkifier.createLinksFromMarkdownText(binding.detailDescriptionView,
description.getContent(), binding.detailDescriptionView); description.getContent(), streamInfo, descriptionDisposables);
break; break;
case Description.PLAIN_TEXT: default: case Description.PLAIN_TEXT: default:
descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(), TextLinkifier.createLinksFromPlainText(binding.detailDescriptionView,
description.getContent(), binding.detailDescriptionView); description.getContent(), streamInfo, descriptionDisposables);
break; break;
} }
} }
@ -198,8 +195,8 @@ public class DescriptionFragment extends BaseFragment {
}); });
if (linkifyContent) { if (linkifyContent) {
TextLinkifier.createLinksFromPlainText(requireContext(), TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, null,
content, itemBinding.metadataContentView); descriptionDisposables);
} else { } else {
itemBinding.metadataContentView.setText(content); itemBinding.metadataContentView.setText(content);
} }

View file

@ -91,12 +91,12 @@ import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants; 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.ListHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper; 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.ThemeHelper;
import java.util.ArrayList; import java.util.ArrayList;
@ -454,8 +454,8 @@ public final class VideoDetailFragment
break; break;
case R.id.detail_controls_share: case R.id.detail_controls_share:
if (currentInfo != null) { if (currentInfo != null) {
ShareUtils.shareText(requireContext(), ShareUtils.shareText(requireContext(), currentInfo.getName(),
currentInfo.getName(), currentInfo.getUrl()); currentInfo.getUrl(), currentInfo.getThumbnailUrl());
} }
break; break;
case R.id.detail_controls_open_in_browser: case R.id.detail_controls_open_in_browser:
@ -472,7 +472,7 @@ public final class VideoDetailFragment
if (DEBUG) { if (DEBUG) {
Log.i(TAG, "Failed to start kore", e); Log.i(TAG, "Failed to start kore", e);
} }
KoreUtil.showInstallKoreDialog(requireContext()); KoreUtils.showInstallKoreDialog(requireContext());
} }
} }
break; break;
@ -631,7 +631,7 @@ public final class VideoDetailFragment
binding.detailControlsShare.setOnClickListener(this); binding.detailControlsShare.setOnClickListener(this);
binding.detailControlsOpenInBrowser.setOnClickListener(this); binding.detailControlsOpenInBrowser.setOnClickListener(this);
binding.detailControlsPlayWithKodi.setOnClickListener(this); binding.detailControlsPlayWithKodi.setOnClickListener(this);
binding.detailControlsPlayWithKodi.setVisibility(KoreUtil.shouldShowPlayWithKodi( binding.detailControlsPlayWithKodi.setVisibility(KoreUtils.shouldShowPlayWithKodi(
requireContext(), serviceId) ? View.VISIBLE : View.GONE); requireContext(), serviceId) ? View.VISIBLE : View.GONE);
binding.overlayThumbnail.setOnClickListener(this); binding.overlayThumbnail.setOnClickListener(this);
@ -1546,8 +1546,8 @@ public final class VideoDetailFragment
.getDefaultResolutionIndex(activity, sortedVideoStreams); .getDefaultResolutionIndex(activity, sortedVideoStreams);
updateProgressInfo(info); updateProgressInfo(info);
initThumbnailViews(info); initThumbnailViews(info);
disposables.add(showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView, showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator)); binding.detailMetaInfoSeparator, disposables);
if (player == null || player.isStopped()) { if (player == null || player.isStopped()) {
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl()); updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());

View file

@ -33,7 +33,7 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.player.helper.PlayerHolder; 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.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
@ -371,7 +371,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
)); ));
} }
entries.add(StreamDialogEntry.open_in_browser); entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) { if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi); entries.add(StreamDialogEntry.play_with_kodi);
} }
if (!isNullOrEmpty(item.getUploaderUrl())) { if (!isNullOrEmpty(item.getUploaderUrl())) {

View file

@ -43,7 +43,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; 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 org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList; import java.util.ArrayList;
@ -203,7 +203,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
break; break;
case R.id.menu_item_share: case R.id.menu_item_share:
if (currentInfo != null) { if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl()); ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
currentInfo.getAvatarUrl());
} }
break; break;
default: default:

View file

@ -42,10 +42,10 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants; 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.Localization;
import org.schabi.newpipe.util.NavigationHelper; 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 org.schabi.newpipe.util.StreamDialogEntry;
import java.util.ArrayList; import java.util.ArrayList;
@ -162,7 +162,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
)); ));
} }
entries.add(StreamDialogEntry.open_in_browser); entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) { if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi); entries.add(StreamDialogEntry.play_with_kodi);
} }
@ -251,7 +251,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
ShareUtils.openUrlInBrowser(requireContext(), url); ShareUtils.openUrlInBrowser(requireContext(), url);
break; break;
case R.id.menu_item_share: case R.id.menu_item_share:
ShareUtils.shareText(requireContext(), name, url); ShareUtils.shareText(requireContext(), name, url, currentInfo.getThumbnailUrl());
break; break;
case R.id.menu_item_bookmark: case R.id.menu_item_bookmark:
onBookmarkClicked(); onBookmarkClicked();

View file

@ -278,8 +278,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
handleSearchSuggestion(); handleSearchSuggestion();
disposables.add(showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo), showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator)); searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator,
disposables);
if (TextUtils.isEmpty(searchString) || wasSearchFocused) { if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
showKeyboardSearch(); showKeyboardSearch();
@ -841,7 +842,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
infoListAdapter.clearStreamItemList(); infoListAdapter.clearStreamItemList();
hideSuggestionsPanel(); hideSuggestionsPanel();
showMetaInfoInTextView(null, searchBinding.searchMetaInfoTextView, showMetaInfoInTextView(null, searchBinding.searchMetaInfoTextView,
searchBinding.searchMetaInfoSeparator); searchBinding.searchMetaInfoSeparator, disposables);
hideKeyboardSearch(); hideKeyboardSearch();
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString) disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
@ -986,8 +987,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
// List<MetaInfo> cannot be bundled without creating some containers // List<MetaInfo> cannot be bundled without creating some containers
metaInfo = new MetaInfo[result.getMetaInfo().size()]; metaInfo = new MetaInfo[result.getMetaInfo().size()];
metaInfo = result.getMetaInfo().toArray(metaInfo); metaInfo = result.getMetaInfo().toArray(metaInfo);
disposables.add(showMetaInfoInTextView(result.getMetaInfo(), showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView,
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator)); searchBinding.searchMetaInfoSeparator, disposables);
handleSearchSuggestion(); handleSearchSuggestion();

View file

@ -24,7 +24,7 @@ import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; 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.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;

View file

@ -36,7 +36,7 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.settings.HistorySettingsFragment; 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.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StreamDialogEntry; import org.schabi.newpipe.util.StreamDialogEntry;
@ -359,7 +359,7 @@ public class StatisticsPlaylistFragment
)); ));
} }
entries.add(StreamDialogEntry.open_in_browser); entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi); entries.add(StreamDialogEntry.play_with_kodi);
} }

View file

@ -44,7 +44,7 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; 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.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
@ -770,7 +770,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
)); ));
} }
entries.add(StreamDialogEntry.open_in_browser); entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi); entries.add(StreamDialogEntry.play_with_kodi);
} }

View file

@ -56,9 +56,9 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
import org.schabi.newpipe.streams.io.StoredFileHelper import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.ShareUtils
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -293,7 +293,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
val actions = DialogInterface.OnClickListener { _, i -> val actions = DialogInterface.OnClickListener { _, i ->
when (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) 1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
2 -> deleteChannel(selectedItem) 2 -> deleteChannel(selectedItem)
} }

View file

@ -47,7 +47,7 @@ import java.util.List;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static org.schabi.newpipe.util.ShareUtils.shareText; import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
public final class PlayQueueActivity extends AppCompatActivity public final class PlayQueueActivity extends AppCompatActivity
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
@ -313,7 +313,8 @@ public final class PlayQueueActivity extends AppCompatActivity
final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3, final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3,
Menu.NONE, R.string.share); Menu.NONE, R.string.share);
share.setOnMenuItemClickListener(menuItem -> { share.setOnMenuItemClickListener(menuItem -> {
shareText(getApplicationContext(), item.getTitle(), item.getUrl()); shareText(getApplicationContext(), item.getTitle(), item.getUrl(),
item.getThumbnailUrl());
return true; return true;
}); });

View file

@ -123,11 +123,11 @@ import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ImageDisplayConstants; 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.ListHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.ExpandableSurfaceView; import org.schabi.newpipe.views.ExpandableSurfaceView;
import java.io.IOException; import java.io.IOException;
@ -1033,7 +1033,7 @@ public final class Player implements
// show kodi button if it supports the current service and it is enabled in settings // show kodi button if it supports the current service and it is enabled in settings
binding.playWithKodi.setVisibility(videoPlayerSelected() binding.playWithKodi.setVisibility(videoPlayerSelected()
&& playQueue != null && playQueue.getItem() != null && playQueue != null && playQueue.getItem() != null
&& KoreUtil.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId()) && KoreUtils.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId())
? View.VISIBLE : View.GONE); ? View.VISIBLE : View.GONE);
} }
//endregion //endregion
@ -3593,7 +3593,8 @@ public final class Player implements
} else if (v.getId() == binding.moreOptionsButton.getId()) { } else if (v.getId() == binding.moreOptionsButton.getId()) {
onMoreOptionsClicked(); onMoreOptionsClicked();
} else if (v.getId() == binding.share.getId()) { } 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()) { } else if (v.getId() == binding.playWithKodi.getId()) {
onPlayWithKodiClicked(); onPlayWithKodiClicked();
} else if (v.getId() == binding.openInBrowser.getId()) { } else if (v.getId() == binding.openInBrowser.getId()) {
@ -3724,7 +3725,7 @@ public final class Player implements
if (DEBUG) { if (DEBUG) {
Log.i(TAG, "Failed to start kore", e); Log.i(TAG, "Failed to start kore", e);
} }
KoreUtil.showInstallKoreDialog(getParentActivity()); KoreUtils.showInstallKoreDialog(getParentActivity());
} }
} }
} }

View file

@ -1,6 +1,5 @@
package org.schabi.newpipe.util; package org.schabi.newpipe.util;
import android.content.Context;
import android.text.Layout; import android.text.Layout;
import android.text.Selection; import android.text.Selection;
import android.text.Spannable; import android.text.Spannable;
@ -11,27 +10,14 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.util.external_communication.InternalUrlsHandler;
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 io.reactivex.rxjava3.disposables.CompositeDisposable;
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 class CommentTextOnTouchListener implements View.OnTouchListener {
public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener(); public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener();
private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("(.*)#timestamp=(\\d+)");
@Override @Override
public boolean onTouch(final View v, final MotionEvent event) { public boolean onTouch(final View v, final MotionEvent event) {
if (!(v instanceof TextView)) { if (!(v instanceof TextView)) {
@ -64,13 +50,12 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
if (link.length != 0) { if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) { if (action == MotionEvent.ACTION_UP) {
boolean handled = false;
if (link[0] instanceof URLSpan) { if (link[0] instanceof URLSpan) {
handled = handleUrl(v.getContext(), (URLSpan) link[0]); final String url = ((URLSpan) link[0]).getURL();
if (!InternalUrlsHandler.handleUrlCommentsTimestamp(
new CompositeDisposable(), v.getContext(), url)) {
ShareUtils.openUrlInBrowser(v.getContext(), url, false);
} }
if (!handled) {
ShareUtils.openUrlInBrowser(v.getContext(),
((URLSpan) link[0]).getURL(), false);
} }
} else if (action == MotionEvent.ACTION_DOWN) { } else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer, Selection.setSelection(buffer,
@ -83,52 +68,4 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
} }
return false; 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;
}
} }

View file

@ -30,6 +30,7 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.util.external_communication.TextLinkifier;
import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
@ -54,7 +55,7 @@ import java.util.List;
import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
@ -268,18 +269,19 @@ public final class ExtractorHelper {
* @param metaInfos a list of meta information, can be null or empty * @param metaInfos a list of meta information, can be null or empty
* @param metaInfoTextView the text view in which to show the formatted HTML * @param metaInfoTextView the text view in which to show the formatted HTML
* @param metaInfoSeparator another view to be shown or hidden accordingly to the text view * @param metaInfoSeparator another view to be shown or hidden accordingly to the text view
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed * @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<MetaInfo> metaInfos, public static void showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInfos,
final TextView metaInfoTextView, final TextView metaInfoTextView,
final View metaInfoSeparator) { final View metaInfoSeparator,
final CompositeDisposable disposables) {
final Context context = metaInfoTextView.getContext(); final Context context = metaInfoTextView.getContext();
if (metaInfos == null || metaInfos.isEmpty() if (metaInfos == null || metaInfos.isEmpty()
|| !PreferenceManager.getDefaultSharedPreferences(context).getBoolean( || !PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
context.getString(R.string.show_meta_info_key), true)) { context.getString(R.string.show_meta_info_key), true)) {
metaInfoTextView.setVisibility(View.GONE); metaInfoTextView.setVisibility(View.GONE);
metaInfoSeparator.setVisibility(View.GONE); metaInfoSeparator.setVisibility(View.GONE);
return Disposable.empty();
} else { } else {
final StringBuilder stringBuilder = new StringBuilder(); final StringBuilder stringBuilder = new StringBuilder();
@ -310,8 +312,8 @@ public final class ExtractorHelper {
} }
metaInfoSeparator.setVisibility(View.VISIBLE); metaInfoSeparator.setVisibility(View.VISIBLE);
return TextLinkifier.createLinksFromHtmlBlock(context, stringBuilder.toString(), TextLinkifier.createLinksFromHtmlBlock(metaInfoTextView, stringBuilder.toString(),
metaInfoTextView, HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING); HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING, null, disposables);
} }
} }

View file

@ -53,10 +53,11 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList; import java.util.ArrayList;
import static org.schabi.newpipe.util.ShareUtils.installApp; import static org.schabi.newpipe.util.external_communication.ShareUtils.installApp;
public final class NavigationHelper { public final class NavigationHelper {
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag"; public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
@ -252,7 +253,7 @@ public final class NavigationHelper {
public static void resolveActivityOrAskToInstall(final Context context, final Intent intent) { public static void resolveActivityOrAskToInstall(final Context context, final Intent intent) {
if (intent.resolveActivity(context.getPackageManager()) != null) { if (intent.resolveActivity(context.getPackageManager()) != null) {
ShareUtils.openIntentInApp(context, intent); ShareUtils.openIntentInApp(context, intent, false);
} else { } else {
if (context instanceof Activity) { if (context instanceof Activity) {
new AlertDialog.Builder(context) new AlertDialog.Builder(context)

View file

@ -1,229 +0,0 @@
package org.schabi.newpipe.util;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R;
public final class ShareUtils {
private ShareUtils() {
}
/**
* Open an Intent to install an app.
* <p>
* This method tries to open the default app market with the package id passed as the
* 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.
* <p>
* 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.
*
* @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
final boolean marketSchemeResult = openIntentInApp(context, new Intent(Intent.ACTION_VIEW,
Uri.parse("market://details?id=" + packageId))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
if (!marketSchemeResult) {
// Fall back to Google Play Store Web URL (F-Droid can handle it)
openUrlInBrowser(context,
"https://play.google.com/store/apps/details?id=" + packageId, false);
}
}
/**
* Open the url with the system default browser.
* <p>
* If no browser is set as default, fallbacks to
* {@link ShareUtils#openAppChooser(Context, Intent, String)}
*
* @param context the context to use
* @param url the url to browse
* @param httpDefaultBrowserTest the boolean to set if the test for the default browser will be
* 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,
final boolean httpDefaultBrowserTest) {
final String defaultPackageName;
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (httpDefaultBrowserTest) {
defaultPackageName = getDefaultAppPackageName(context, new Intent(Intent.ACTION_VIEW,
Uri.parse("http://")).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} else {
defaultPackageName = getDefaultAppPackageName(context, intent);
}
if (defaultPackageName.equals("android")) {
// No browser set as default (doesn't work on some devices)
openAppChooser(context, intent, context.getString(R.string.open_with));
} else {
if (defaultPackageName.isEmpty()) {
// No app installed to open a web url
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 a browser but an app chooser because of OEMs changes
intent.setPackage(null);
openAppChooser(context, intent, context.getString(R.string.open_with));
}
}
}
return true;
}
/**
* Open the url with the system default browser.
* <p>
* If no browser is set as default, fallbacks to
* {@link ShareUtils#openAppChooser(Context, Intent, String)}
* <p>
* This calls {@link ShareUtils#openUrlInBrowser(Context, String, boolean)} with true
* for the boolean parameter
*
* @param context the context to use
* @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) {
return openUrlInBrowser(context, url, true);
}
/**
* Open an intent with the system default app.
* <p>
* The intent can be of every type, excepted a web intent for which
* {@link ShareUtils#openUrlInBrowser(Context, String, boolean)} should be used.
* <p>
* If no app is set as default, fallbacks to
* {@link ShareUtils#openAppChooser(Context, Intent, String)}
*
* @param context the context to use
* @param intent the intent to open
* @return true if the intent can be opened or false if it cannot be
*/
public static boolean openIntentInApp(final Context context, final Intent intent) {
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));
} 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();
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, context.getString(R.string.open_with));
}
}
}
return true;
}
/**
* Open the system chooser to launch an intent.
* <p>
* 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.
*
* @param context the context to use
* @param intent the intent to open
* @param chooserStringTitle the string of chooser's title
*/
private static void openAppChooser(final Context context, final Intent intent,
final String chooserStringTitle) {
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);
context.startActivity(chooserIntent);
}
/**
* Get the default app package name.
* <p>
* If no app is set as default, it will return "android" (not on some devices because some
* OEMs changed the app chooser).
* <p>
* If no app is installed on user's device to handle the intent, it will return an empty string.
*
* @param context the context to use
* @param intent the intent to get default app
* @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) {
final ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent,
PackageManager.MATCH_DEFAULT_ONLY);
if (resolveInfo == null) {
return "";
} else {
return resolveInfo.activityInfo.packageName;
}
}
/**
* 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
*/
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);
shareIntent.putExtra(Intent.EXTRA_TEXT, url);
openAppChooser(context, shareIntent, context.getString(R.string.share_dialog_title));
}
/**
* Copy the text to clipboard, and indicate to the user whether the operation was completed
* successfully using a Toast.
*
* @param context the context to use
* @param text the text to copy
*/
public static void copyToClipboard(final Context context, final String text) {
final ClipboardManager clipboardManager =
ContextCompat.getSystemService(context, ClipboardManager.class);
if (clipboardManager == null) {
Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_LONG).show();
return;
}
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text));
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
}
}

View file

@ -12,6 +12,8 @@ import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -83,12 +85,13 @@ public enum StreamDialogEntry {
try { try {
NavigationHelper.playWithKore(fragment.requireContext(), videoUrl); NavigationHelper.playWithKore(fragment.requireContext(), videoUrl);
} catch (final Exception e) { } catch (final Exception e) {
KoreUtil.showInstallKoreDialog(fragment.getActivity()); KoreUtils.showInstallKoreDialog(fragment.getActivity());
} }
}), }),
share(R.string.share, (fragment, item) -> 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) -> open_in_browser(R.string.open_in_browser, (fragment, item) ->
ShareUtils.openUrlInBrowser(fragment.getContext(), item.getUrl())); ShareUtils.openUrlInBrowser(fragment.getContext(), item.getUrl()));

View file

@ -1,145 +0,0 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.text.SpannableStringBuilder;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.text.HtmlCompat;
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.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class TextLinkifier {
public static final String TAG = TextLinkifier.class.getSimpleName();
private TextLinkifier() {
}
/**
* Create web links for contents with an HTML description.
* <p>
* This will call
* {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView)}
* 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
* @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 int htmlCompatFlag) {
return changeIntentsOfDescriptionLinks(context,
HtmlCompat.fromHtml(htmlBlock, htmlCompatFlag), textView);
}
/**
* Create web links for contents with a plain text description.
* <p>
* This will call
* {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView)}
* 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
* @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) {
textView.setAutoLinkMask(Linkify.WEB_URLS);
textView.setText(plainTextBlock, TextView.BufferType.SPANNABLE);
return changeIntentsOfDescriptionLinks(context, textView.getText(), textView);
}
/**
* Create web links for contents with a markdown description.
* <p>
* This will call
* {@link TextLinkifier#changeIntentsOfDescriptionLinks(Context, CharSequence, TextView)}
* 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
* @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 Markwon markwon = Markwon.builder(context).usePlugin(LinkifyPlugin.create()).build();
markwon.setMarkdown(textView, markdownBlock);
return changeIntentsOfDescriptionLinks(context, textView.getText(), textView);
}
/**
* Change links generated by libraries in the description of a content to a custom link action.
* <p>
* 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)}.
* <p>
* 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
* @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) {
return Single.fromCallable(() -> {
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) {
ShareUtils.openUrlInBrowser(context, span.getURL(), false);
}
};
textBlockLinked.setSpan(clickableSpan, textBlockLinked.getSpanStart(span),
textBlockLinked.getSpanEnd(span), textBlockLinked.getSpanFlags(span));
textBlockLinked.removeSpan(span);
}
return textBlockLinked;
}).subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
textBlockLinked -> setTextViewCharSequence(textView, textBlockLinked),
throwable -> {
Log.e(TAG, "Unable to linkify text", throwable);
// this should never happen, but if it does, just fallback to it
setTextViewCharSequence(textView, chars);
});
}
private static void setTextViewCharSequence(final TextView textView,
final CharSequence charSequence) {
textView.setText(charSequence);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setVisibility(View.VISIBLE);
}
}

View file

@ -0,0 +1,154 @@
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;
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 org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper;
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.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class InternalUrlsHandler {
private static final Pattern AMPERSAND_TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)");
private static final Pattern HASHTAG_TIMESTAMP_PATTERN =
Pattern.compile("(.*)#timestamp=(\\d+)");
private InternalUrlsHandler() {
}
/**
* Handle a YouTube timestamp comment URL in NewPipe.
* <p>
* 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 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(@NonNull final CompositeDisposable
disposables,
final Context context,
@NonNull final String url) {
return handleUrl(context, url, HASHTAG_TIMESTAMP_PATTERN, disposables);
}
/**
* Handle a YouTube timestamp description URL in NewPipe.
* <p>
* 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 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(@NonNull final CompositeDisposable
disposables,
final Context context,
@NonNull final String url) {
return handleUrl(context, url, AMPERSAND_TIMESTAMP_PATTERN, disposables);
}
/**
* Handle an URL in NewPipe.
* <p>
* 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 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 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(final Context context,
@NonNull final String url,
@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;
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.STREAM && seconds != -1) {
return playOnPopup(context, matchedUrl, service, seconds, disposables);
} 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
* @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(final Context context,
final String url,
@NonNull final StreamingService service,
final int seconds,
@NonNull final CompositeDisposable disposables) {
final LinkHandlerFactory factory = service.getStreamLHFactory();
final String cleanUrl;
try {
cleanUrl = factory.getUrl(factory.getId(url));
} catch (final ParsingException e) {
return false;
}
final Single<StreamInfo> single
= ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false);
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;
}
}

View file

@ -1,28 +1,31 @@
package org.schabi.newpipe.util; package org.schabi.newpipe.util.external_communication;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.util.NavigationHelper;
public final class KoreUtil { public final class KoreUtils {
private KoreUtil() { } private KoreUtils() { }
public static boolean isServiceSupportedByKore(final int serviceId) { public static boolean isServiceSupportedByKore(final int serviceId) {
return (serviceId == ServiceList.YouTube.getServiceId() return (serviceId == ServiceList.YouTube.getServiceId()
|| serviceId == ServiceList.SoundCloud.getServiceId()); || 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) return isServiceSupportedByKore(serviceId)
&& PreferenceManager.getDefaultSharedPreferences(context) && PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_play_with_kodi_key), false); .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); final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(R.string.kore_not_found) builder.setMessage(R.string.kore_not_found)
.setPositiveButton(R.string.install, (dialog, which) -> .setPositiveButton(R.string.install, (dialog, which) ->

View file

@ -0,0 +1,302 @@
package org.schabi.newpipe.util.external_communication;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
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.annotation.NonNull;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R;
public final class ShareUtils {
private ShareUtils() {
}
/**
* Open an Intent to install an app.
* <p>
* This method tries to open the default app market with the package id passed as the
* 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.
* <p>
* 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(@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))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), false);
if (!marketSchemeResult) {
// Fall back to Google Play Store Web URL (F-Droid can handle it)
openUrlInBrowser(context,
"https://play.google.com/store/apps/details?id=" + packageId, false);
}
}
/**
* Open the url with the system default browser.
* <p>
* If no browser is set as default, fallbacks to
* {@link #openAppChooser(Context, Intent, boolean)}
*
* @param context the context to use
* @param url the url to browse
* @param httpDefaultBrowserTest the boolean to set if the test for the default browser will be
* 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(@NonNull final Context context,
final String url,
final boolean httpDefaultBrowserTest) {
final String defaultPackageName;
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (httpDefaultBrowserTest) {
defaultPackageName = getDefaultAppPackageName(context, new Intent(Intent.ACTION_VIEW,
Uri.parse("http://")).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} else {
defaultPackageName = getDefaultAppPackageName(context, intent);
}
if (defaultPackageName.equals("android")) {
// No browser set as default (doesn't work on some devices)
openAppChooser(context, intent, true);
} else {
if (defaultPackageName.isEmpty()) {
// No app installed to open a web url
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 a browser but an app chooser because of OEMs changes
intent.setPackage(null);
openAppChooser(context, intent, true);
}
}
}
return true;
}
/**
* Open the url with the system default browser.
* <p>
* If no browser is set as default, fallbacks to
* {@link #openAppChooser(Context, Intent, boolean)}
* <p>
* This calls {@link #openUrlInBrowser(Context, String, boolean)} with true
* for the boolean parameter
*
* @param context the context to use
* @param url the url to browse
* @return true if the URL can be opened or false if it cannot be
**/
public static boolean openUrlInBrowser(@NonNull final Context context, final String url) {
return openUrlInBrowser(context, url, true);
}
/**
* Open an intent with the system default app.
* <p>
* The intent can be of every type, excepted a web intent for which
* {@link #openUrlInBrowser(Context, String, boolean)} should be used.
* <p>
* 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
* @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(@NonNull final Context context,
@NonNull final Intent intent,
final boolean showToast) {
final String defaultPackageName = getDefaultAppPackageName(context, intent);
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;
}
/**
* Open the system chooser to launch an intent.
* <p>
* This method opens an {@link android.content.Intent#ACTION_CHOOSER} of the intent putted
* 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 setTitleChooser set the title "Open with" to the chooser if true, else not
*/
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);
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
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);
}
/**
* Get the default app package name.
* <p>
* If no app is set as default, it will return "android" (not on some devices because some
* OEMs changed the app chooser).
* <p>
* If no app is installed on user's device to handle the intent, it will return an empty string.
*
* @param context the context to use
* @param intent the intent to get default app
* @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(@NonNull final Context context,
@NonNull final Intent intent) {
final ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent,
PackageManager.MATCH_DEFAULT_ONLY);
if (resolveInfo == null) {
return "";
} else {
return resolveInfo.activityInfo.packageName;
}
}
/**
* 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.
* Support sharing the image of the content needs to done, if possible.
*
* @param context the context to use
* @param title the title of the content
* @param content the content to share
* @param imagePreviewUrl the image of the subject
*/
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);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT, content);
if (!title.isEmpty()) {
shareIntent.putExtra(Intent.EXTRA_TITLE, title);
}
/* 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).
if (!imagePreviewUrl.isEmpty()) {
//shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}*/
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.
* <p>
* 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(@NonNull final Context context,
@NonNull 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.
*
* @param context the context to use
* @param text the text to copy
*/
public static void copyToClipboard(@NonNull final Context context, final String text) {
final ClipboardManager clipboardManager =
ContextCompat.getSystemService(context, ClipboardManager.class);
if (clipboardManager == null) {
Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_LONG).show();
return;
}
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text));
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
}
}

View file

@ -0,0 +1,287 @@
package org.schabi.newpipe.util.external_communication;
import android.content.Context;
import android.text.SpannableStringBuilder;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.util.Log;
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.Info;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.util.NavigationHelper;
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;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.util.external_communication.InternalUrlsHandler.playOnPopup;
public final class TextLinkifier {
public static final String TAG = TextLinkifier.class.getSimpleName();
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() {
}
/**
* Create web links for contents with an HTML description.
* <p>
* 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
* @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
* @param disposables disposables created by the method are added here and their lifecycle
* should be handled by the calling class
*/
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.
* <p>
* 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
* @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
* @param disposables disposables created by the method are added here and their lifecycle
* should be handled by the calling class
*/
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);
changeIntentsOfDescriptionLinks(textView, textView.getText(), relatedInfo, disposables);
}
/**
* Create web links for contents with a markdown description.
* <p>
* 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
* @param disposables disposables created by the method are added here and their lifecycle
* should be handled by the calling class
*/
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();
changeIntentsOfDescriptionLinks(textView, markwon.toMarkdown(markdownBlock), relatedInfo,
disposables);
}
/**
* Add click listeners which opens a search on hashtags in a plain text.
* <p>
* 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 relatedInfo used to search for the term in the correct service
*/
private static void addClickListenersOnHashtags(final Context context,
@NonNull final SpannableStringBuilder
spannableDescription,
final Info relatedInfo) {
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);
// 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, relatedInfo.getServiceId(),
parsedHashtag);
}
}, hashtagStart, hashtagEnd, 0);
}
}
}
/**
* Add click listeners which opens the popup player on timestamps in a plain text.
* <p>
* 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 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 CompositeDisposable disposables) {
final String descriptionText = spannableDescription.toString();
final Matcher timestampsMatches = TIMESTAMPS_PATTERN.matcher(descriptionText);
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 seconds;
if (timestampParts.length == 3) { // timestamp format: XX:XX:XX
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
seconds = Integer.parseInt(timestampParts[0]) * 60 // minutes
+ Integer.parseInt(timestampParts[1]); // seconds
} else {
continue;
}
spannableDescription.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull final View view) {
playOnPopup(context, relatedInfo.getUrl(), relatedInfo.getService(), seconds,
disposables);
}
}, timestampStart, timestampEnd, 0);
}
}
/**
* 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.
* <p>
* 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, 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.
* <p>
* This method is required in order to intercept links and e.g. show a confirmation dialog
* before opening a web link.
*
* @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
* @param disposables disposables created by the method are added here and their lifecycle
* should be handled by the calling class
*/
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
final SpannableStringBuilder textBlockLinked = new SpannableStringBuilder(chars);
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.handleUrlDescriptionTimestamp(
new CompositeDisposable(), context, url)) {
ShareUtils.openUrlInBrowser(context, url, false);
}
}
};
textBlockLinked.setSpan(clickableSpan, textBlockLinked.getSpanStart(span),
textBlockLinked.getSpanEnd(span), textBlockLinked.getSpanFlags(span));
textBlockLinked.removeSpan(span);
}
// add click actions on plain text timestamps only for description of contents,
// unneeded for meta-info or other TextViews
if (relatedInfo != null) {
if (relatedInfo instanceof StreamInfo) {
addClickListenersOnTimestamps(context, textBlockLinked, relatedInfo,
disposables);
}
addClickListenersOnHashtags(context, textBlockLinked, relatedInfo);
}
return textBlockLinked;
}).subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
textBlockLinked -> setTextViewCharSequence(textView, textBlockLinked),
throwable -> {
Log.e(TAG, "Unable to linkify text", throwable);
// this should never happen, but if it does, just fallback to it
setTextViewCharSequence(textView, chars);
}));
}
private static void setTextViewCharSequence(@NonNull final TextView textView,
final CharSequence charSequence) {
textView.setText(charSequence);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setVisibility(View.VISIBLE);
}
}

View file

@ -2,7 +2,6 @@ package us.shandian.giga.ui.adapter;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Color; import android.graphics.Color;
@ -45,7 +44,7 @@ import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.io.File; import java.io.File;
import java.net.URI; import java.net.URI;
@ -348,10 +347,8 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
final Uri uri = resolveShareableUri(mission);
Intent intent = new Intent(Intent.ACTION_VIEW); Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mimeType); intent.setDataAndType(resolveShareableUri(mission), mimeType);
intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@ -361,10 +358,8 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
intent.addFlags(FLAG_ACTIVITY_NEW_TASK); intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
} }
//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (intent.resolveActivity(mContext.getPackageManager()) != null) { if (intent.resolveActivity(mContext.getPackageManager()) != null) {
ShareUtils.openIntentInApp(mContext, intent); ShareUtils.openIntentInApp(mContext, intent, false);
} else { } else {
Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG).show(); Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG).show();
} }
@ -377,19 +372,18 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
shareIntent.setType(resolveMimeType(mission)); shareIntent.setType(resolveMimeType(mission));
shareIntent.putExtra(Intent.EXTRA_STREAM, resolveShareableUri(mission)); shareIntent.putExtra(Intent.EXTRA_STREAM, resolveShareableUri(mission));
shareIntent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); shareIntent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
final Intent intent = new Intent(Intent.ACTION_CHOOSER); final Intent intent = new Intent(Intent.ACTION_CHOOSER);
intent.putExtra(Intent.EXTRA_INTENT, shareIntent); 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)); 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);
} }
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
mContext.startActivity(intent);
} }
/** /**