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:
commit
92910eb227
29 changed files with 851 additions and 548 deletions
|
@ -129,13 +129,8 @@ public final class CheckForNewAppVersion {
|
||||||
|
|
||||||
if (BuildConfig.VERSION_CODE < versionCode) {
|
if (BuildConfig.VERSION_CODE < versionCode) {
|
||||||
// A pending intent to open the apk location url in the browser.
|
// A pending intent to open the apk location url in the browser.
|
||||||
final Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||||
|
|
||||||
final Intent intent = new Intent(Intent.ACTION_CHOOSER);
|
|
||||||
intent.putExtra(Intent.EXTRA_INTENT, viewIntent);
|
|
||||||
intent.putExtra(Intent.EXTRA_TITLE, R.string.open_with);
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
|
||||||
final PendingIntent pendingIntent
|
final PendingIntent pendingIntent
|
||||||
= PendingIntent.getActivity(application, 0, intent, 0);
|
= PendingIntent.getActivity(application, 0, intent, 0);
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.urlfinder.UrlFinder;
|
import org.schabi.newpipe.util.urlfinder.UrlFinder;
|
||||||
import org.schabi.newpipe.views.FocusOverlayView;
|
import org.schabi.newpipe.views.FocusOverlayView;
|
||||||
|
|
|
@ -17,8 +17,8 @@ import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.databinding.ActivityAboutBinding
|
import org.schabi.newpipe.databinding.ActivityAboutBinding
|
||||||
import org.schabi.newpipe.databinding.FragmentAboutBinding
|
import org.schabi.newpipe.databinding.FragmentAboutBinding
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.ShareUtils
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
class AboutActivity : AppCompatActivity() {
|
class AboutActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
|
@ -19,7 +19,6 @@ import java.util.Objects
|
||||||
*/
|
*/
|
||||||
class LicenseFragment : Fragment() {
|
class LicenseFragment : Fragment() {
|
||||||
private lateinit var softwareComponents: Array<SoftwareComponent>
|
private lateinit var softwareComponents: Array<SoftwareComponent>
|
||||||
private var componentForContextMenu: SoftwareComponent? = null
|
|
||||||
private var activeLicense: License? = null
|
private var activeLicense: License? = null
|
||||||
private val compositeDisposable = CompositeDisposable()
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,8 @@ import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.ShareUtils
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
|
|
|
@ -27,7 +27,7 @@ import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
@ -195,7 +195,8 @@ public class ErrorActivity extends AppCompatActivity {
|
||||||
onBackPressed();
|
onBackPressed();
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_share_error:
|
case R.id.menu_item_share_error:
|
||||||
ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson());
|
ShareUtils.shareText(getApplicationContext(),
|
||||||
|
getString(R.string.error_report_title), buildJson());
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
@ -220,13 +221,10 @@ public class ErrorActivity extends AppCompatActivity {
|
||||||
+ getString(R.string.app_name) + " "
|
+ getString(R.string.app_name) + " "
|
||||||
+ BuildConfig.VERSION_NAME)
|
+ BuildConfig.VERSION_NAME)
|
||||||
.putExtra(Intent.EXTRA_TEXT, buildJson());
|
.putExtra(Intent.EXTRA_TEXT, buildJson());
|
||||||
if (i.resolveActivity(getPackageManager()) != null) {
|
ShareUtils.openIntentInApp(context, i, true);
|
||||||
ShareUtils.openIntentInApp(context, i);
|
|
||||||
}
|
|
||||||
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
|
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
|
||||||
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false);
|
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
.setNegativeButton(R.string.decline, (dialog, which) -> {
|
.setNegativeButton(R.string.decline, (dialog, which) -> {
|
||||||
// do nothing
|
// do nothing
|
||||||
|
|
|
@ -23,15 +23,15 @@ import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.TextLinkifier;
|
import org.schabi.newpipe.util.external_communication.TextLinkifier;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
|
||||||
import static android.text.TextUtils.isEmpty;
|
import static android.text.TextUtils.isEmpty;
|
||||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||||
|
@ -41,8 +41,7 @@ public class DescriptionFragment extends BaseFragment {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
StreamInfo streamInfo = null;
|
StreamInfo streamInfo = null;
|
||||||
@Nullable
|
final CompositeDisposable descriptionDisposables = new CompositeDisposable();
|
||||||
Disposable descriptionDisposable = null;
|
|
||||||
FragmentDescriptionBinding binding;
|
FragmentDescriptionBinding binding;
|
||||||
|
|
||||||
public DescriptionFragment() {
|
public DescriptionFragment() {
|
||||||
|
@ -67,10 +66,8 @@ public class DescriptionFragment extends BaseFragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
|
descriptionDisposables.clear();
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (descriptionDisposable != null) {
|
|
||||||
descriptionDisposable.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -133,17 +130,17 @@ public class DescriptionFragment extends BaseFragment {
|
||||||
final Description description = streamInfo.getDescription();
|
final Description description = streamInfo.getDescription();
|
||||||
switch (description.getType()) {
|
switch (description.getType()) {
|
||||||
case Description.HTML:
|
case Description.HTML:
|
||||||
descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock(requireContext(),
|
TextLinkifier.createLinksFromHtmlBlock(binding.detailDescriptionView,
|
||||||
description.getContent(), binding.detailDescriptionView,
|
description.getContent(), HtmlCompat.FROM_HTML_MODE_LEGACY, streamInfo,
|
||||||
HtmlCompat.FROM_HTML_MODE_LEGACY);
|
descriptionDisposables);
|
||||||
break;
|
break;
|
||||||
case Description.MARKDOWN:
|
case Description.MARKDOWN:
|
||||||
descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(),
|
TextLinkifier.createLinksFromMarkdownText(binding.detailDescriptionView,
|
||||||
description.getContent(), binding.detailDescriptionView);
|
description.getContent(), streamInfo, descriptionDisposables);
|
||||||
break;
|
break;
|
||||||
case Description.PLAIN_TEXT: default:
|
case Description.PLAIN_TEXT: default:
|
||||||
descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(),
|
TextLinkifier.createLinksFromPlainText(binding.detailDescriptionView,
|
||||||
description.getContent(), binding.detailDescriptionView);
|
description.getContent(), streamInfo, descriptionDisposables);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,8 +195,8 @@ public class DescriptionFragment extends BaseFragment {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (linkifyContent) {
|
if (linkifyContent) {
|
||||||
TextLinkifier.createLinksFromPlainText(requireContext(),
|
TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, null,
|
||||||
content, itemBinding.metadataContentView);
|
descriptionDisposables);
|
||||||
} else {
|
} else {
|
||||||
itemBinding.metadataContentView.setText(content);
|
itemBinding.metadataContentView.setText(content);
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,12 +91,12 @@ import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
import org.schabi.newpipe.util.KoreUtil;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -454,8 +454,8 @@ public final class VideoDetailFragment
|
||||||
break;
|
break;
|
||||||
case R.id.detail_controls_share:
|
case R.id.detail_controls_share:
|
||||||
if (currentInfo != null) {
|
if (currentInfo != null) {
|
||||||
ShareUtils.shareText(requireContext(),
|
ShareUtils.shareText(requireContext(), currentInfo.getName(),
|
||||||
currentInfo.getName(), currentInfo.getUrl());
|
currentInfo.getUrl(), currentInfo.getThumbnailUrl());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.detail_controls_open_in_browser:
|
case R.id.detail_controls_open_in_browser:
|
||||||
|
@ -472,7 +472,7 @@ public final class VideoDetailFragment
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.i(TAG, "Failed to start kore", e);
|
Log.i(TAG, "Failed to start kore", e);
|
||||||
}
|
}
|
||||||
KoreUtil.showInstallKoreDialog(requireContext());
|
KoreUtils.showInstallKoreDialog(requireContext());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -631,7 +631,7 @@ public final class VideoDetailFragment
|
||||||
binding.detailControlsShare.setOnClickListener(this);
|
binding.detailControlsShare.setOnClickListener(this);
|
||||||
binding.detailControlsOpenInBrowser.setOnClickListener(this);
|
binding.detailControlsOpenInBrowser.setOnClickListener(this);
|
||||||
binding.detailControlsPlayWithKodi.setOnClickListener(this);
|
binding.detailControlsPlayWithKodi.setOnClickListener(this);
|
||||||
binding.detailControlsPlayWithKodi.setVisibility(KoreUtil.shouldShowPlayWithKodi(
|
binding.detailControlsPlayWithKodi.setVisibility(KoreUtils.shouldShowPlayWithKodi(
|
||||||
requireContext(), serviceId) ? View.VISIBLE : View.GONE);
|
requireContext(), serviceId) ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
binding.overlayThumbnail.setOnClickListener(this);
|
binding.overlayThumbnail.setOnClickListener(this);
|
||||||
|
@ -1546,8 +1546,8 @@ public final class VideoDetailFragment
|
||||||
.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
||||||
updateProgressInfo(info);
|
updateProgressInfo(info);
|
||||||
initThumbnailViews(info);
|
initThumbnailViews(info);
|
||||||
disposables.add(showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||||
binding.detailMetaInfoSeparator));
|
binding.detailMetaInfoSeparator, disposables);
|
||||||
|
|
||||||
if (player == null || player.isStopped()) {
|
if (player == null || player.isStopped()) {
|
||||||
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
|
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
|
||||||
|
|
|
@ -33,7 +33,7 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.util.KoreUtil;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
|
@ -371,7 +371,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
entries.add(StreamDialogEntry.open_in_browser);
|
entries.add(StreamDialogEntry.open_in_browser);
|
||||||
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
||||||
entries.add(StreamDialogEntry.play_with_kodi);
|
entries.add(StreamDialogEntry.play_with_kodi);
|
||||||
}
|
}
|
||||||
if (!isNullOrEmpty(item.getUploaderUrl())) {
|
if (!isNullOrEmpty(item.getUploaderUrl())) {
|
||||||
|
|
|
@ -43,7 +43,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -203,7 +203,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_share:
|
case R.id.menu_item_share:
|
||||||
if (currentInfo != null) {
|
if (currentInfo != null) {
|
||||||
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl());
|
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
|
||||||
|
currentInfo.getAvatarUrl());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -42,10 +42,10 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
import org.schabi.newpipe.util.KoreUtil;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -162,7 +162,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
entries.add(StreamDialogEntry.open_in_browser);
|
entries.add(StreamDialogEntry.open_in_browser);
|
||||||
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
||||||
entries.add(StreamDialogEntry.play_with_kodi);
|
entries.add(StreamDialogEntry.play_with_kodi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,7 +251,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
ShareUtils.openUrlInBrowser(requireContext(), url);
|
ShareUtils.openUrlInBrowser(requireContext(), url);
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_share:
|
case R.id.menu_item_share:
|
||||||
ShareUtils.shareText(requireContext(), name, url);
|
ShareUtils.shareText(requireContext(), name, url, currentInfo.getThumbnailUrl());
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_bookmark:
|
case R.id.menu_item_bookmark:
|
||||||
onBookmarkClicked();
|
onBookmarkClicked();
|
||||||
|
|
|
@ -278,8 +278,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
|
|
||||||
handleSearchSuggestion();
|
handleSearchSuggestion();
|
||||||
|
|
||||||
disposables.add(showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
|
showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
|
||||||
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator));
|
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator,
|
||||||
|
disposables);
|
||||||
|
|
||||||
if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
|
if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
|
||||||
showKeyboardSearch();
|
showKeyboardSearch();
|
||||||
|
@ -841,7 +842,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
hideSuggestionsPanel();
|
hideSuggestionsPanel();
|
||||||
showMetaInfoInTextView(null, searchBinding.searchMetaInfoTextView,
|
showMetaInfoInTextView(null, searchBinding.searchMetaInfoTextView,
|
||||||
searchBinding.searchMetaInfoSeparator);
|
searchBinding.searchMetaInfoSeparator, disposables);
|
||||||
hideKeyboardSearch();
|
hideKeyboardSearch();
|
||||||
|
|
||||||
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
|
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
|
||||||
|
@ -986,8 +987,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
// List<MetaInfo> cannot be bundled without creating some containers
|
// List<MetaInfo> cannot be bundled without creating some containers
|
||||||
metaInfo = new MetaInfo[result.getMetaInfo().size()];
|
metaInfo = new MetaInfo[result.getMetaInfo().size()];
|
||||||
metaInfo = result.getMetaInfo().toArray(metaInfo);
|
metaInfo = result.getMetaInfo().toArray(metaInfo);
|
||||||
disposables.add(showMetaInfoInTextView(result.getMetaInfo(),
|
showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView,
|
||||||
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator));
|
searchBinding.searchMetaInfoSeparator, disposables);
|
||||||
|
|
||||||
handleSearchSuggestion();
|
handleSearchSuggestion();
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
|
@ -36,7 +36,7 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.settings.HistorySettingsFragment;
|
import org.schabi.newpipe.settings.HistorySettingsFragment;
|
||||||
import org.schabi.newpipe.util.KoreUtil;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||||
|
@ -359,7 +359,7 @@ public class StatisticsPlaylistFragment
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
entries.add(StreamDialogEntry.open_in_browser);
|
entries.add(StreamDialogEntry.open_in_browser);
|
||||||
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||||
entries.add(StreamDialogEntry.play_with_kodi);
|
entries.add(StreamDialogEntry.play_with_kodi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.util.KoreUtil;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
@ -770,7 +770,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
entries.add(StreamDialogEntry.open_in_browser);
|
entries.add(StreamDialogEntry.open_in_browser);
|
||||||
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||||
entries.add(StreamDialogEntry.play_with_kodi);
|
entries.add(StreamDialogEntry.play_with_kodi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,9 +56,9 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
import org.schabi.newpipe.util.OnClickGesture
|
import org.schabi.newpipe.util.OnClickGesture
|
||||||
import org.schabi.newpipe.util.ShareUtils
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
@ -293,7 +293,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||||
|
|
||||||
val actions = DialogInterface.OnClickListener { _, i ->
|
val actions = DialogInterface.OnClickListener { _, i ->
|
||||||
when (i) {
|
when (i) {
|
||||||
0 -> ShareUtils.shareText(requireContext(), selectedItem.name, selectedItem.url)
|
0 -> ShareUtils.shareText(requireContext(), selectedItem.name, selectedItem.url,
|
||||||
|
selectedItem.thumbnailUrl)
|
||||||
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
|
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
|
||||||
2 -> deleteChannel(selectedItem)
|
2 -> deleteChannel(selectedItem)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ import java.util.List;
|
||||||
|
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
import static org.schabi.newpipe.util.ShareUtils.shareText;
|
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
|
||||||
|
|
||||||
public final class PlayQueueActivity extends AppCompatActivity
|
public final class PlayQueueActivity extends AppCompatActivity
|
||||||
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
||||||
|
@ -313,7 +313,8 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3,
|
final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3,
|
||||||
Menu.NONE, R.string.share);
|
Menu.NONE, R.string.share);
|
||||||
share.setOnMenuItemClickListener(menuItem -> {
|
share.setOnMenuItemClickListener(menuItem -> {
|
||||||
shareText(getApplicationContext(), item.getTitle(), item.getUrl());
|
shareText(getApplicationContext(), item.getTitle(), item.getUrl(),
|
||||||
|
item.getThumbnailUrl());
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -123,11 +123,11 @@ import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
||||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
import org.schabi.newpipe.util.KoreUtil;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.SerializedCache;
|
import org.schabi.newpipe.util.SerializedCache;
|
||||||
import org.schabi.newpipe.util.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.views.ExpandableSurfaceView;
|
import org.schabi.newpipe.views.ExpandableSurfaceView;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -1033,7 +1033,7 @@ public final class Player implements
|
||||||
// show kodi button if it supports the current service and it is enabled in settings
|
// show kodi button if it supports the current service and it is enabled in settings
|
||||||
binding.playWithKodi.setVisibility(videoPlayerSelected()
|
binding.playWithKodi.setVisibility(videoPlayerSelected()
|
||||||
&& playQueue != null && playQueue.getItem() != null
|
&& playQueue != null && playQueue.getItem() != null
|
||||||
&& KoreUtil.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId())
|
&& KoreUtils.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId())
|
||||||
? View.VISIBLE : View.GONE);
|
? View.VISIBLE : View.GONE);
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
@ -3593,7 +3593,8 @@ public final class Player implements
|
||||||
} else if (v.getId() == binding.moreOptionsButton.getId()) {
|
} else if (v.getId() == binding.moreOptionsButton.getId()) {
|
||||||
onMoreOptionsClicked();
|
onMoreOptionsClicked();
|
||||||
} else if (v.getId() == binding.share.getId()) {
|
} else if (v.getId() == binding.share.getId()) {
|
||||||
ShareUtils.shareText(context, getVideoTitle(), getVideoUrlAtCurrentTime());
|
ShareUtils.shareText(context, getVideoTitle(), getVideoUrlAtCurrentTime(),
|
||||||
|
currentItem.getThumbnailUrl());
|
||||||
} else if (v.getId() == binding.playWithKodi.getId()) {
|
} else if (v.getId() == binding.playWithKodi.getId()) {
|
||||||
onPlayWithKodiClicked();
|
onPlayWithKodiClicked();
|
||||||
} else if (v.getId() == binding.openInBrowser.getId()) {
|
} else if (v.getId() == binding.openInBrowser.getId()) {
|
||||||
|
@ -3724,7 +3725,7 @@ public final class Player implements
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.i(TAG, "Failed to start kore", e);
|
Log.i(TAG, "Failed to start kore", e);
|
||||||
}
|
}
|
||||||
KoreUtil.showInstallKoreDialog(getParentActivity());
|
KoreUtils.showInstallKoreDialog(getParentActivity());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.schabi.newpipe.util;
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
import android.text.Selection;
|
import android.text.Selection;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
|
@ -11,27 +10,14 @@ import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.util.external_communication.InternalUrlsHandler;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
|
||||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public class CommentTextOnTouchListener implements View.OnTouchListener {
|
public class CommentTextOnTouchListener implements View.OnTouchListener {
|
||||||
public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener();
|
public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener();
|
||||||
|
|
||||||
private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("(.*)#timestamp=(\\d+)");
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouch(final View v, final MotionEvent event) {
|
public boolean onTouch(final View v, final MotionEvent event) {
|
||||||
if (!(v instanceof TextView)) {
|
if (!(v instanceof TextView)) {
|
||||||
|
@ -64,13 +50,12 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
|
||||||
|
|
||||||
if (link.length != 0) {
|
if (link.length != 0) {
|
||||||
if (action == MotionEvent.ACTION_UP) {
|
if (action == MotionEvent.ACTION_UP) {
|
||||||
boolean handled = false;
|
|
||||||
if (link[0] instanceof URLSpan) {
|
if (link[0] instanceof URLSpan) {
|
||||||
handled = handleUrl(v.getContext(), (URLSpan) link[0]);
|
final String url = ((URLSpan) link[0]).getURL();
|
||||||
|
if (!InternalUrlsHandler.handleUrlCommentsTimestamp(
|
||||||
|
new CompositeDisposable(), v.getContext(), url)) {
|
||||||
|
ShareUtils.openUrlInBrowser(v.getContext(), url, false);
|
||||||
}
|
}
|
||||||
if (!handled) {
|
|
||||||
ShareUtils.openUrlInBrowser(v.getContext(),
|
|
||||||
((URLSpan) link[0]).getURL(), false);
|
|
||||||
}
|
}
|
||||||
} else if (action == MotionEvent.ACTION_DOWN) {
|
} else if (action == MotionEvent.ACTION_DOWN) {
|
||||||
Selection.setSelection(buffer,
|
Selection.setSelection(buffer,
|
||||||
|
@ -83,52 +68,4 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean handleUrl(final Context context, final URLSpan urlSpan) {
|
|
||||||
String url = urlSpan.getURL();
|
|
||||||
int seconds = -1;
|
|
||||||
final Matcher matcher = TIMESTAMP_PATTERN.matcher(url);
|
|
||||||
if (matcher.matches()) {
|
|
||||||
url = matcher.group(1);
|
|
||||||
seconds = Integer.parseInt(matcher.group(2));
|
|
||||||
}
|
|
||||||
final StreamingService service;
|
|
||||||
final StreamingService.LinkType linkType;
|
|
||||||
try {
|
|
||||||
service = NewPipe.getServiceByUrl(url);
|
|
||||||
linkType = service.getLinkTypeByUrl(url);
|
|
||||||
} catch (final ExtractionException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (linkType == StreamingService.LinkType.NONE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (linkType == StreamingService.LinkType.STREAM && seconds != -1) {
|
|
||||||
return playOnPopup(context, url, service, seconds);
|
|
||||||
} else {
|
|
||||||
NavigationHelper.openRouterActivity(context, url);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean playOnPopup(final Context context, final String url,
|
|
||||||
final StreamingService service, final int seconds) {
|
|
||||||
final LinkHandlerFactory factory = service.getStreamLHFactory();
|
|
||||||
final String cleanUrl;
|
|
||||||
try {
|
|
||||||
cleanUrl = factory.getUrl(factory.getId(url));
|
|
||||||
} catch (final ParsingException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final Single single
|
|
||||||
= ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false);
|
|
||||||
single.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(info -> {
|
|
||||||
final PlayQueue playQueue
|
|
||||||
= new SinglePlayQueue((StreamInfo) info, seconds * 1000);
|
|
||||||
NavigationHelper.playOnPopupPlayer(context, playQueue, false);
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.util.external_communication.TextLinkifier;
|
||||||
import org.schabi.newpipe.extractor.Info;
|
import org.schabi.newpipe.extractor.Info;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||||
|
@ -54,7 +55,7 @@ import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Maybe;
|
import io.reactivex.rxjava3.core.Maybe;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||||
|
|
||||||
|
@ -268,18 +269,19 @@ public final class ExtractorHelper {
|
||||||
* @param metaInfos a list of meta information, can be null or empty
|
* @param metaInfos a list of meta information, can be null or empty
|
||||||
* @param metaInfoTextView the text view in which to show the formatted HTML
|
* @param metaInfoTextView the text view in which to show the formatted HTML
|
||||||
* @param metaInfoSeparator another view to be shown or hidden accordingly to the text view
|
* @param metaInfoSeparator another view to be shown or hidden accordingly to the text view
|
||||||
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
|
* @param disposables disposables created by the method are added here and their lifecycle
|
||||||
|
* should be handled by the calling class
|
||||||
*/
|
*/
|
||||||
public static Disposable showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInfos,
|
public static void showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInfos,
|
||||||
final TextView metaInfoTextView,
|
final TextView metaInfoTextView,
|
||||||
final View metaInfoSeparator) {
|
final View metaInfoSeparator,
|
||||||
|
final CompositeDisposable disposables) {
|
||||||
final Context context = metaInfoTextView.getContext();
|
final Context context = metaInfoTextView.getContext();
|
||||||
if (metaInfos == null || metaInfos.isEmpty()
|
if (metaInfos == null || metaInfos.isEmpty()
|
||||||
|| !PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
|
|| !PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
|
||||||
context.getString(R.string.show_meta_info_key), true)) {
|
context.getString(R.string.show_meta_info_key), true)) {
|
||||||
metaInfoTextView.setVisibility(View.GONE);
|
metaInfoTextView.setVisibility(View.GONE);
|
||||||
metaInfoSeparator.setVisibility(View.GONE);
|
metaInfoSeparator.setVisibility(View.GONE);
|
||||||
return Disposable.empty();
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
final StringBuilder stringBuilder = new StringBuilder();
|
final StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
@ -310,8 +312,8 @@ public final class ExtractorHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
metaInfoSeparator.setVisibility(View.VISIBLE);
|
metaInfoSeparator.setVisibility(View.VISIBLE);
|
||||||
return TextLinkifier.createLinksFromHtmlBlock(context, stringBuilder.toString(),
|
TextLinkifier.createLinksFromHtmlBlock(metaInfoTextView, stringBuilder.toString(),
|
||||||
metaInfoTextView, HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING);
|
HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING, null, disposables);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,10 +53,11 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
import org.schabi.newpipe.settings.SettingsActivity;
|
import org.schabi.newpipe.settings.SettingsActivity;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.ShareUtils.installApp;
|
import static org.schabi.newpipe.util.external_communication.ShareUtils.installApp;
|
||||||
|
|
||||||
public final class NavigationHelper {
|
public final class NavigationHelper {
|
||||||
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
|
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
|
||||||
|
@ -252,7 +253,7 @@ public final class NavigationHelper {
|
||||||
|
|
||||||
public static void resolveActivityOrAskToInstall(final Context context, final Intent intent) {
|
public static void resolveActivityOrAskToInstall(final Context context, final Intent intent) {
|
||||||
if (intent.resolveActivity(context.getPackageManager()) != null) {
|
if (intent.resolveActivity(context.getPackageManager()) != null) {
|
||||||
ShareUtils.openIntentInApp(context, intent);
|
ShareUtils.openIntentInApp(context, intent, false);
|
||||||
} else {
|
} else {
|
||||||
if (context instanceof Activity) {
|
if (context instanceof Activity) {
|
||||||
new AlertDialog.Builder(context)
|
new AlertDialog.Builder(context)
|
||||||
|
|
|
@ -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.MainPlayer;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -83,12 +85,13 @@ public enum StreamDialogEntry {
|
||||||
try {
|
try {
|
||||||
NavigationHelper.playWithKore(fragment.requireContext(), videoUrl);
|
NavigationHelper.playWithKore(fragment.requireContext(), videoUrl);
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
KoreUtil.showInstallKoreDialog(fragment.getActivity());
|
KoreUtils.showInstallKoreDialog(fragment.getActivity());
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
share(R.string.share, (fragment, item) ->
|
share(R.string.share, (fragment, item) ->
|
||||||
ShareUtils.shareText(fragment.getContext(), item.getName(), item.getUrl())),
|
ShareUtils.shareText(fragment.getContext(), item.getName(), item.getUrl(),
|
||||||
|
item.getThumbnailUrl())),
|
||||||
|
|
||||||
open_in_browser(R.string.open_in_browser, (fragment, item) ->
|
open_in_browser(R.string.open_in_browser, (fragment, item) ->
|
||||||
ShareUtils.openUrlInBrowser(fragment.getContext(), item.getUrl()));
|
ShareUtils.openUrlInBrowser(fragment.getContext(), item.getUrl()));
|
||||||
|
|
|
@ -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 android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.ServiceList;
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
public final class KoreUtil {
|
public final class KoreUtils {
|
||||||
private KoreUtil() { }
|
private KoreUtils() { }
|
||||||
|
|
||||||
public static boolean isServiceSupportedByKore(final int serviceId) {
|
public static boolean isServiceSupportedByKore(final int serviceId) {
|
||||||
return (serviceId == ServiceList.YouTube.getServiceId()
|
return (serviceId == ServiceList.YouTube.getServiceId()
|
||||||
|| serviceId == ServiceList.SoundCloud.getServiceId());
|
|| serviceId == ServiceList.SoundCloud.getServiceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean shouldShowPlayWithKodi(final Context context, final int serviceId) {
|
public static boolean shouldShowPlayWithKodi(@NonNull final Context context,
|
||||||
|
final int serviceId) {
|
||||||
return isServiceSupportedByKore(serviceId)
|
return isServiceSupportedByKore(serviceId)
|
||||||
&& PreferenceManager.getDefaultSharedPreferences(context)
|
&& PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
.getBoolean(context.getString(R.string.show_play_with_kodi_key), false);
|
.getBoolean(context.getString(R.string.show_play_with_kodi_key), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showInstallKoreDialog(final Context context) {
|
public static void showInstallKoreDialog(@NonNull final Context context) {
|
||||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||||
builder.setMessage(R.string.kore_not_found)
|
builder.setMessage(R.string.kore_not_found)
|
||||||
.setPositiveButton(R.string.install, (dialog, which) ->
|
.setPositiveButton(R.string.install, (dialog, which) ->
|
|
@ -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.annotation.SuppressLint;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
|
@ -45,7 +44,7 @@ import org.schabi.newpipe.error.ErrorActivity;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -348,10 +347,8 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
|
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
|
||||||
|
|
||||||
final Uri uri = resolveShareableUri(mission);
|
|
||||||
|
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||||
intent.setDataAndType(uri, mimeType);
|
intent.setDataAndType(resolveShareableUri(mission), mimeType);
|
||||||
intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
|
intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
@ -361,10 +358,8 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||||
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
|
|
||||||
if (intent.resolveActivity(mContext.getPackageManager()) != null) {
|
if (intent.resolveActivity(mContext.getPackageManager()) != null) {
|
||||||
ShareUtils.openIntentInApp(mContext, intent);
|
ShareUtils.openIntentInApp(mContext, intent, false);
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG).show();
|
Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
@ -377,19 +372,18 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||||
shareIntent.setType(resolveMimeType(mission));
|
shareIntent.setType(resolveMimeType(mission));
|
||||||
shareIntent.putExtra(Intent.EXTRA_STREAM, resolveShareableUri(mission));
|
shareIntent.putExtra(Intent.EXTRA_STREAM, resolveShareableUri(mission));
|
||||||
shareIntent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
|
shareIntent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|
||||||
final Intent intent = new Intent(Intent.ACTION_CHOOSER);
|
final Intent intent = new Intent(Intent.ACTION_CHOOSER);
|
||||||
intent.putExtra(Intent.EXTRA_INTENT, shareIntent);
|
intent.putExtra(Intent.EXTRA_INTENT, shareIntent);
|
||||||
|
// unneeded to set a title to the chooser on Android P and higher because the system
|
||||||
|
// ignores this title on these versions
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
|
||||||
intent.putExtra(Intent.EXTRA_TITLE, mContext.getString(R.string.share_dialog_title));
|
intent.putExtra(Intent.EXTRA_TITLE, mContext.getString(R.string.share_dialog_title));
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
|
|
||||||
try {
|
|
||||||
intent.setPackage("android");
|
|
||||||
mContext.startActivity(intent);
|
|
||||||
} catch (final ActivityNotFoundException e) {
|
|
||||||
// falling back to OEM chooser if Android's system chooser was removed by the OEM
|
|
||||||
intent.setPackage(null);
|
|
||||||
mContext.startActivity(intent);
|
|
||||||
}
|
}
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|
||||||
|
mContext.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue