diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dab6fb2ec..0b6cf50f7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -124,7 +124,7 @@ + android:theme="@style/RouterActivityThemeDark"> @@ -175,17 +175,21 @@ - - + + + + android:theme="@style/RouterActivityThemeDark"> @@ -204,6 +208,11 @@ + + + + + diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java index 186b4adc2..6cd79e2c9 100644 --- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java @@ -77,17 +77,6 @@ public abstract class BaseFragment extends Fragment { protected void initListeners() { } - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - protected final int resolveResourceIdFromAttr(@AttrRes int attr) { - TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{attr}); - int attributeResourceId = a.getResourceId(0, 0); - a.recycle(); - return attributeResourceId; - } - /*////////////////////////////////////////////////////////////////////////// // DisplayImageOptions default configurations //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 41e557b52..8aaa248dd 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -3,14 +3,25 @@ package org.schabi.newpipe; import android.content.Intent; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; +import android.text.TextUtils; import android.widget.Toast; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; import java.util.Collection; import java.util.HashSet; -/** +import icepick.Icepick; +import icepick.State; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; + +/* * Copyright (C) Christian Schabesberger 2017 * RouterActivity.java is part of NewPipe. * @@ -34,23 +45,71 @@ import java.util.HashSet; */ public class RouterActivity extends AppCompatActivity { + @State + protected String currentUrl; + protected CompositeDisposable disposables = new CompositeDisposable(); + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + Icepick.restoreInstanceState(this, savedInstanceState); - String videoUrl = getUrl(getIntent()); - handleUrl(videoUrl); + if (TextUtils.isEmpty(currentUrl)) { + currentUrl = getUrl(getIntent()); + + if (TextUtils.isEmpty(currentUrl)) { + Toast.makeText(this, R.string.invalid_url_toast, Toast.LENGTH_LONG).show(); + finish(); + } + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + Icepick.saveInstanceState(this, outState); + } + + @Override + protected void onStart() { + super.onStart(); + handleUrl(currentUrl); } protected void handleUrl(String url) { - boolean success = NavigationHelper.openByLink(this, url); - if (!success) { + disposables.add(Observable + .fromCallable(() -> NavigationHelper.getIntentByLink(this, url)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(intent -> { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + + finish(); + }, this::handleError) + ); + } + + protected void handleError(Throwable error) { + error.printStackTrace(); + + if (error instanceof ExtractionException) { Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); + } else { + ExtractorHelper.handleGeneralException(this, -1, null, error, UserAction.SOMETHING_ELSE, null); } finish(); } + @Override + protected void onDestroy() { + super.onDestroy(); + + disposables.clear(); + } + /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ @@ -71,7 +130,8 @@ public class RouterActivity extends AppCompatActivity { } else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) { //this means that vidoe was called through share menu String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); - videoUrl = getUris(extraText)[0]; + final String[] uris = getUris(extraText); + videoUrl = uris.length > 0 ? uris[0] : null; } return videoUrl; diff --git a/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java new file mode 100644 index 000000000..7196e413d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/RouterPlayerActivity.java @@ -0,0 +1,413 @@ +package org.schabi.newpipe; + +import android.app.IntentService; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.preference.PreferenceManager; +import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; +import android.support.v7.app.AlertDialog; +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.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.Toast; + +import org.schabi.newpipe.extractor.Info; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.ServiceList; +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.ExtractionException; +import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.playlist.ChannelPlayQueue; +import org.schabi.newpipe.playlist.PlayQueue; +import org.schabi.newpipe.playlist.PlaylistPlayQueue; +import org.schabi.newpipe.playlist.SinglePlayQueue; +import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.ThemeHelper; + +import java.io.Serializable; +import java.util.Arrays; + +import icepick.State; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; + +import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; + +/** + * Get the url from the intent and open it in the chosen preferred player + */ +public class RouterPlayerActivity extends RouterActivity { + + @State + protected int currentServiceId = -1; + private StreamingService currentService; + @State + protected LinkType currentLinkType; + @State + protected int selectedRadioPosition = -1; + protected int selectedPreviously = -1; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { + super.onCreate(savedInstanceState, persistentState); + setTheme(ThemeHelper.isLightThemeSelected(this) ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark); + } + + @Override + protected void handleUrl(String url) { + disposables.add(Observable + .fromCallable(() -> { + if (currentServiceId == -1) { + currentService = NewPipe.getServiceByUrl(url); + currentServiceId = currentService.getServiceId(); + currentLinkType = currentService.getLinkTypeByUrl(url); + currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType); + } else { + currentService = NewPipe.getService(currentServiceId); + } + + return currentLinkType != LinkType.NONE; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + if (result) { + onSuccess(); + } else { + onError(); + } + }, this::handleError)); + } + + protected void onError() { + Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); + finish(); + } + + protected void onSuccess() { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); + boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); + + if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) { + Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show(); + finish(); + return; + } + + // TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.) + if (currentService == ServiceList.SoundCloud.getService()) { + handleChoice(getString(R.string.background_player_key)); + return; + } + + final String playerChoiceKey = preferences.getString(getString(R.string.preferred_player_key), getString(R.string.preferred_player_default)); + final String alwaysAskKey = getString(R.string.always_ask_player_key); + + if (playerChoiceKey.equals(alwaysAskKey)) { + showDialog(); + } else { + handleChoice(playerChoiceKey); + } + } + + private void showDialog() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this, + ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme); + + LayoutInflater inflater = LayoutInflater.from(themeWrapper); + final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false); + final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); + + final AdapterChoiceItem[] choices = { + new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.play)), + new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.audio)), + new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player), + resolveResourceIdFromAttr(themeWrapper, R.attr.popup)) + }; + + final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { + final int indexOfChild = radioGroup.indexOfChild(radioGroup.findViewById(radioGroup.getCheckedRadioButtonId())); + final AdapterChoiceItem choice = choices[indexOfChild]; + + handleChoice(choice.key); + + if (which == DialogInterface.BUTTON_POSITIVE) { + preferences.edit().putString(getString(R.string.preferred_player_key), choice.key).apply(); + } + }; + + final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper) + .setTitle(R.string.preferred_player_share_menu_title) + .setView(radioGroup) + .setCancelable(true) + .setNegativeButton(R.string.just_once, dialogButtonsClickListener) + .setPositiveButton(R.string.always, dialogButtonsClickListener) + .setOnDismissListener((dialog) -> finish()) + .create(); + + alertDialog.setOnShowListener(dialog -> { + setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1); + }); + + radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, 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[selectedRadioPosition].key); + } + }; + + int id = 12345; + for (AdapterChoiceItem item : choices) { + final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); + radioButton.setText(item.description); + radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0); + 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_player_last_selected_key), null); + if (!TextUtils.isEmpty(lastSelectedPlayer)) { + for (int i = 0; i < choices.length; i++) { + AdapterChoiceItem c = choices[i]; + if (lastSelectedPlayer.equals(c.key)) { + selectedRadioPosition = i; + break; + } + } + } + } + + selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1); + if (selectedRadioPosition != -1) { + ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); + } + selectedPreviously = selectedRadioPosition; + + alertDialog.show(); + } + + private void setDialogButtonsState(AlertDialog dialog, 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 handleChoice(final String playerChoiceKey) { + if (Arrays.asList(getResources().getStringArray(R.array.preferred_player_values_list)).contains(playerChoiceKey)) { + PreferenceManager.getDefaultSharedPreferences(this).edit() + .putString(getString(R.string.preferred_player_last_selected_key), playerChoiceKey).apply(); + } + + if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) { + PermissionHelper.showPopupEnablementToast(this); + finish(); + return; + } + + final Intent intent = new Intent(this, FetcherService.class); + intent.putExtra(FetcherService.KEY_CHOICE, new Choice(currentService.getServiceId(), currentLinkType, currentUrl, playerChoiceKey)); + startService(intent); + + finish(); + } + + private static class AdapterChoiceItem { + final String description, key; + @DrawableRes + final int icon; + + AdapterChoiceItem(String key, String description, int icon) { + this.description = description; + this.key = key; + this.icon = icon; + } + } + + private static class Choice implements Serializable { + final int serviceId; + final String url, playerChoice; + final LinkType linkType; + + Choice(int serviceId, LinkType linkType, String url, String playerChoice) { + this.serviceId = serviceId; + this.linkType = linkType; + this.url = url; + this.playerChoice = playerChoice; + } + + @Override + public String toString() { + return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Service Fetcher + //////////////////////////////////////////////////////////////////////////*/ + + public static class FetcherService extends IntentService { + + private static final int ID = 456; + public static final String KEY_CHOICE = "key_choice"; + private Disposable fetcher; + + public FetcherService() { + super(FetcherService.class.getSimpleName()); + } + + @Override + public void onCreate() { + super.onCreate(); + startForeground(ID, createNotification().build()); + } + + @Override + protected void onHandleIntent(@Nullable Intent intent) { + if (intent == null) return; + + final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE); + if (!(serializable instanceof Choice)) return; + Choice playerChoice = (Choice) serializable; + handleChoice(playerChoice); + } + + public void handleChoice(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 -> ExtractorHelper.handleGeneralException(this, + choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice)); + } + } + + public Consumer getResultHandler(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); + boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false); + boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false); + boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this); + + PlayQueue playQueue; + String playerChoice = choice.playerChoice; + + if (info instanceof StreamInfo) { + if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { + NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info); + + } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) { + NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info); + + } else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) { + NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info); + + } else { + playQueue = new SinglePlayQueue((StreamInfo) info); + + if (playerChoice.equals(videoPlayerKey)) { + NavigationHelper.playOnMainPlayer(this, playQueue); + } else if (playerChoice.equals(backgroundPlayerKey)) { + NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); + } else if (playerChoice.equals(popupPlayerKey)) { + NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true); + } + } + } + + if (info instanceof ChannelInfo || info instanceof PlaylistInfo) { + playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); + + if (playerChoice.equals(videoPlayerKey)) { + NavigationHelper.playOnMainPlayer(this, playQueue); + } else if (playerChoice.equals(backgroundPlayerKey)) { + NavigationHelper.playOnBackgroundPlayer(this, playQueue); + } else if (playerChoice.equals(popupPlayerKey)) { + NavigationHelper.playOnPopupPlayer(this, playQueue); + } + } + }; + } + + @Override + public void onDestroy() { + super.onDestroy(); + stopForeground(true); + 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)); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/RouterPopupActivity.java b/app/src/main/java/org/schabi/newpipe/RouterPopupActivity.java deleted file mode 100644 index fe0b6e907..000000000 --- a/app/src/main/java/org/schabi/newpipe/RouterPopupActivity.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.schabi.newpipe; - -import android.content.Intent; -import android.os.Build; -import android.widget.Toast; - -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.player.PopupVideoPlayer; -import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.PermissionHelper; - -/** - * Get the url from the intent and open a popup player - */ -public class RouterPopupActivity extends RouterActivity { - - @Override - protected void handleUrl(String url) { - if (!PermissionHelper.isPopupEnabled(this)) { - PermissionHelper.showPopupEnablementToast(this); - finish(); - return; - } - StreamingService service; - try { - service = NewPipe.getServiceByUrl(url); - } catch (ExtractionException e) { - Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); - return; - } - - Intent callIntent = new Intent(this, PopupVideoPlayer.class); - switch (service.getLinkTypeByUrl(url)) { - case STREAM: - break; - default: - Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); - return; - } - - callIntent.putExtra(Constants.KEY_URL, url); - callIntent.putExtra(Constants.KEY_SERVICE_ID, service.getServiceId()); - startService(callIntent); - - finish(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 665a8f7f9..c7b61eceb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -13,7 +13,6 @@ import android.support.annotation.DrawableRes; import android.support.annotation.FloatRange; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; -import android.support.v4.text.TextUtilsCompat; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; @@ -25,7 +24,6 @@ import android.text.util.Linkify; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -37,7 +35,6 @@ import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.PopupMenu; import android.widget.RelativeLayout; import android.widget.Spinner; import android.widget.TextView; @@ -51,7 +48,6 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ParsingException; @@ -80,6 +76,7 @@ import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.ThemeHelper; import java.io.Serializable; import java.util.ArrayList; @@ -394,7 +391,7 @@ public class VideoDetailFragment extends BaseStateFragment implement if (relatedStreamsView.getChildCount() > initialCount) { relatedStreamsView.removeViews(initialCount, relatedStreamsView.getChildCount() - (initialCount)); - relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, resolveResourceIdFromAttr(R.attr.expand))); + relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand))); return; } @@ -404,7 +401,7 @@ public class VideoDetailFragment extends BaseStateFragment implement //Log.d(TAG, "i = " + i); relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item)); } - relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, resolveResourceIdFromAttr(R.attr.collapse))); + relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.collapse))); } /*////////////////////////////////////////////////////////////////////////// @@ -579,7 +576,7 @@ public class VideoDetailFragment extends BaseStateFragment implement relatedStreamRootLayout.setVisibility(View.VISIBLE); relatedStreamExpandButton.setVisibility(View.VISIBLE); - relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, resolveResourceIdFromAttr(R.attr.expand))); + relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand))); } else { if (info.getNextVideo() == null) relatedStreamRootLayout.setVisibility(View.GONE); relatedStreamExpandButton.setVisibility(View.GONE); @@ -807,7 +804,7 @@ public class VideoDetailFragment extends BaseStateFragment implement if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) { openNormalBackgroundPlayer(append); } else { - openExternalBackgroundPlayer(audioStream); + NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), audioStream); } } @@ -841,13 +838,12 @@ public class VideoDetailFragment extends BaseStateFragment implement } if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) { - openExternalVideoPlayer(selectedVideoStream); + NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), selectedVideoStream); } else { openNormalPlayer(selectedVideoStream); } } - private void openNormalBackgroundPlayer(final boolean append) { final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); if (append) { @@ -857,40 +853,6 @@ public class VideoDetailFragment extends BaseStateFragment implement } } - private void openExternalBackgroundPlayer(AudioStream audioStream) { - Intent intent; - intent = new Intent(); - try { - intent.setAction(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.parse(audioStream.getUrl()), audioStream.getFormat().getMimeType()); - intent.putExtra(Intent.EXTRA_TITLE, currentInfo.getName()); - intent.putExtra("title", currentInfo.getName()); - activity.startActivity(intent); - } catch (Exception e) { - e.printStackTrace(); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setMessage(R.string.no_player_found) - .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url))); - activity.startActivity(intent); - } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Log.i(TAG, "You unlocked a secret unicorn."); - } - }); - builder.create().show(); - Log.e(TAG, "Either no Streaming player for audio was installed, or something important crashed:"); - e.printStackTrace(); - } - } - private void openNormalPlayer(VideoStream selectedVideoStream) { Intent mIntent; boolean useOldPlayer = PlayerHelper.isUsingOldPlayer(activity) || (Build.VERSION.SDK_INT < 16); @@ -909,33 +871,6 @@ public class VideoDetailFragment extends BaseStateFragment implement startActivity(mIntent); } - private void openExternalVideoPlayer(VideoStream selectedVideoStream) { - // External Player - Intent intent = new Intent(); - try { - intent.setAction(Intent.ACTION_VIEW) - .setDataAndType(Uri.parse(selectedVideoStream.getUrl()), selectedVideoStream.getFormat().getMimeType()) - .putExtra(Intent.EXTRA_TITLE, currentInfo.getName()) - .putExtra("title", currentInfo.getName()); - this.startActivity(intent); - } catch (Exception e) { - e.printStackTrace(); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setMessage(R.string.no_player_found) - .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent() - .setAction(Intent.ACTION_VIEW) - .setData(Uri.parse(getString(R.string.fdroid_vlc_url))); - startActivity(intent); - } - }) - .setNegativeButton(R.string.cancel, null); - builder.create().show(); - } - } - /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ @@ -1212,4 +1147,4 @@ public class VideoDetailFragment extends BaseStateFragment implement showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema); } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 427c97741..55a73d484 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -111,6 +111,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen public static final String PLAYBACK_QUALITY = "playback_quality"; public static final String PLAY_QUEUE = "play_queue"; public static final String APPEND_ONLY = "append_only"; + public static final String SELECT_ON_APPEND = "select_on_append"; /*////////////////////////////////////////////////////////////////////////// // Playback @@ -218,7 +219,13 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen // Resolve append intents if (intent.getBooleanExtra(APPEND_ONLY, false) && playQueue != null) { + int sizeBeforeAppend = playQueue.size(); playQueue.append(queue.getStreams()); + + if (intent.getBooleanExtra(SELECT_ON_APPEND, false) && queue.getStreams().size() > 0) { + playQueue.setIndex(sizeBeforeAppend); + } + return; } diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 0f0d8d785..c3803f0d5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -31,7 +31,6 @@ import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.PixelFormat; import android.os.Build; -import android.os.Handler; import android.os.IBinder; import android.preference.PreferenceManager; import android.support.annotation.NonNull; @@ -49,20 +48,12 @@ import android.widget.PopupMenu; import android.widget.RemoteViews; import android.widget.SeekBar; import android.widget.TextView; -import android.widget.Toast; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import org.schabi.newpipe.BuildConfig; -import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; -import org.schabi.newpipe.ReCaptchaActivity; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; -import org.schabi.newpipe.extractor.exceptions.ParsingException; -import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; -import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.player.event.PlayerEventListener; @@ -70,22 +61,12 @@ import org.schabi.newpipe.player.helper.LockManager; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.old.PlayVideoActivity; import org.schabi.newpipe.playlist.PlayQueueItem; -import org.schabi.newpipe.playlist.SinglePlayQueue; -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; -import java.io.IOException; import java.util.List; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - import static org.schabi.newpipe.player.helper.PlayerHelper.isUsingOldPlayer; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -125,8 +106,8 @@ public final class PopupVideoPlayer extends Service { private RemoteViews notRemoteView; private VideoPlayerImpl playerImpl; - private Disposable currentWorker; private LockManager lockManager; + /*////////////////////////////////////////////////////////////////////////// // Service-Activity Binder //////////////////////////////////////////////////////////////////////////*/ @@ -157,21 +138,8 @@ public final class PopupVideoPlayer extends Service { if (playerImpl.getPlayer() == null) initPopup(); if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true); - if (intent != null && intent.getStringExtra(Constants.KEY_URL) != null) { - final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); - final String url = intent.getStringExtra(Constants.KEY_URL); + playerImpl.handleIntent(intent); - playerImpl.setStartedFromNewPipe(false); - - final FetcherHandler fetcherRunnable = new FetcherHandler(this, serviceId, url); - currentWorker = ExtractorHelper.getStreamInfo(serviceId,url,false) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(fetcherRunnable::onReceive, fetcherRunnable::onError); - } else { - playerImpl.setStartedFromNewPipe(true); - playerImpl.handleIntent(intent); - } return START_NOT_STICKY; } @@ -302,7 +270,6 @@ public final class PopupVideoPlayer extends Service { } if (lockManager != null) lockManager.releaseWifiAndCpu(); if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); - if (currentWorker != null) currentWorker.dispose(); mBinder = null; playerImpl = null; @@ -452,7 +419,6 @@ public final class PopupVideoPlayer extends Service { this.getPlaybackPitch(), this.getPlaybackQuality() ); - if (!isStartedFromNewPipe()) intent.putExtra(VideoPlayer.STARTED_FROM_NEWPIPE, false); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } else { intent = new Intent(PopupVideoPlayer.this, PlayVideoActivity.class) @@ -862,63 +828,4 @@ public final class PopupVideoPlayer extends Service { return true; } } - - /** - * Fetcher handler used if open by a link out of NewPipe - */ - private class FetcherHandler { - private final int serviceId; - private final String url; - - private final Context context; - private final Handler mainHandler; - - private FetcherHandler(Context context, int serviceId, String url) { - this.mainHandler = new Handler(PopupVideoPlayer.this.getMainLooper()); - this.context = context; - this.url = url; - this.serviceId = serviceId; - } - - private void onReceive(final StreamInfo info) { - mainHandler.post(() -> { - final Intent intent = NavigationHelper.getPlayerIntent(getApplicationContext(), - PopupVideoPlayer.class, new SinglePlayQueue(info)); - playerImpl.handleIntent(intent); - }); - } - - private void onError(final Throwable exception) { - if (DEBUG) Log.d(TAG, "onError() called with: exception = [" + exception + "]"); - exception.printStackTrace(); - mainHandler.post(() -> { - if (exception instanceof ReCaptchaException) { - onReCaptchaException(); - } else if (exception instanceof IOException) { - Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show(); - } else if (exception instanceof YoutubeStreamExtractor.GemaException) { - Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show(); - } else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) { - Toast.makeText(context, R.string.live_streams_not_supported, Toast.LENGTH_LONG).show(); - } else if (exception instanceof ContentNotAvailableException) { - Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); - } else { - int errorId = exception instanceof YoutubeStreamExtractor.DecryptException ? R.string.youtube_signature_decryption_error : - exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; - ErrorActivity.reportError(mainHandler, context, exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, errorId)); - } - }); - stopSelf(); - } - - private void onReCaptchaException() { - Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); - // Starting ReCaptcha Challenge Activity - Intent intent = new Intent(context, ReCaptchaActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - stopSelf(); - } - } - } \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index a3c8b53dc..5399ff047 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -39,7 +39,6 @@ import android.view.Menu; import android.view.MenuItem; import android.view.SurfaceView; import android.view.View; -import android.widget.ImageButton; import android.widget.ImageView; import android.widget.PopupMenu; import android.widget.ProgressBar; @@ -85,12 +84,6 @@ public abstract class VideoPlayer extends BasePlayer public static final boolean DEBUG = BasePlayer.DEBUG; public final String TAG; - /*////////////////////////////////////////////////////////////////////////// - // Intent - //////////////////////////////////////////////////////////////////////////*/ - - public static final String STARTED_FROM_NEWPIPE = "started_from_newpipe"; - /*////////////////////////////////////////////////////////////////////////// // Player //////////////////////////////////////////////////////////////////////////*/ @@ -102,7 +95,6 @@ public abstract class VideoPlayer extends BasePlayer protected String playbackQuality; - private boolean startedFromNewPipe = true; protected boolean wasPlaying = false; /*////////////////////////////////////////////////////////////////////////// @@ -695,14 +687,6 @@ public abstract class VideoPlayer extends BasePlayer return availableStreams.get(selectedStreamIndex); } - public boolean isStartedFromNewPipe() { - return startedFromNewPipe; - } - - public void setStartedFromNewPipe(boolean startedFromNewPipe) { - this.startedFromNewPipe = startedFromNewPipe; - } - public Handler getControlsVisibilityHandler() { return controlsVisibilityHandler; } diff --git a/app/src/main/java/org/schabi/newpipe/playlist/ChannelPlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/ChannelPlayQueue.java index 3c615608c..d3e31982a 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/ChannelPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/ChannelPlayQueue.java @@ -15,6 +15,10 @@ public final class ChannelPlayQueue extends AbstractInfoPlayQueue { + if (exception instanceof ReCaptchaException) { + Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); + // Starting ReCaptcha Challenge Activity + Intent intent = new Intent(context, ReCaptchaActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } else if (exception instanceof IOException) { + Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show(); + } else if (exception instanceof YoutubeStreamExtractor.GemaException) { + Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show(); + } else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) { + Toast.makeText(context, R.string.live_streams_not_supported, Toast.LENGTH_LONG).show(); + } else if (exception instanceof ContentNotAvailableException) { + Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); + } else { + int errorId = exception instanceof YoutubeStreamExtractor.DecryptException ? R.string.youtube_signature_decryption_error : + exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; + ErrorActivity.reportError(handler, context, exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(userAction, + serviceId == -1 ? "none" : NewPipe.getNameOfService(serviceId), url + (optionalErrorMessage == null ? "" : optionalErrorMessage), errorId)); + } + }); + + } + /** * Check if throwable have the cause that can be assignable from the causes to check. * @@ -263,17 +303,4 @@ public final class ExtractorHelper { InterruptedIOException.class, InterruptedException.class); } - - public static String toUpperCase(String value) { - StringBuilder sb = new StringBuilder(value); - for (int index = 0; index < sb.length(); index++) { - char c = sb.charAt(index); - if (Character.isLowerCase(c)) { - sb.setCharAt(index, Character.toUpperCase(c)); - } else { - sb.setCharAt(index, Character.toLowerCase(c)); - } - } - return sb.toString(); - } } diff --git a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java index 46c08b01b..0f082cc11 100644 --- a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java +++ b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java @@ -1,4 +1,4 @@ -/** +/* * Copyright 2017 Mauricio Colli * InfoCache.java is part of NewPipe * diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 3a9125bdf..ee000104d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -9,6 +9,8 @@ import android.os.Build; import android.preference.PreferenceManager; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; +import android.support.v7.app.AlertDialog; +import android.util.Log; import android.widget.Toast; import com.nostra13.universalimageloader.core.ImageLoader; @@ -21,6 +23,10 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.Stream; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.list.channel.ChannelFragment; @@ -36,9 +42,12 @@ import org.schabi.newpipe.player.MainVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayerActivity; import org.schabi.newpipe.player.VideoPlayer; +import org.schabi.newpipe.player.old.PlayVideoActivity; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.settings.SettingsActivity; +import java.util.ArrayList; + @SuppressWarnings({"unused", "WeakerAccess"}) public class NavigationHelper { public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag"; @@ -46,6 +55,7 @@ public class NavigationHelper { /*////////////////////////////////////////////////////////////////////////// // Players //////////////////////////////////////////////////////////////////////////*/ + public static Intent getPlayerIntent(final Context context, final Class targetClazz, final PlayQueue playQueue, @@ -65,9 +75,11 @@ public class NavigationHelper { public static Intent getPlayerEnqueueIntent(final Context context, final Class targetClazz, - final PlayQueue playQueue) { + final PlayQueue playQueue, + final boolean selectOnAppend) { return getPlayerIntent(context, targetClazz, playQueue) - .putExtra(BasePlayer.APPEND_ONLY, true); + .putExtra(BasePlayer.APPEND_ONLY, true) + .putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); } public static Intent getPlayerIntent(final Context context, @@ -84,16 +96,39 @@ public class NavigationHelper { } public static void playOnMainPlayer(final Context context, final PlayQueue queue) { - context.startActivity(getPlayerIntent(context, MainVideoPlayer.class, queue)); + final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue); + playerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(playerIntent); } - public static void playOnPopupPlayer(final Activity activity, final PlayQueue queue) { - if (!PermissionHelper.isPopupEnabled(activity)) { - PermissionHelper.showPopupEnablementToast(activity); + public static void playOnOldVideoPlayer(Context context, StreamInfo info) { + ArrayList videoStreamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false)); + int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList); + + if (index == -1) { + Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show(); return; } - Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - activity.startService(getPlayerIntent(activity, PopupVideoPlayer.class, queue)); + + VideoStream videoStream = videoStreamsList.get(index); + Intent intent = new Intent(context, PlayVideoActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(PlayVideoActivity.VIDEO_TITLE, info.getName()) + .putExtra(PlayVideoActivity.STREAM_URL, videoStream.getUrl()) + .putExtra(PlayVideoActivity.VIDEO_URL, info.getUrl()) + .putExtra(PlayVideoActivity.START_POSITION, info.getStartPosition()); + + context.startActivity(intent); + } + + public static void playOnPopupPlayer(final Context context, final PlayQueue queue) { + if (!PermissionHelper.isPopupEnabled(context)) { + PermissionHelper.showPopupEnablementToast(context); + return; + } + + Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); + context.startService(getPlayerIntent(context, PopupVideoPlayer.class, queue)); } public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue) { @@ -101,19 +136,92 @@ public class NavigationHelper { context.startService(getPlayerIntent(context, BackgroundPlayer.class, queue)); } - public static void enqueueOnPopupPlayer(final Activity activity, final PlayQueue queue) { - if (!PermissionHelper.isPopupEnabled(activity)) { - PermissionHelper.showPopupEnablementToast(activity); + public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue) { + enqueueOnPopupPlayer(context, queue, false); + } + + public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) { + if (!PermissionHelper.isPopupEnabled(context)) { + PermissionHelper.showPopupEnablementToast(context); return; } - Toast.makeText(activity, R.string.popup_playing_append, Toast.LENGTH_SHORT).show(); - activity.startService(getPlayerEnqueueIntent(activity, PopupVideoPlayer.class, queue)); + + Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show(); + context.startService(getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend)); } public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue) { - Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show(); - context.startService(getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue)); + enqueueOnBackgroundPlayer(context, queue, false); } + + public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) { + Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show(); + context.startService(getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend)); + } + + /*////////////////////////////////////////////////////////////////////////// + // External Players + //////////////////////////////////////////////////////////////////////////*/ + + public static void playOnExternalAudioPlayer(Context context, StreamInfo info) { + final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); + + if (index == -1) { + Toast.makeText(context, R.string.audio_streams_empty, Toast.LENGTH_SHORT).show(); + return; + } + + AudioStream audioStream = info.getAudioStreams().get(index); + playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream); + } + + public static void playOnExternalVideoPlayer(Context context, StreamInfo info) { + ArrayList videoStreamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false)); + int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList); + + if (index == -1) { + Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show(); + return; + } + + VideoStream videoStream = videoStreamsList.get(index); + playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream); + } + + public static void playOnExternalPlayer(Context context, String name, String artist, Stream stream) { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.parse(stream.getUrl()), stream.getFormat().getMimeType()); + intent.putExtra(Intent.EXTRA_TITLE, name); + intent.putExtra("title", name); + intent.putExtra("artist", artist); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + resolveActivityOrAskToInstall(context, intent); + } + + public static void resolveActivityOrAskToInstall(Context context, Intent intent) { + if (intent.resolveActivity(context.getPackageManager()) != null) { + context.startActivity(intent); + } else { + if (context instanceof Activity) { + new AlertDialog.Builder(context) + .setMessage(R.string.no_player_found) + .setPositiveButton(R.string.install, (dialog, which) -> { + Intent i = new Intent(); + i.setAction(Intent.ACTION_VIEW); + i.setData(Uri.parse(context.getString(R.string.fdroid_vlc_url))); + context.startActivity(i); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> Log.i("NavigationHelper", "You unlocked a secret unicorn.")) + .show(); + //Log.e("NavigationHelper", "Either no Streaming player for audio was installed, or something important crashed:"); + } else { + Toast.makeText(context, R.string.no_player_found_toast, Toast.LENGTH_LONG).show(); + } + } + } + /*////////////////////////////////////////////////////////////////////////// // Through FragmentManager //////////////////////////////////////////////////////////////////////////*/ @@ -287,19 +395,6 @@ public class NavigationHelper { // Link handling //////////////////////////////////////////////////////////////////////////*/ - public static boolean openByLink(Context context, String url) { - Intent intentByLink; - try { - intentByLink = getIntentByLink(context, url); - } catch (ExtractionException e) { - return false; - } - intentByLink.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intentByLink.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - context.startActivity(intentByLink); - return true; - } - private static Intent getOpenIntent(Context context, String url, int serviceId, StreamingService.LinkType type) { Intent mIntent = new Intent(context, MainActivity.class); mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); @@ -317,15 +412,14 @@ public class NavigationHelper { throw new ExtractionException("Service not supported at the moment"); } - int serviceId = service.getServiceId(); StreamingService.LinkType linkType = service.getLinkTypeByUrl(url); if (linkType == StreamingService.LinkType.NONE) { - throw new ExtractionException("Url not known to service. service=" + serviceId + " url=" + url); + throw new ExtractionException("Url not known to service. service=" + service + " url=" + url); } url = getCleanUrl(service, url, linkType); - Intent rIntent = getOpenIntent(context, url, serviceId, linkType); + Intent rIntent = getOpenIntent(context, url, service.getServiceId(), linkType); switch (linkType) { case STREAM: @@ -337,7 +431,7 @@ public class NavigationHelper { return rIntent; } - private static String getCleanUrl(StreamingService service, String dirtyUrl, StreamingService.LinkType linkType) throws ExtractionException { + public static String getCleanUrl(StreamingService service, String dirtyUrl, StreamingService.LinkType linkType) throws ExtractionException { switch (linkType) { case STREAM: return service.getStreamUrlIdHandler().cleanUrl(dirtyUrl); @@ -351,7 +445,6 @@ public class NavigationHelper { return null; } - private static Uri openMarketUrl(String packageName) { return Uri.parse("market://details") .buildUpon() diff --git a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java index 7cf804401..a33348934 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java @@ -20,7 +20,6 @@ import org.schabi.newpipe.R; public class PermissionHelper { public static final int PERMISSION_WRITE_STORAGE = 778; public static final int PERMISSION_READ_STORAGE = 777; - public static final int PERMISSION_SYSTEM_ALERT_WINDOW = 779; public static boolean checkStoragePermissions(Activity activity) { @@ -80,27 +79,25 @@ public class PermissionHelper { * In order to be able to draw over other apps, the permission android.permission.SYSTEM_ALERT_WINDOW have to be granted. *

* On < API 23 (MarshMallow) the permission was granted when the user installed the application (via AndroidManifest), - * on > 23, however, it have to start a activity asking the user if he agree. + * on > 23, however, it have to start a activity asking the user if he agrees. *

- * This method just return if canDraw over other apps, if it doesn't, try to get the permission, - * it does not get the result of the startActivityForResult, if the user accept, the next time that he tries to open - * it will return true. + * This method just return if the app has permission to draw over other apps, and if it doesn't, it will try to get the permission. * - * @param activity context to startActivityForResult * @return returns {@link Settings#canDrawOverlays(Context)} **/ @RequiresApi(api = Build.VERSION_CODES.M) - public static boolean checkSystemAlertWindowPermission(Activity activity) { - if (!Settings.canDrawOverlays(activity)) { - Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + activity.getPackageName())); - activity.startActivityForResult(i, PERMISSION_SYSTEM_ALERT_WINDOW); + public static boolean checkSystemAlertWindowPermission(Context context) { + if (!Settings.canDrawOverlays(context)) { + Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName())); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(i); return false; }else return true; } - public static boolean isPopupEnabled(Activity activity) { + public static boolean isPopupEnabled(Context context) { return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || - PermissionHelper.checkSystemAlertWindowPermission(activity); + PermissionHelper.checkSystemAlertWindowPermission(context); } public static void showPopupEnablementToast(Context context) { diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index 6fdf035f4..b078ac0c8 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -1,7 +1,10 @@ package org.schabi.newpipe.util; import android.content.Context; +import android.content.res.TypedArray; import android.preference.PreferenceManager; +import android.support.annotation.AttrRes; +import android.support.annotation.StyleRes; import org.schabi.newpipe.R; @@ -13,17 +16,7 @@ public class ThemeHelper { * @param context context that the theme will be applied */ public static void setTheme(Context context) { - String lightTheme = context.getResources().getString(R.string.light_theme_key); - String darkTheme = context.getResources().getString(R.string.dark_theme_key); - String blackTheme = context.getResources().getString(R.string.black_theme_key); - - String selectedTheme = getSelectedTheme(context); - - if (selectedTheme.equals(lightTheme)) context.setTheme(R.style.LightTheme); - else if (selectedTheme.equals(blackTheme)) context.setTheme(R.style.BlackTheme); - else if (selectedTheme.equals(darkTheme)) context.setTheme(R.style.DarkTheme); - // Fallback - else context.setTheme(R.style.DarkTheme); + context.setTheme(getSelectedThemeStyle(context)); } /** @@ -35,9 +28,34 @@ public class ThemeHelper { return getSelectedTheme(context).equals(context.getResources().getString(R.string.light_theme_key)); } + @StyleRes + public static int getSelectedThemeStyle(Context context) { + String lightTheme = context.getResources().getString(R.string.light_theme_key); + String darkTheme = context.getResources().getString(R.string.dark_theme_key); + String blackTheme = context.getResources().getString(R.string.black_theme_key); + + String selectedTheme = getSelectedTheme(context); + + if (selectedTheme.equals(lightTheme)) return R.style.LightTheme; + else if (selectedTheme.equals(blackTheme)) return R.style.BlackTheme; + else if (selectedTheme.equals(darkTheme)) return R.style.DarkTheme; + // Fallback + else return R.style.DarkTheme; + } + public static String getSelectedTheme(Context context) { String themeKey = context.getString(R.string.theme_key); String defaultTheme = context.getResources().getString(R.string.default_theme_value); return PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, defaultTheme); } + + /** + * Get a resource id from a resource styled according to the the context's theme. + */ + public static int resolveResourceIdFromAttr(Context context, @AttrRes int attr) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + int attributeResourceId = a.getResourceId(0, 0); + a.recycle(); + return attributeResourceId; + } } diff --git a/app/src/main/res/drawable-hdpi/ic_play_arrow_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_play_arrow_black_24dp.png new file mode 100644 index 000000000..e9c288c99 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_play_arrow_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png new file mode 100644 index 000000000..57c9fa546 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_play_arrow_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_play_arrow_black_24dp.png new file mode 100644 index 000000000..d78c57bad Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_play_arrow_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png new file mode 100644 index 000000000..c61e948bb Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png new file mode 100644 index 000000000..f208795fc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png new file mode 100644 index 000000000..a3c80e73d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png new file mode 100644 index 000000000..5345ee3c4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png new file mode 100644 index 000000000..547ef30aa Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_black_24dp.png new file mode 100644 index 000000000..d12d49562 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_24dp.png new file mode 100644 index 000000000..be5c062b5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_24dp.png differ diff --git a/app/src/main/res/drawable/dark_checked_selector.xml b/app/src/main/res/drawable/dark_checked_selector.xml new file mode 100644 index 000000000..59019470f --- /dev/null +++ b/app/src/main/res/drawable/dark_checked_selector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/light_checked_selector.xml b/app/src/main/res/drawable/light_checked_selector.xml new file mode 100644 index 000000000..b782a3688 --- /dev/null +++ b/app/src/main/res/drawable/light_checked_selector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_radio_icon_item.xml b/app/src/main/res/layout/list_radio_icon_item.xml new file mode 100644 index 000000000..20b966995 --- /dev/null +++ b/app/src/main/res/layout/list_radio_icon_item.xml @@ -0,0 +1,19 @@ + + diff --git a/app/src/main/res/layout/preferred_player_dialog_view.xml b/app/src/main/res/layout/preferred_player_dialog_view.xml new file mode 100644 index 000000000..83e1031a5 --- /dev/null +++ b/app/src/main/res/layout/preferred_player_dialog_view.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 86b9644ae..e14d9cb80 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -22,10 +22,12 @@ + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index b77a2d229..694e4900f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -8,6 +8,7 @@ #000000 #32000000 #48868686 + #2a868686 #1fa6a6a6 #5a000000 #ffffff @@ -21,6 +22,7 @@ #FFFFFF #0affffff #48ffffff + #2affffff #1f717171 #82000000 #424242 diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index f0c496aa2..7a34d296b 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -149,6 +149,29 @@ @string/charset_most_special_characters_value + + preferred_player_key + @string/always_ask_player_key + preferred_player_last_selected + + video_player + background_player + popup_player + always_ask_player + + + @string/video_player + @string/background_player + @string/popup_player + @string/always_ask_player + + + @string/video_player_key + @string/background_player_key + @string/popup_player_key + @string/always_ask_player_key + + af diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a6014a29..6d5e48d2d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ %1$s views Published on %1$s No stream player found. Do you want to install VLC? + No stream player found (you can install VLC to play it) Install Cancel https://f-droid.org/repository/browse/?fdfilter=vlc&fdid=org.videolan.vlc @@ -119,6 +120,8 @@ Best resolution Undo Play All + Always + Just Once newpipe NewPipe Notification @@ -148,6 +151,10 @@ Failed to play this stream Unrecoverable player error occurred Recovering from player error + External players don\'t support these types of links + Invalid URL + No video streams found + No audio streams found Sorry, that should not have happened. @@ -336,4 +343,17 @@ Close Drawer YouTube SoundCloud + + + @string/preferred_player_settings_title + Open with preferred player + Preferred player + + Video player + Background player + Popup player + Always ask + + Getting info… + "The requested content is loading" diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index c0c16e30f..b9c1d9f9c 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -29,9 +29,11 @@ @drawable/ic_fiber_manual_record_black_24dp @drawable/ic_arrow_top_left_black_24dp @drawable/ic_more_vert_black_24dp + @drawable/ic_play_arrow_black_24dp @color/light_separator_color @color/light_contrast_background_color + @drawable/light_checked_selector @color/light_queue_background_color @drawable/toolbar_shadow_light @drawable/light_selector @@ -69,9 +71,11 @@ @drawable/ic_fiber_manual_record_white_24dp @drawable/ic_arrow_top_left_white_24dp @drawable/ic_more_vert_white_24dp + @drawable/ic_play_arrow_white_24dp @color/dark_separator_color @color/dark_contrast_background_color + @drawable/dark_checked_selector @color/dark_queue_background_color @drawable/toolbar_shadow_dark @drawable/dark_selector @@ -164,12 +168,27 @@ @color/dark_youtube_primary_color - + + diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml index 6685f1a25..ceacfb142 100644 --- a/app/src/main/res/xml/video_audio_settings.xml +++ b/app/src/main/res/xml/video_audio_settings.xml @@ -1,7 +1,6 @@ + +