package org.schabi.newpipe; import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO; import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO; import android.annotation.SuppressLint; import android.app.IntentService; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Bundle; import android.text.TextUtils; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.Toast; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; import androidx.core.app.NotificationCompat; import androidx.core.app.ServiceCompat; import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceManager; import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.databinding.ListRadioIconItemBinding; import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding; import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.ReCaptchaActivity; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService.LinkType; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException; import org.schabi.newpipe.extractor.exceptions.PaidContentException; import org.schabi.newpipe.extractor.exceptions.PrivateContentException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException; import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.urlfinder.UrlFinder; import org.schabi.newpipe.views.FocusOverlayView; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import icepick.Icepick; import icepick.State; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.functions.Consumer; import io.reactivex.rxjava3.schedulers.Schedulers; /** * Get the url from the intent and open it in the chosen preferred player. */ public class RouterActivity extends AppCompatActivity { protected final CompositeDisposable disposables = new CompositeDisposable(); @State protected int currentServiceId = -1; @State protected LinkType currentLinkType; @State protected int selectedRadioPosition = -1; protected int selectedPreviously = -1; protected String currentUrl; private StreamingService currentService; private boolean selectionIsDownload = false; private boolean selectionIsAddToPlaylist = false; private AlertDialog alertDialogChoice = null; @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); Icepick.restoreInstanceState(this, savedInstanceState); if (TextUtils.isEmpty(currentUrl)) { currentUrl = getUrl(getIntent()); if (TextUtils.isEmpty(currentUrl)) { handleText(); finish(); } } ThemeHelper.setDayNightMode(this); setTheme(ThemeHelper.isLightThemeSelected(this) ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark); Localization.assureCorrectAppLanguage(this); } @Override protected void onStop() { super.onStop(); // we need to dismiss the dialog before leaving the activity or we get leaks if (alertDialogChoice != null) { alertDialogChoice.dismiss(); } } @Override protected void onSaveInstanceState(@NonNull final Bundle outState) { super.onSaveInstanceState(outState); Icepick.saveInstanceState(this, outState); } @Override protected void onStart() { super.onStart(); handleUrl(currentUrl); } @Override protected void onDestroy() { super.onDestroy(); disposables.clear(); } private void handleUrl(final String url) { disposables.add(Observable .fromCallable(() -> { try { if (currentServiceId == -1) { currentService = NewPipe.getServiceByUrl(url); currentServiceId = currentService.getServiceId(); currentLinkType = currentService.getLinkTypeByUrl(url); currentUrl = url; } else { currentService = NewPipe.getService(currentServiceId); } // return whether the url was found to be supported or not return currentLinkType != LinkType.NONE; } catch (final ExtractionException e) { // this can be reached only when the url is completely unsupported return false; } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(isUrlSupported -> { if (isUrlSupported) { onSuccess(); } else { showUnsupportedUrlDialog(url); } }, throwable -> handleError(this, new ErrorInfo(throwable, UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url)))); } /** * @param context the context. It will be {@code finish()}ed at the end of the handling if it is * an instance of {@link RouterActivity}. * @param errorInfo the error information */ private static void handleError(final Context context, final ErrorInfo errorInfo) { if (errorInfo.getThrowable() != null) { errorInfo.getThrowable().printStackTrace(); } if (errorInfo.getThrowable() instanceof ReCaptchaException) { Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); // Starting ReCaptcha Challenge Activity final Intent intent = new Intent(context, ReCaptchaActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } else if (errorInfo.getThrowable() != null && ExceptionUtils.isNetworkRelated(errorInfo.getThrowable())) { Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show(); } else if (errorInfo.getThrowable() instanceof AgeRestrictedContentException) { Toast.makeText(context, R.string.restricted_video_no_stream, Toast.LENGTH_LONG).show(); } else if (errorInfo.getThrowable() instanceof GeographicRestrictionException) { Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show(); } else if (errorInfo.getThrowable() instanceof PaidContentException) { Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show(); } else if (errorInfo.getThrowable() instanceof PrivateContentException) { Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show(); } else if (errorInfo.getThrowable() instanceof SoundCloudGoPlusContentException) { Toast.makeText(context, R.string.soundcloud_go_plus_content, Toast.LENGTH_LONG).show(); } else if (errorInfo.getThrowable() instanceof YoutubeMusicPremiumContentException) { Toast.makeText(context, R.string.youtube_music_premium_content, Toast.LENGTH_LONG).show(); } else if (errorInfo.getThrowable() instanceof ContentNotAvailableException) { Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); } else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) { Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show(); } else { ErrorUtil.createNotification(context, errorInfo); } if (context instanceof RouterActivity) { ((RouterActivity) context).finish(); } } private void showUnsupportedUrlDialog(final String url) { final Context context = getThemeWrapperContext(); new AlertDialog.Builder(context) .setTitle(R.string.unsupported_url) .setMessage(R.string.unsupported_url_dialog_message) .setIcon(R.drawable.ic_share) .setPositiveButton(R.string.open_in_browser, (dialog, which) -> ShareUtils.openUrlInBrowser(this, url)) .setNegativeButton(R.string.share, (dialog, which) -> ShareUtils.shareText(this, "", url)) // no subject .setNeutralButton(R.string.cancel, null) .setOnDismissListener(dialog -> finish()) .show(); } protected void onSuccess() { final SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(this); final ChoiceAvailabilityChecker choiceChecker = new ChoiceAvailabilityChecker( getChoicesForService(currentService, currentLinkType), preferences.getString(getString(R.string.preferred_open_action_key), getString(R.string.preferred_open_action_default))); // Check for non-player related choices if (choiceChecker.isAvailableAndSelected( R.string.show_info_key, R.string.download_key, R.string.add_to_playlist_key)) { handleChoice(choiceChecker.getSelectedChoiceKey()); return; } // Check if the choice is player related if (choiceChecker.isAvailableAndSelected( R.string.video_player_key, R.string.background_player_key, R.string.popup_player_key)) { final String selectedChoice = choiceChecker.getSelectedChoiceKey(); final boolean isExtVideoEnabled = preferences.getBoolean( getString(R.string.use_external_video_player_key), false); final boolean isExtAudioEnabled = preferences.getBoolean( getString(R.string.use_external_audio_player_key), false); final boolean isVideoPlayerSelected = selectedChoice.equals(getString(R.string.video_player_key)) || selectedChoice.equals(getString(R.string.popup_player_key)); final boolean isAudioPlayerSelected = selectedChoice.equals(getString(R.string.background_player_key)); if (currentLinkType != LinkType.STREAM && ((isExtAudioEnabled && isAudioPlayerSelected) || (isExtVideoEnabled && isVideoPlayerSelected)) ) { Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show(); handleChoice(getString(R.string.show_info_key)); return; } final List capabilities = currentService.getServiceInfo().getMediaCapabilities(); // Check if the service supports the choice if ((isVideoPlayerSelected && capabilities.contains(VIDEO)) || (isAudioPlayerSelected && capabilities.contains(AUDIO))) { handleChoice(selectedChoice); } else { handleChoice(getString(R.string.show_info_key)); } return; } // Default / Ask always final List availableChoices = choiceChecker.getAvailableChoices(); switch (availableChoices.size()) { case 1: handleChoice(availableChoices.get(0).key); break; case 0: handleChoice(getString(R.string.show_info_key)); break; default: showDialog(availableChoices); break; } } /** * This is a helper class for checking if the choices are available and/or selected. */ class ChoiceAvailabilityChecker { private final List availableChoices; private final String selectedChoiceKey; ChoiceAvailabilityChecker( @NonNull final List availableChoices, @NonNull final String selectedChoiceKey) { this.availableChoices = availableChoices; this.selectedChoiceKey = selectedChoiceKey; } public List getAvailableChoices() { return availableChoices; } public String getSelectedChoiceKey() { return selectedChoiceKey; } public boolean isAvailableAndSelected(@StringRes final int... wantedKeys) { return Arrays.stream(wantedKeys).anyMatch(this::isAvailableAndSelected); } public boolean isAvailableAndSelected(@StringRes final int wantedKey) { final String wanted = getString(wantedKey); // Check if the wanted option is selected if (!selectedChoiceKey.equals(wanted)) { return false; } // Check if it's available return availableChoices.stream().anyMatch(item -> wanted.equals(item.key)); } } private void showDialog(final List choices) { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); final Context themeWrapperContext = getThemeWrapperContext(); final LayoutInflater layoutInflater = LayoutInflater.from(themeWrapperContext); final SingleChoiceDialogViewBinding binding = SingleChoiceDialogViewBinding.inflate(layoutInflater); final RadioGroup radioGroup = binding.list; final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { final int indexOfChild = radioGroup.indexOfChild( radioGroup.findViewById(radioGroup.getCheckedRadioButtonId())); final AdapterChoiceItem choice = choices.get(indexOfChild); handleChoice(choice.key); // open future streams always like this one, because "always" button was used by user if (which == DialogInterface.BUTTON_POSITIVE) { preferences.edit() .putString(getString(R.string.preferred_open_action_key), choice.key) .apply(); } }; alertDialogChoice = new AlertDialog.Builder(themeWrapperContext) .setTitle(R.string.preferred_open_action_share_menu_title) .setView(binding.getRoot()) .setCancelable(true) .setNegativeButton(R.string.just_once, dialogButtonsClickListener) .setPositiveButton(R.string.always, dialogButtonsClickListener) .setOnDismissListener(dialog -> { if (!selectionIsDownload && !selectionIsAddToPlaylist) { finish(); } }) .create(); alertDialogChoice.setOnShowListener(dialog -> setDialogButtonsState( alertDialogChoice, radioGroup.getCheckedRadioButtonId() != -1)); radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialogChoice, true)); final View.OnClickListener radioButtonsClickListener = v -> { final int indexOfChild = radioGroup.indexOfChild(v); if (indexOfChild == -1) { return; } selectedPreviously = selectedRadioPosition; selectedRadioPosition = indexOfChild; if (selectedPreviously == selectedRadioPosition) { handleChoice(choices.get(selectedRadioPosition).key); } }; int id = 12345; for (final AdapterChoiceItem item : choices) { final RadioButton radioButton = ListRadioIconItemBinding.inflate(layoutInflater) .getRoot(); radioButton.setText(item.description); radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds( AppCompatResources.getDrawable(themeWrapperContext, item.icon), null, null, null); radioButton.setChecked(false); radioButton.setId(id++); radioButton.setLayoutParams(new RadioGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); radioButton.setOnClickListener(radioButtonsClickListener); radioGroup.addView(radioButton); } if (selectedRadioPosition == -1) { final String lastSelectedPlayer = preferences.getString( getString(R.string.preferred_open_action_last_selected_key), null); if (!TextUtils.isEmpty(lastSelectedPlayer)) { for (int i = 0; i < choices.size(); i++) { final AdapterChoiceItem c = choices.get(i); if (lastSelectedPlayer.equals(c.key)) { selectedRadioPosition = i; break; } } } } selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.size() - 1); if (selectedRadioPosition != -1) { ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); } selectedPreviously = selectedRadioPosition; alertDialogChoice.show(); if (DeviceUtils.isTv(this)) { FocusOverlayView.setupFocusObserver(alertDialogChoice); } } private List getChoicesForService(final StreamingService service, final LinkType linkType) { final AdapterChoiceItem showInfo = new AdapterChoiceItem( getString(R.string.show_info_key), getString(R.string.show_info), R.drawable.ic_info_outline); final AdapterChoiceItem videoPlayer = new AdapterChoiceItem( getString(R.string.video_player_key), getString(R.string.video_player), R.drawable.ic_play_arrow); final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem( getString(R.string.background_player_key), getString(R.string.background_player), R.drawable.ic_headset); final AdapterChoiceItem popupPlayer = new AdapterChoiceItem( getString(R.string.popup_player_key), getString(R.string.popup_player), R.drawable.ic_picture_in_picture); final List returnedItems = new ArrayList<>(); returnedItems.add(showInfo); // Always present final List capabilities = service.getServiceInfo().getMediaCapabilities(); if (linkType == LinkType.STREAM) { if (capabilities.contains(VIDEO)) { returnedItems.add(videoPlayer); returnedItems.add(popupPlayer); } if (capabilities.contains(AUDIO)) { returnedItems.add(backgroundPlayer); } // download is redundant for linkType CHANNEL AND PLAYLIST (till playlist downloading is // not supported ) returnedItems.add(new AdapterChoiceItem(getString(R.string.download_key), getString(R.string.download), R.drawable.ic_file_download)); // Add to playlist is not necessary for CHANNEL and PLAYLIST linkType since those can // not be added to a playlist returnedItems.add(new AdapterChoiceItem(getString(R.string.add_to_playlist_key), getString(R.string.add_to_playlist), R.drawable.ic_add)); } else { // LinkType.NONE is never present because it's filtered out before // channels and playlist can be played as they contain a list of videos final SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(this); final boolean isExtVideoEnabled = preferences.getBoolean( getString(R.string.use_external_video_player_key), false); final boolean isExtAudioEnabled = preferences.getBoolean( getString(R.string.use_external_audio_player_key), false); if (capabilities.contains(VIDEO) && !isExtVideoEnabled) { returnedItems.add(videoPlayer); returnedItems.add(popupPlayer); } if (capabilities.contains(AUDIO) && !isExtAudioEnabled) { returnedItems.add(backgroundPlayer); } } return returnedItems; } private Context getThemeWrapperContext() { return new ContextThemeWrapper(this, ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme); } private void setDialogButtonsState(final AlertDialog dialog, final boolean state) { final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); if (negativeButton == null || positiveButton == null) { return; } negativeButton.setEnabled(state); positiveButton.setEnabled(state); } private void handleText() { final String searchString = getIntent().getStringExtra(Intent.EXTRA_TEXT); final int serviceId = getIntent().getIntExtra(Constants.KEY_SERVICE_ID, 0); final Intent intent = new Intent(getThemeWrapperContext(), MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); NavigationHelper.openSearch(getThemeWrapperContext(), serviceId, searchString); } private void handleChoice(final String selectedChoiceKey) { final List validChoicesList = Arrays.asList(getResources() .getStringArray(R.array.preferred_open_action_values_list)); if (validChoicesList.contains(selectedChoiceKey)) { PreferenceManager.getDefaultSharedPreferences(this).edit() .putString(getString( R.string.preferred_open_action_last_selected_key), selectedChoiceKey) .apply(); } if (selectedChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { PermissionHelper.showPopupEnablementToast(this); finish(); return; } if (selectedChoiceKey.equals(getString(R.string.download_key))) { if (PermissionHelper.checkStoragePermissions(this, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { selectionIsDownload = true; openDownloadDialog(); } return; } if (selectedChoiceKey.equals(getString(R.string.add_to_playlist_key))) { selectionIsAddToPlaylist = true; openAddToPlaylistDialog(); return; } // stop and bypass FetcherService if InfoScreen was selected since // StreamDetailFragment can fetch data itself if (selectedChoiceKey.equals(getString(R.string.show_info_key)) || canHandleChoiceLikeShowInfo(selectedChoiceKey)) { disposables.add(Observable .fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(intent -> { startActivity(intent); finish(); }, throwable -> handleError(this, new ErrorInfo(throwable, UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl))) ); return; } final Intent intent = new Intent(this, FetcherService.class); final Choice choice = new Choice(currentService.getServiceId(), currentLinkType, currentUrl, selectedChoiceKey); intent.putExtra(FetcherService.KEY_CHOICE, choice); startService(intent); finish(); } private boolean canHandleChoiceLikeShowInfo(final String selectedChoiceKey) { if (!selectedChoiceKey.equals(getString(R.string.video_player_key))) { return false; } // "video player" can be handled like "show info" (because VideoDetailFragment can load // the stream instead of FetcherService) when... // ...Autoplay is enabled if (!PlayerHelper.isAutoplayAllowedByUser(getThemeWrapperContext())) { return false; } final boolean isExtVideoEnabled = PreferenceManager.getDefaultSharedPreferences(this) .getBoolean(getString(R.string.use_external_video_player_key), false); // ...it's not done via an external player if (isExtVideoEnabled) { return false; } // ...the player is not running or in normal Video-mode/type final PlayerService.PlayerType playerType = PlayerHolder.getInstance().getType(); return playerType == null || playerType == PlayerService.PlayerType.MAIN; } private void openAddToPlaylistDialog() { // Getting the stream info usually takes a moment // Notifying the user here to ensure that no confusion arises Toast.makeText( getApplicationContext(), getString(R.string.processing_may_take_a_moment), Toast.LENGTH_SHORT) .show(); disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, false) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( info -> PlaylistDialog.createCorrespondingDialog( getThemeWrapperContext(), Collections.singletonList(new StreamEntity(info)), playlistDialog -> { playlistDialog.setOnDismissListener(dialog -> finish()); playlistDialog.show( this.getSupportFragmentManager(), "addToPlaylistDialog" ); } ), throwable -> handleError(this, new ErrorInfo( throwable, UserAction.REQUESTED_STREAM, "Tried to add " + currentUrl + " to a playlist", currentService.getServiceId()) ) ) ); } @SuppressLint("CheckResult") private void openDownloadDialog() { disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { final DownloadDialog downloadDialog = new DownloadDialog(this, result); downloadDialog.setOnDismissListener(dialog -> finish()); final FragmentManager fm = getSupportFragmentManager(); downloadDialog.show(fm, "downloadDialog"); fm.executePendingTransactions(); }, throwable -> showUnsupportedUrlDialog(currentUrl))); } @Override public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); for (final int i : grantResults) { if (i == PackageManager.PERMISSION_DENIED) { finish(); return; } } if (requestCode == PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE) { openDownloadDialog(); } } private static class AdapterChoiceItem { final String description; final String key; @DrawableRes final int icon; AdapterChoiceItem(final String key, final String description, final int icon) { this.key = key; this.description = description; this.icon = icon; } } private static class Choice implements Serializable { final int serviceId; final String url; final String playerChoice; final LinkType linkType; Choice(final int serviceId, final LinkType linkType, final String url, final String playerChoice) { this.serviceId = serviceId; this.linkType = linkType; this.url = url; this.playerChoice = playerChoice; } @NonNull @Override public String toString() { return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; } } public static class FetcherService extends IntentService { public static final String KEY_CHOICE = "key_choice"; private static final int ID = 456; private Disposable fetcher; public FetcherService() { super(FetcherService.class.getSimpleName()); } @Override public void onCreate() { super.onCreate(); startForeground(ID, createNotification().build()); } @Override protected void onHandleIntent(@Nullable final Intent intent) { if (intent == null) { return; } final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); if (!(serializable instanceof Choice)) { return; } final Choice playerChoice = (Choice) serializable; handleChoice(playerChoice); } public void handleChoice(final Choice choice) { Single single = null; UserAction userAction = UserAction.SOMETHING_ELSE; switch (choice.linkType) { case STREAM: single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false); userAction = UserAction.REQUESTED_STREAM; break; case CHANNEL: single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false); userAction = UserAction.REQUESTED_CHANNEL; break; case PLAYLIST: single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false); userAction = UserAction.REQUESTED_PLAYLIST; break; } if (single != null) { final UserAction finalUserAction = userAction; final Consumer resultHandler = getResultHandler(choice); fetcher = single .observeOn(AndroidSchedulers.mainThread()) .subscribe(info -> { resultHandler.accept(info); if (fetcher != null) { fetcher.dispose(); } }, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction, choice.url + " opened with " + choice.playerChoice, choice.serviceId))); } } public Consumer getResultHandler(final Choice choice) { return info -> { final String videoPlayerKey = getString(R.string.video_player_key); final String backgroundPlayerKey = getString(R.string.background_player_key); final String popupPlayerKey = getString(R.string.popup_player_key); final SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(this); final boolean isExtVideoEnabled = preferences.getBoolean( getString(R.string.use_external_video_player_key), false); final boolean isExtAudioEnabled = preferences.getBoolean( getString(R.string.use_external_audio_player_key), false); final PlayQueue playQueue; if (info instanceof StreamInfo) { if (choice.playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); return; } else if (choice.playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); return; } playQueue = new SinglePlayQueue((StreamInfo) info); } else if (info instanceof ChannelInfo) { playQueue = new ChannelPlayQueue((ChannelInfo) info); } else if (info instanceof PlaylistInfo) { playQueue = new PlaylistPlayQueue((PlaylistInfo) info); } else { return; } if (choice.playerChoice.equals(videoPlayerKey)) { NavigationHelper.playOnMainPlayer(this, playQueue, false); } else if (choice.playerChoice.equals(backgroundPlayerKey)) { NavigationHelper.playOnBackgroundPlayer(this, playQueue, true); } else if (choice.playerChoice.equals(popupPlayerKey)) { NavigationHelper.playOnPopupPlayer(this, playQueue, true); } }; } @Override public void onDestroy() { super.onDestroy(); ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); if (fetcher != null) { fetcher.dispose(); } } private NotificationCompat.Builder createNotification() { return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) .setOngoing(true) .setSmallIcon(R.drawable.ic_newpipe_triangle_white) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentTitle( getString(R.string.preferred_player_fetcher_notification_title)) .setContentText( getString(R.string.preferred_player_fetcher_notification_message)); } } /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ @Nullable private String getUrl(final Intent intent) { String foundUrl = null; if (intent.getData() != null) { // Called from another app foundUrl = intent.getData().toString(); } else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) { // Called from the share menu final String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); foundUrl = UrlFinder.firstUrlFromInput(extraText); } return foundUrl; } }