mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	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:
		| @@ -129,13 +129,8 @@ 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)); | ||||
|  | ||||
|             final Intent intent = new Intent(Intent.ACTION_CHOOSER); | ||||
|             intent.putExtra(Intent.EXTRA_INTENT, viewIntent); | ||||
|             intent.putExtra(Intent.EXTRA_TITLE, R.string.open_with); | ||||
|             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); | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -17,8 +17,8 @@ import org.schabi.newpipe.R | ||||
| import org.schabi.newpipe.databinding.ActivityAboutBinding | ||||
| import org.schabi.newpipe.databinding.FragmentAboutBinding | ||||
| 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 | ||||
|  | ||||
| class AboutActivity : AppCompatActivity() { | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|   | ||||
| @@ -19,7 +19,6 @@ import java.util.Objects | ||||
|  */ | ||||
| class LicenseFragment : Fragment() { | ||||
|     private lateinit var softwareComponents: Array<SoftwareComponent> | ||||
|     private var componentForContextMenu: SoftwareComponent? = null | ||||
|     private var activeLicense: License? = null | ||||
|     private val compositeDisposable = CompositeDisposable() | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
| @@ -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; | ||||
| @@ -220,13 +221,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 | ||||
|   | ||||
| @@ -23,15 +23,15 @@ 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; | ||||
| 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(requireContext(), | ||||
|                         description.getContent(), binding.detailDescriptionView, | ||||
|                         HtmlCompat.FROM_HTML_MODE_LEGACY); | ||||
|                 TextLinkifier.createLinksFromHtmlBlock(binding.detailDescriptionView, | ||||
|                         description.getContent(), HtmlCompat.FROM_HTML_MODE_LEGACY, streamInfo, | ||||
|                         descriptionDisposables); | ||||
|                 break; | ||||
|             case Description.MARKDOWN: | ||||
|                 descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(), | ||||
|                         description.getContent(), binding.detailDescriptionView); | ||||
|                 TextLinkifier.createLinksFromMarkdownText(binding.detailDescriptionView, | ||||
|                         description.getContent(), streamInfo, descriptionDisposables); | ||||
|                 break; | ||||
|             case Description.PLAIN_TEXT: default: | ||||
|                 descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(), | ||||
|                         description.getContent(), binding.detailDescriptionView); | ||||
|                 TextLinkifier.createLinksFromPlainText(binding.detailDescriptionView, | ||||
|                         description.getContent(), streamInfo, descriptionDisposables); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
| @@ -198,8 +195,8 @@ public class DescriptionFragment extends BaseFragment { | ||||
|         }); | ||||
|  | ||||
|         if (linkifyContent) { | ||||
|             TextLinkifier.createLinksFromPlainText(requireContext(), | ||||
|                     content, itemBinding.metadataContentView); | ||||
|             TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, null, | ||||
|                     descriptionDisposables); | ||||
|         } else { | ||||
|             itemBinding.metadataContentView.setText(content); | ||||
|         } | ||||
|   | ||||
| @@ -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; | ||||
| @@ -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: | ||||
| @@ -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); | ||||
| @@ -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()); | ||||
|   | ||||
| @@ -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<I, N> extends BaseStateFragment<I> | ||||
|             )); | ||||
|         } | ||||
|         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())) { | ||||
|   | ||||
| @@ -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; | ||||
| @@ -203,7 +203,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | ||||
|                 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: | ||||
|   | ||||
| @@ -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<PlaylistInfo> { | ||||
|             )); | ||||
|         } | ||||
|         entries.add(StreamDialogEntry.open_in_browser); | ||||
|         if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) { | ||||
|         if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) { | ||||
|             entries.add(StreamDialogEntry.play_with_kodi); | ||||
|         } | ||||
|  | ||||
| @@ -251,7 +251,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { | ||||
|                 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(); | ||||
|   | ||||
| @@ -278,8 +278,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | ||||
|  | ||||
|         handleSearchSuggestion(); | ||||
|  | ||||
|         disposables.add(showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo), | ||||
|                     searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator)); | ||||
|         showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo), | ||||
|                 searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator, | ||||
|                 disposables); | ||||
|  | ||||
|         if (TextUtils.isEmpty(searchString) || wasSearchFocused) { | ||||
|             showKeyboardSearch(); | ||||
| @@ -841,7 +842,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I | ||||
|         infoListAdapter.clearStreamItemList(); | ||||
|         hideSuggestionsPanel(); | ||||
|         showMetaInfoInTextView(null, searchBinding.searchMetaInfoTextView, | ||||
|                 searchBinding.searchMetaInfoSeparator); | ||||
|                 searchBinding.searchMetaInfoSeparator, disposables); | ||||
|         hideKeyboardSearch(); | ||||
|  | ||||
|         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 | ||||
|         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(); | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -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<List<PlaylistSt | ||||
|             )); | ||||
|         } | ||||
|         entries.add(StreamDialogEntry.open_in_browser); | ||||
|         if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { | ||||
|         if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { | ||||
|             entries.add(StreamDialogEntry.play_with_kodi); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -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.PREVIOUS_EXPORT_MODE | ||||
| 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.OnClickGesture | ||||
| import org.schabi.newpipe.util.ShareUtils | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Date | ||||
| import java.util.Locale | ||||
| @@ -293,7 +293,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() { | ||||
|  | ||||
|         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) | ||||
|             } | ||||
|   | ||||
| @@ -47,7 +47,7 @@ import java.util.List; | ||||
|  | ||||
| import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; | ||||
| 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 | ||||
|         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, | ||||
|                 Menu.NONE, R.string.share); | ||||
|         share.setOnMenuItemClickListener(menuItem -> { | ||||
|             shareText(getApplicationContext(), item.getTitle(), item.getUrl()); | ||||
|             shareText(getApplicationContext(), item.getTitle(), item.getUrl(), | ||||
|                     item.getThumbnailUrl()); | ||||
|             return true; | ||||
|         }); | ||||
|  | ||||
|   | ||||
| @@ -123,11 +123,11 @@ import org.schabi.newpipe.player.resolver.MediaSourceTag; | ||||
| import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; | ||||
| import org.schabi.newpipe.util.DeviceUtils; | ||||
| 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.NavigationHelper; | ||||
| 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 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 | ||||
|         binding.playWithKodi.setVisibility(videoPlayerSelected() | ||||
|                 && playQueue != null && playQueue.getItem() != null | ||||
|                 && KoreUtil.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId()) | ||||
|                 && KoreUtils.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId()) | ||||
|                 ? View.VISIBLE : View.GONE); | ||||
|     } | ||||
|     //endregion | ||||
| @@ -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()) { | ||||
| @@ -3724,7 +3725,7 @@ public final class Player implements | ||||
|                 if (DEBUG) { | ||||
|                     Log.i(TAG, "Failed to start kore", e); | ||||
|                 } | ||||
|                 KoreUtil.showInstallKoreDialog(getParentActivity()); | ||||
|                 KoreUtils.showInstallKoreDialog(getParentActivity()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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,14 @@ 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 org.schabi.newpipe.util.external_communication.ShareUtils; | ||||
| import org.schabi.newpipe.util.external_communication.InternalUrlsHandler; | ||||
|  | ||||
| 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; | ||||
| import io.reactivex.rxjava3.disposables.CompositeDisposable; | ||||
|  | ||||
| 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)) { | ||||
| @@ -64,13 +50,12 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { | ||||
|  | ||||
|                 if (link.length != 0) { | ||||
|                     if (action == MotionEvent.ACTION_UP) { | ||||
|                         boolean handled = false; | ||||
|                         if (link[0] instanceof URLSpan) { | ||||
|                             handled = handleUrl(v.getContext(), (URLSpan) link[0]); | ||||
|                         } | ||||
|                         if (!handled) { | ||||
|                             ShareUtils.openUrlInBrowser(v.getContext(), | ||||
|                                     ((URLSpan) link[0]).getURL(), false); | ||||
|                             final String url = ((URLSpan) link[0]).getURL(); | ||||
|                             if (!InternalUrlsHandler.handleUrlCommentsTimestamp( | ||||
|                                     new CompositeDisposable(), v.getContext(), url)) { | ||||
|                                 ShareUtils.openUrlInBrowser(v.getContext(), url, false); | ||||
|                             } | ||||
|                         } | ||||
|                     } else if (action == MotionEvent.ACTION_DOWN) { | ||||
|                         Selection.setSelection(buffer, | ||||
| @@ -83,52 +68,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; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -30,6 +30,7 @@ import androidx.preference.PreferenceManager; | ||||
|  | ||||
| import org.schabi.newpipe.MainActivity; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.util.external_communication.TextLinkifier; | ||||
| import org.schabi.newpipe.extractor.Info; | ||||
| import org.schabi.newpipe.extractor.InfoItem; | ||||
| 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.Single; | ||||
| import io.reactivex.rxjava3.disposables.Disposable; | ||||
| import io.reactivex.rxjava3.disposables.CompositeDisposable; | ||||
|  | ||||
| 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 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<MetaInfo> metaInfos, | ||||
|                                                     final TextView metaInfoTextView, | ||||
|                                                     final View metaInfoSeparator) { | ||||
|     public static void showMetaInfoInTextView(@Nullable final List<MetaInfo> 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(); | ||||
| @@ -310,8 +312,8 @@ public final class ExtractorHelper { | ||||
|             } | ||||
|  | ||||
|             metaInfoSeparator.setVisibility(View.VISIBLE); | ||||
|             return TextLinkifier.createLinksFromHtmlBlock(context, stringBuilder.toString(), | ||||
|                     metaInfoTextView, HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING); | ||||
|             TextLinkifier.createLinksFromHtmlBlock(metaInfoTextView, stringBuilder.toString(), | ||||
|                     HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING, null, disposables); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -53,10 +53,11 @@ import org.schabi.newpipe.player.helper.PlayerHolder; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| import org.schabi.newpipe.settings.SettingsActivity; | ||||
| import org.schabi.newpipe.util.external_communication.ShareUtils; | ||||
|  | ||||
| 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 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) { | ||||
|         if (intent.resolveActivity(context.getPackageManager()) != null) { | ||||
|             ShareUtils.openIntentInApp(context, intent); | ||||
|             ShareUtils.openIntentInApp(context, intent, false); | ||||
|         } else { | ||||
|             if (context instanceof Activity) { | ||||
|                 new AlertDialog.Builder(context) | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -12,6 +12,8 @@ import org.schabi.newpipe.local.dialog.PlaylistCreationDialog; | ||||
| import org.schabi.newpipe.player.MainPlayer; | ||||
| import org.schabi.newpipe.player.helper.PlayerHolder; | ||||
| 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.List; | ||||
| @@ -83,12 +85,13 @@ public enum StreamDialogEntry { | ||||
|         try { | ||||
|             NavigationHelper.playWithKore(fragment.requireContext(), videoUrl); | ||||
|         } catch (final Exception e) { | ||||
|             KoreUtil.showInstallKoreDialog(fragment.getActivity()); | ||||
|             KoreUtils.showInstallKoreDialog(fragment.getActivity()); | ||||
|         } | ||||
|     }), | ||||
|  | ||||
|     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())); | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
| @@ -1,28 +1,31 @@ | ||||
| package org.schabi.newpipe.util; | ||||
| package org.schabi.newpipe.util.external_communication; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.appcompat.app.AlertDialog; | ||||
| import androidx.preference.PreferenceManager; | ||||
| 
 | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.ServiceList; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
| 
 | ||||
| public final class KoreUtil { | ||||
|     private KoreUtil() { } | ||||
| public final class KoreUtils { | ||||
|     private KoreUtils() { } | ||||
| 
 | ||||
|     public static boolean isServiceSupportedByKore(final int serviceId) { | ||||
|         return (serviceId == ServiceList.YouTube.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) | ||||
|                 && 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) -> | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -2,7 +2,6 @@ package us.shandian.giga.ui.adapter; | ||||
|  | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.NotificationManager; | ||||
| import android.content.ActivityNotFoundException; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| 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.UserAction; | ||||
| 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.net.URI; | ||||
| @@ -348,10 +347,8 @@ public class MissionAdapter extends Adapter<ViewHolder> 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 +358,8 @@ public class MissionAdapter extends Adapter<ViewHolder> 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 +372,18 @@ public class MissionAdapter extends Adapter<ViewHolder> 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); | ||||
|         // 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.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|         intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); | ||||
|  | ||||
|         mContext.startActivity(intent); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Stypox
					Stypox