Refactor player: separate UIs and more

This commit is contained in:
Stypox 2022-04-08 09:35:14 +02:00
parent bc3731265e
commit 76ced59b62
No known key found for this signature in database
GPG key ID: 4BDF1B40A49FDD23
38 changed files with 4242 additions and 3564 deletions

View file

@ -166,7 +166,7 @@ afterEvaluate {
if (!System.properties.containsKey('skipFormatKtlint')) { if (!System.properties.containsKey('skipFormatKtlint')) {
preDebugBuild.dependsOn formatKtlint preDebugBuild.dependsOn formatKtlint
} }
preDebugBuild.dependsOn runCheckstyle, runKtlint //preDebugBuild.dependsOn runCheckstyle, runKtlint
} }
sonarqube { sonarqube {

View file

@ -44,7 +44,7 @@
</receiver> </receiver>
<service <service
android:name=".player.MainPlayer" android:name=".player.PlayerService"
android:exported="false" android:exported="false"
android:foregroundServiceType="mediaPlayback"> android:foregroundServiceType="mediaPlayback">
<intent-filter> <intent-filter>

View file

@ -60,7 +60,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
@ -630,8 +630,8 @@ public class RouterActivity extends AppCompatActivity {
} }
// ...the player is not running or in normal Video-mode/type // ...the player is not running or in normal Video-mode/type
final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType(); final PlayerService.PlayerType playerType = PlayerHolder.getInstance().getType();
return playerType == null || playerType == MainPlayer.PlayerType.VIDEO; return playerType == null || playerType == PlayerService.PlayerType.MAIN;
} }
private void openAddToPlaylistDialog() { private void openAddToPlaylistDialog() {

View file

@ -43,6 +43,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.viewbinding.ViewBinding;
import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
@ -77,8 +78,8 @@ import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.MainPlayer.PlayerType; import org.schabi.newpipe.player.PlayerService.PlayerType;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
@ -87,6 +88,8 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.player.ui.MainPlayerUi;
import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
@ -106,6 +109,7 @@ import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import icepick.State; import icepick.State;
@ -202,7 +206,7 @@ public final class VideoDetailFragment
private ContentObserver settingsContentObserver; private ContentObserver settingsContentObserver;
@Nullable @Nullable
private MainPlayer playerService; private PlayerService playerService;
private Player player; private Player player;
private final PlayerHolder playerHolder = PlayerHolder.getInstance(); private final PlayerHolder playerHolder = PlayerHolder.getInstance();
@ -211,7 +215,7 @@ public final class VideoDetailFragment
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @Override
public void onServiceConnected(final Player connectedPlayer, public void onServiceConnected(final Player connectedPlayer,
final MainPlayer connectedPlayerService, final PlayerService connectedPlayerService,
final boolean playAfterConnect) { final boolean playAfterConnect) {
player = connectedPlayer; player = connectedPlayer;
playerService = connectedPlayerService; playerService = connectedPlayerService;
@ -219,6 +223,7 @@ public final class VideoDetailFragment
// It will do nothing if the player is not in fullscreen mode // It will do nothing if the player is not in fullscreen mode
hideSystemUiIfNeeded(); hideSystemUiIfNeeded();
final Optional<MainPlayerUi> playerUi = player.UIs().get(MainPlayerUi.class);
if (!player.videoPlayerSelected() && !playAfterConnect) { if (!player.videoPlayerSelected() && !playAfterConnect) {
return; return;
} }
@ -227,22 +232,23 @@ public final class VideoDetailFragment
// If the video is playing but orientation changed // If the video is playing but orientation changed
// let's make the video in fullscreen again // let's make the video in fullscreen again
checkLandscape(); checkLandscape();
} else if (player.isFullscreen() && !player.isVerticalVideo() } else if (playerUi.map(ui -> ui.isFullscreen() && !ui.isVerticalVideo()).orElse(false)
// Tablet UI has orientation-independent fullscreen // Tablet UI has orientation-independent fullscreen
&& !DeviceUtils.isTablet(activity)) { && !DeviceUtils.isTablet(activity)) {
// Device is in portrait orientation after rotation but UI is in fullscreen. // Device is in portrait orientation after rotation but UI is in fullscreen.
// Return back to non-fullscreen state // Return back to non-fullscreen state
player.toggleFullscreen(); playerUi.ifPresent(MainPlayerUi::toggleFullscreen);
} }
if (playerIsNotStopped() && player.videoPlayerSelected()) { if (playerIsNotStopped() && player.videoPlayerSelected()) {
addVideoPlayerView(); addVideoPlayerView();
} }
//noinspection SimplifyOptionalCallChains
if (playAfterConnect if (playAfterConnect
|| (currentInfo != null || (currentInfo != null
&& isAutoplayEnabled() && isAutoplayEnabled()
&& player.getParentActivity() == null)) { && !playerUi.isPresent())) {
autoPlayEnabled = true; // forcefully start playing autoPlayEnabled = true; // forcefully start playing
openVideoPlayerAutoFullscreen(); openVideoPlayerAutoFullscreen();
} }
@ -518,7 +524,7 @@ public final class VideoDetailFragment
case R.id.overlay_play_pause_button: case R.id.overlay_play_pause_button:
if (playerIsNotStopped()) { if (playerIsNotStopped()) {
player.playPause(); player.playPause();
player.hideControls(0, 0); player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
showSystemUi(); showSystemUi();
} else { } else {
autoPlayEnabled = true; // forcefully start playing autoPlayEnabled = true; // forcefully start playing
@ -583,12 +589,12 @@ public final class VideoDetailFragment
if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) { if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) {
binding.detailVideoTitleView.setMaxLines(10); binding.detailVideoTitleView.setMaxLines(10);
animateRotation(binding.detailToggleSecondaryControlsView, animateRotation(binding.detailToggleSecondaryControlsView,
Player.DEFAULT_CONTROLS_DURATION, 180); VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180);
binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE); binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE);
} else { } else {
binding.detailVideoTitleView.setMaxLines(1); binding.detailVideoTitleView.setMaxLines(1);
animateRotation(binding.detailToggleSecondaryControlsView, animateRotation(binding.detailToggleSecondaryControlsView,
Player.DEFAULT_CONTROLS_DURATION, 0); VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 0);
binding.detailSecondaryControlPanel.setVisibility(View.GONE); binding.detailSecondaryControlPanel.setVisibility(View.GONE);
} }
// view pager height has changed, update the tab layout // view pager height has changed, update the tab layout
@ -746,7 +752,9 @@ public final class VideoDetailFragment
@Override @Override
public boolean onKeyDown(final int keyCode) { public boolean onKeyDown(final int keyCode) {
return isPlayerAvailable() && player.onKeyDown(keyCode); return isPlayerAvailable()
&& player.UIs().get(VideoPlayerUi.class)
.map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false);
} }
@Override @Override
@ -756,7 +764,7 @@ public final class VideoDetailFragment
} }
// If we are in fullscreen mode just exit from it via first back press // If we are in fullscreen mode just exit from it via first back press
if (isPlayerAvailable() && player.isFullscreen()) { if (isFullscreen()) {
if (!DeviceUtils.isTablet(activity)) { if (!DeviceUtils.isTablet(activity)) {
player.pause(); player.pause();
} }
@ -1006,8 +1014,7 @@ public final class VideoDetailFragment
getChildFragmentManager().beginTransaction() getChildFragmentManager().beginTransaction()
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info)) .replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
.commitAllowingStateLoss(); .commitAllowingStateLoss();
binding.relatedItemsLayout.setVisibility( binding.relatedItemsLayout.setVisibility(isFullscreen() ? View.GONE : View.VISIBLE);
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.VISIBLE);
} }
} }
@ -1087,8 +1094,12 @@ public final class VideoDetailFragment
private void toggleFullscreenIfInFullscreenMode() { private void toggleFullscreenIfInFullscreenMode() {
// If a user watched video inside fullscreen mode and than chose another player // If a user watched video inside fullscreen mode and than chose another player
// return to non-fullscreen mode // return to non-fullscreen mode
if (isPlayerAvailable() && player.isFullscreen()) { if (isPlayerAvailable()) {
player.toggleFullscreen(); player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
if (playerUi.isFullscreen()) {
playerUi.toggleFullscreen();
}
});
} }
} }
@ -1214,16 +1225,10 @@ public final class VideoDetailFragment
} }
final PlayQueue queue = setupPlayQueueForIntent(false); final PlayQueue queue = setupPlayQueueForIntent(false);
// Video view can have elements visible from popup,
// We hide it here but once it ready the view will be shown in handleIntent()
if (playerService.getView() != null) {
playerService.getView().setVisibility(View.GONE);
}
addVideoPlayerView(); addVideoPlayerView();
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(), final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
MainPlayer.class, queue, true, autoPlayEnabled); PlayerService.class, queue, true, autoPlayEnabled);
ContextCompat.startForegroundService(activity, playerIntent); ContextCompat.startForegroundService(activity, playerIntent);
} }
@ -1235,8 +1240,8 @@ public final class VideoDetailFragment
* be reused in a few milliseconds and the flickering would be annoying. * be reused in a few milliseconds and the flickering would be annoying.
*/ */
private void hideMainPlayerOnLoadingNewStream() { private void hideMainPlayerOnLoadingNewStream() {
if (!isPlayerServiceAvailable() //noinspection SimplifyOptionalCallChains
|| playerService.getView() == null if (!isPlayerServiceAvailable() || !getRoot().isPresent()
|| !player.videoPlayerSelected()) { || !player.videoPlayerSelected()) {
return; return;
} }
@ -1244,7 +1249,7 @@ public final class VideoDetailFragment
removeVideoPlayerView(); removeVideoPlayerView();
if (isAutoplayEnabled()) { if (isAutoplayEnabled()) {
playerService.stopForImmediateReusing(); playerService.stopForImmediateReusing();
playerService.getView().setVisibility(View.GONE); getRoot().ifPresent(view -> view.setVisibility(View.GONE));
} else { } else {
playerHolder.stopService(); playerHolder.stopService();
} }
@ -1302,26 +1307,33 @@ public final class VideoDetailFragment
} }
private void addVideoPlayerView() { private void addVideoPlayerView() {
if (!isPlayerAvailable() || getView() == null) { if (!isPlayerAvailable()) {
return; return;
} }
// Check if viewHolder already contains a child final Optional<View> root = player.UIs().get(VideoPlayerUi.class)
if (player.getRootView().getParent() != binding.playerPlaceholder) { .map(VideoPlayerUi::getBinding)
.map(ViewBinding::getRoot);
// Check if viewHolder already contains a child TODO TODO whaat
/*if (playerService != null
&& root.map(View::getParent).orElse(null) != binding.playerPlaceholder) {
playerService.removeViewFromParent(); playerService.removeViewFromParent();
} }*/
setHeightThumbnail(); setHeightThumbnail();
// Prevent from re-adding a view multiple times // Prevent from re-adding a view multiple times
if (player.getRootView().getParent() == null) { if (root.isPresent() && root.get().getParent() == null) {
binding.playerPlaceholder.addView(player.getRootView()); binding.playerPlaceholder.addView(root.get());
} }
} }
private void removeVideoPlayerView() { private void removeVideoPlayerView() {
makeDefaultHeightForVideoPlaceholder(); makeDefaultHeightForVideoPlaceholder();
playerService.removeViewFromParent(); if (player != null) {
player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
}
} }
private void makeDefaultHeightForVideoPlaceholder() { private void makeDefaultHeightForVideoPlaceholder() {
@ -1362,7 +1374,7 @@ public final class VideoDetailFragment
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels; final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener); requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
if (isPlayerAvailable() && player.isFullscreen()) { if (isFullscreen()) {
final int height = (DeviceUtils.isInMultiWindow(activity) final int height = (DeviceUtils.isInMultiWindow(activity)
? requireView() ? requireView()
: activity.getWindow().getDecorView()).getHeight(); : activity.getWindow().getDecorView()).getHeight();
@ -1387,8 +1399,9 @@ public final class VideoDetailFragment
binding.detailThumbnailImageView.setMinimumHeight(newHeight); binding.detailThumbnailImageView.setMinimumHeight(newHeight);
if (isPlayerAvailable()) { if (isPlayerAvailable()) {
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT); final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
player.getSurfaceView() player.UIs().get(VideoPlayerUi.class).ifPresent(ui ->
.setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight); ui.getBinding().surfaceView.setHeights(newHeight,
ui.isFullscreen() ? newHeight : maxHeight));
} }
} }
@ -1517,7 +1530,7 @@ public final class VideoDetailFragment
if (binding.relatedItemsLayout != null) { if (binding.relatedItemsLayout != null) {
if (showRelatedItems) { if (showRelatedItems) {
binding.relatedItemsLayout.setVisibility( binding.relatedItemsLayout.setVisibility(
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.INVISIBLE); isFullscreen() ? View.GONE : View.INVISIBLE);
} else { } else {
binding.relatedItemsLayout.setVisibility(View.GONE); binding.relatedItemsLayout.setVisibility(View.GONE);
} }
@ -1778,6 +1791,14 @@ public final class VideoDetailFragment
// Player event listener // Player event listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override
public void onViewCreated() {
// Video view can have elements visible from popup,
// We hide it here but once it ready the view will be shown in handleIntent()
getRoot().ifPresent(view -> view.setVisibility(View.GONE));
addVideoPlayerView();
}
@Override @Override
public void onQueueUpdate(final PlayQueue queue) { public void onQueueUpdate(final PlayQueue queue) {
playQueue = queue; playQueue = queue;
@ -1898,15 +1919,10 @@ public final class VideoDetailFragment
@Override @Override
public void onFullscreenStateChanged(final boolean fullscreen) { public void onFullscreenStateChanged(final boolean fullscreen) {
setupBrightness(); setupBrightness();
//noinspection SimplifyOptionalCallChains
if (!isPlayerAndPlayerServiceAvailable() if (!isPlayerAndPlayerServiceAvailable()
|| playerService.getView() == null || !player.UIs().get(MainPlayerUi.class).isPresent()
|| player.getParentActivity() == null) { || getRoot().map(View::getParent).orElse(null) == null) {
return;
}
final View view = playerService.getView();
final ViewGroup parent = (ViewGroup) view.getParent();
if (parent == null) {
return; return;
} }
@ -1934,7 +1950,7 @@ public final class VideoDetailFragment
final boolean isLandscape = DeviceUtils.isLandscape(requireContext()); final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
if (DeviceUtils.isTablet(activity) if (DeviceUtils.isTablet(activity)
&& (!globalScreenOrientationLocked(activity) || isLandscape)) { && (!globalScreenOrientationLocked(activity) || isLandscape)) {
player.toggleFullscreen(); player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
return; return;
} }
@ -2017,7 +2033,7 @@ public final class VideoDetailFragment
} }
activity.getWindow().getDecorView().setSystemUiVisibility(visibility); activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
if (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen())) { if (isInMultiWindow || isFullscreen()) {
activity.getWindow().setStatusBarColor(Color.TRANSPARENT); activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
} }
@ -2026,13 +2042,17 @@ public final class VideoDetailFragment
// Listener implementation // Listener implementation
public void hideSystemUiIfNeeded() { public void hideSystemUiIfNeeded() {
if (isPlayerAvailable() if (isFullscreen()
&& player.isFullscreen()
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { && bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
hideSystemUi(); hideSystemUi();
} }
} }
private boolean isFullscreen() {
return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class)
.map(VideoPlayerUi::isFullscreen).orElse(false);
}
private boolean playerIsNotStopped() { private boolean playerIsNotStopped() {
return isPlayerAvailable() && !player.isStopped(); return isPlayerAvailable() && !player.isStopped();
} }
@ -2055,10 +2075,7 @@ public final class VideoDetailFragment
} }
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes(); final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
if (!isPlayerAvailable() if (!isFullscreen() || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
|| !player.videoPlayerSelected()
|| !player.isFullscreen()
|| bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
// Apply system brightness when the player is not in fullscreen // Apply system brightness when the player is not in fullscreen
restoreDefaultBrightness(); restoreDefaultBrightness();
} else { } else {
@ -2082,7 +2099,7 @@ public final class VideoDetailFragment
setAutoPlay(true); setAutoPlay(true);
} }
player.checkLandscape(); player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape);
// Let's give a user time to look at video information page if video is not playing // Let's give a user time to look at video information page if video is not playing
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) { if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
player.play(); player.play();
@ -2309,10 +2326,10 @@ public final class VideoDetailFragment
if (DeviceUtils.isLandscape(requireContext()) if (DeviceUtils.isLandscape(requireContext())
&& isPlayerAvailable() && isPlayerAvailable()
&& player.isPlaying() && player.isPlaying()
&& !player.isFullscreen() && !isFullscreen()
&& !DeviceUtils.isTablet(activity) && !DeviceUtils.isTablet(activity)) {
&& player.videoPlayerSelected()) { player.UIs().get(MainPlayerUi.class)
player.toggleFullscreen(); .ifPresent(MainPlayerUi::toggleFullscreen);
} }
setOverlayLook(binding.appBarLayout, behavior, 1); setOverlayLook(binding.appBarLayout, behavior, 1);
break; break;
@ -2325,17 +2342,22 @@ public final class VideoDetailFragment
// Re-enable clicks // Re-enable clicks
setOverlayElementsClickable(true); setOverlayElementsClickable(true);
if (isPlayerAvailable()) { if (isPlayerAvailable()) {
player.closeItemsList(); player.UIs().get(MainPlayerUi.class)
.ifPresent(MainPlayerUi::closeItemsList);
} }
setOverlayLook(binding.appBarLayout, behavior, 0); setOverlayLook(binding.appBarLayout, behavior, 0);
break; break;
case BottomSheetBehavior.STATE_DRAGGING: case BottomSheetBehavior.STATE_DRAGGING:
case BottomSheetBehavior.STATE_SETTLING: case BottomSheetBehavior.STATE_SETTLING:
if (isPlayerAvailable() && player.isFullscreen()) { if (isFullscreen()) {
showSystemUi(); showSystemUi();
} }
if (isPlayerAvailable() && player.isControlsVisible()) { if (isPlayerAvailable()) {
player.hideControls(0, 0); player.UIs().get(MainPlayerUi.class).ifPresent(ui -> {
if (ui.isControlsVisible()) {
ui.hideControls(0, 0);
}
});
} }
break; break;
} }
@ -2409,4 +2431,13 @@ public final class VideoDetailFragment
boolean isPlayerAndPlayerServiceAvailable() { boolean isPlayerAndPlayerServiceAvailable() {
return (player != null && playerService != null); return (player != null && playerService != null);
} }
public Optional<View> getRoot() {
if (player == null) {
return Optional.empty();
}
return player.UIs().get(VideoPlayerUi.class)
.map(playerUi -> playerUi.getBinding().getRoot());
}
} }

View file

@ -43,7 +43,7 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.local.feed.notifications.NotificationHelper; import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
import org.schabi.newpipe.player.MainPlayer.PlayerType; import org.schabi.newpipe.player.PlayerService.PlayerType;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;

View file

@ -43,7 +43,7 @@ import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.player.MainPlayer.PlayerType; import org.schabi.newpipe.player.PlayerService.PlayerType;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;

View file

@ -43,7 +43,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.local.BaseLocalListFragment; import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainPlayer.PlayerType; import org.schabi.newpipe.player.PlayerService.PlayerType;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;

View file

@ -26,14 +26,14 @@ import java.util.List;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE; import static org.schabi.newpipe.player.PlayerService.ACTION_CLOSE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD; import static org.schabi.newpipe.player.PlayerService.ACTION_FAST_FORWARD;
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND; import static org.schabi.newpipe.player.PlayerService.ACTION_FAST_REWIND;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_NEXT;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PAUSE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PREVIOUS;
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; import static org.schabi.newpipe.player.PlayerService.ACTION_REPEAT;
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; import static org.schabi.newpipe.player.PlayerService.ACTION_SHUFFLE;
/** /**
* This is a utility class for player notifications. * This is a utility class for player notifications.
@ -173,7 +173,7 @@ public final class NotificationUtil {
} }
void createNotificationAndStartForeground(final Player player, final Service service) { public void createNotificationAndStartForeground(final Player player, final Service service) {
if (notificationBuilder == null) { if (notificationBuilder == null) {
notificationBuilder = createNotification(player); notificationBuilder = createNotification(player);
} }

View file

@ -51,7 +51,9 @@ public final class PlayQueueActivity extends AppCompatActivity
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
protected Player player; private Player player;
private PlayQueueAdapter adapter = null;
private boolean serviceBound; private boolean serviceBound;
private ServiceConnection serviceConnection; private ServiceConnection serviceConnection;
@ -132,7 +134,7 @@ public final class PlayQueueActivity extends AppCompatActivity
openPlaybackParameterDialog(); openPlaybackParameterDialog();
return true; return true;
case R.id.action_mute: case R.id.action_mute:
player.onMuteUnmuteButtonClicked(); player.toggleMute();
return true; return true;
case R.id.action_system_audio: case R.id.action_system_audio:
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS)); startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
@ -168,7 +170,7 @@ public final class PlayQueueActivity extends AppCompatActivity
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
private void bind() { private void bind() {
final Intent bindIntent = new Intent(this, MainPlayer.class); final Intent bindIntent = new Intent(this, PlayerService.class);
final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE); final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
if (!success) { if (!success) {
unbindService(serviceConnection); unbindService(serviceConnection);
@ -184,10 +186,7 @@ public final class PlayQueueActivity extends AppCompatActivity
player.removeActivityListener(this); player.removeActivityListener(this);
} }
if (player != null && player.getPlayQueueAdapter() != null) { onQueueUpdate(null);
player.getPlayQueueAdapter().unsetSelectedListener();
}
queueControlBinding.playQueue.setAdapter(null);
if (itemTouchHelper != null) { if (itemTouchHelper != null) {
itemTouchHelper.attachToRecyclerView(null); itemTouchHelper.attachToRecyclerView(null);
} }
@ -210,15 +209,15 @@ public final class PlayQueueActivity extends AppCompatActivity
if (service instanceof PlayerServiceBinder) { if (service instanceof PlayerServiceBinder) {
player = ((PlayerServiceBinder) service).getPlayerInstance(); player = ((PlayerServiceBinder) service).getPlayerInstance();
} else if (service instanceof MainPlayer.LocalBinder) { } else if (service instanceof PlayerService.LocalBinder) {
player = ((MainPlayer.LocalBinder) service).getPlayer(); player = ((PlayerService.LocalBinder) service).getPlayer();
} }
if (player == null || player.getPlayQueue() == null if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
|| player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) {
unbind(); unbind();
finish(); finish();
} else { } else {
onQueueUpdate(player.getPlayQueue());
buildComponents(); buildComponents();
if (player != null) { if (player != null) {
player.setActivityListener(PlayQueueActivity.this); player.setActivityListener(PlayQueueActivity.this);
@ -241,7 +240,6 @@ public final class PlayQueueActivity extends AppCompatActivity
private void buildQueue() { private void buildQueue() {
queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this)); queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this));
queueControlBinding.playQueue.setAdapter(player.getPlayQueueAdapter());
queueControlBinding.playQueue.setClickable(true); queueControlBinding.playQueue.setClickable(true);
queueControlBinding.playQueue.setLongClickable(true); queueControlBinding.playQueue.setLongClickable(true);
queueControlBinding.playQueue.clearOnScrollListeners(); queueControlBinding.playQueue.clearOnScrollListeners();
@ -249,8 +247,6 @@ public final class PlayQueueActivity extends AppCompatActivity
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue); itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue);
player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener());
} }
private void buildMetadata() { private void buildMetadata() {
@ -370,7 +366,7 @@ public final class PlayQueueActivity extends AppCompatActivity
} }
if (view.getId() == queueControlBinding.controlRepeat.getId()) { if (view.getId() == queueControlBinding.controlRepeat.getId()) {
player.onRepeatClicked(); player.cycleNextRepeatMode();
} else if (view.getId() == queueControlBinding.controlBackward.getId()) { } else if (view.getId() == queueControlBinding.controlBackward.getId()) {
player.playPrevious(); player.playPrevious();
} else if (view.getId() == queueControlBinding.controlFastRewind.getId()) { } else if (view.getId() == queueControlBinding.controlFastRewind.getId()) {
@ -382,7 +378,7 @@ public final class PlayQueueActivity extends AppCompatActivity
} else if (view.getId() == queueControlBinding.controlForward.getId()) { } else if (view.getId() == queueControlBinding.controlForward.getId()) {
player.playNext(); player.playNext();
} else if (view.getId() == queueControlBinding.controlShuffle.getId()) { } else if (view.getId() == queueControlBinding.controlShuffle.getId()) {
player.onShuffleClicked(); player.toggleShuffleModeEnabled();
} else if (view.getId() == queueControlBinding.metadata.getId()) { } else if (view.getId() == queueControlBinding.metadata.getId()) {
scrollToSelected(); scrollToSelected();
} else if (view.getId() == queueControlBinding.liveSync.getId()) { } else if (view.getId() == queueControlBinding.liveSync.getId()) {
@ -445,7 +441,15 @@ public final class PlayQueueActivity extends AppCompatActivity
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@Override @Override
public void onQueueUpdate(final PlayQueue queue) { public void onQueueUpdate(@Nullable final PlayQueue queue) {
if (queue == null) {
adapter = null;
queueControlBinding.playQueue.setAdapter(null);
} else {
adapter = new PlayQueueAdapter(this, queue);
adapter.setSelectedListener(getOnSelectedListener());
queueControlBinding.playQueue.setAdapter(adapter);
}
} }
@Override @Override
@ -454,7 +458,6 @@ public final class PlayQueueActivity extends AppCompatActivity
onStateChanged(state); onStateChanged(state);
onPlayModeChanged(repeatMode, shuffled); onPlayModeChanged(repeatMode, shuffled);
onPlaybackParameterChanged(parameters); onPlaybackParameterChanged(parameters);
onMaybePlaybackAdapterChanged();
onMaybeMuteChanged(); onMaybeMuteChanged();
} }
@ -582,17 +585,6 @@ public final class PlayQueueActivity extends AppCompatActivity
} }
} }
private void onMaybePlaybackAdapterChanged() {
if (player == null) {
return;
}
final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter();
if (maybeNewAdapter != null
&& queueControlBinding.playQueue.getAdapter() != maybeNewAdapter) {
queueControlBinding.playQueue.setAdapter(maybeNewAdapter);
}
}
private void onMaybeMuteChanged() { private void onMaybeMuteChanged() {
if (menu != null && player != null) { if (menu != null && player != null) {
final MenuItem item = menu.findItem(R.id.action_mute); final MenuItem item = menu.findItem(R.id.action_mute);

File diff suppressed because it is too large Load diff

View file

@ -19,44 +19,35 @@
package org.schabi.newpipe.player; package org.schabi.newpipe.player;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Service; import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Binder; import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.App; import org.schabi.newpipe.App;
import org.schabi.newpipe.databinding.PlayerBinding; import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
/** /**
* One service for all players. * One service for all players.
* *
* @author mauriciocolli * @author mauriciocolli
*/ */
public final class MainPlayer extends Service { public final class PlayerService extends Service {
private static final String TAG = "MainPlayer"; private static final String TAG = PlayerService.class.getSimpleName();
private static final boolean DEBUG = Player.DEBUG; private static final boolean DEBUG = Player.DEBUG;
private Player player; private Player player;
private WindowManager windowManager;
private final IBinder mBinder = new MainPlayer.LocalBinder(); private final IBinder mBinder = new PlayerService.LocalBinder();
public enum PlayerType { public enum PlayerType {
VIDEO, MAIN,
AUDIO, AUDIO,
POPUP POPUP
} }
@ -67,7 +58,7 @@ public final class MainPlayer extends Service {
static final String ACTION_CLOSE static final String ACTION_CLOSE
= App.PACKAGE_NAME + ".player.MainPlayer.CLOSE"; = App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
static final String ACTION_PLAY_PAUSE public static final String ACTION_PLAY_PAUSE
= App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE"; = App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
static final String ACTION_REPEAT static final String ACTION_REPEAT
= App.PACKAGE_NAME + ".player.MainPlayer.REPEAT"; = App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
@ -94,19 +85,12 @@ public final class MainPlayer extends Service {
Log.d(TAG, "onCreate() called"); Log.d(TAG, "onCreate() called");
} }
assureCorrectAppLanguage(this); assureCorrectAppLanguage(this);
windowManager = ContextCompat.getSystemService(this, WindowManager.class);
ThemeHelper.setTheme(this); ThemeHelper.setTheme(this);
createView();
}
private void createView() {
final PlayerBinding binding = PlayerBinding.inflate(LayoutInflater.from(this));
player = new Player(this); player = new Player(this);
player.setupFromView(binding); /*final MainPlayerUi mainPlayerUi = new MainPlayerUi(player,
PlayerBinding.inflate(LayoutInflater.from(this)));
NotificationUtil.getInstance().createNotificationAndStartForeground(player, this); player.UIs().add(mainPlayerUi);*/
} }
@Override @Override
@ -121,11 +105,6 @@ public final class MainPlayer extends Service {
return START_NOT_STICKY; return START_NOT_STICKY;
} }
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|| intent.getStringExtra(Player.PLAY_QUEUE_KEY) != null) {
NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
}
player.handleIntent(intent); player.handleIntent(intent);
if (player.getMediaSessionManager() != null) { if (player.getMediaSessionManager() != null) {
player.getMediaSessionManager().handleMediaButtonIntent(intent); player.getMediaSessionManager().handleMediaButtonIntent(intent);
@ -144,13 +123,7 @@ public final class MainPlayer extends Service {
// Releases wifi & cpu, disables keepScreenOn, etc. // Releases wifi & cpu, disables keepScreenOn, etc.
// We can't just pause the player here because it will make transition // We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth // from one stream to a new stream not smooth
player.smoothStopPlayer(); player.smoothStopForImmediateReusing();
player.setRecovery();
// Android TV will handle back button in case controls will be visible
// (one more additional unneeded click while the player is hidden)
player.hideControls(0, 0);
player.closeItemsList();
// Notification shows information about old stream but if a user selects // Notification shows information about old stream but if a user selects
// a stream from backStack it's not actual anymore // a stream from backStack it's not actual anymore
@ -180,18 +153,7 @@ public final class MainPlayer extends Service {
private void cleanup() { private void cleanup() {
if (player != null) { if (player != null) {
// Exit from fullscreen when user closes the player via notification
if (player.isFullscreen()) {
player.toggleFullscreen();
}
removeViewFromParent();
player.saveStreamProgressState();
player.setRecovery();
player.stopActivityBinding();
player.removePopupFromView();
player.destroy(); player.destroy();
player = null; player = null;
} }
} }
@ -212,48 +174,14 @@ public final class MainPlayer extends Service {
return mBinder; return mBinder;
} }
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
boolean isLandscape() {
// DisplayMetrics from activity context knows about MultiWindow feature
// while DisplayMetrics from app context doesn't
return DeviceUtils.isLandscape(player != null && player.getParentActivity() != null
? player.getParentActivity() : this);
}
@Nullable
public View getView() {
if (player == null) {
return null;
}
return player.getRootView();
}
public void removeViewFromParent() {
if (getView() != null && getView().getParent() != null) {
if (player.getParentActivity() != null) {
// This means view was added to fragment
final ViewGroup parent = (ViewGroup) getView().getParent();
parent.removeView(getView());
} else {
// This means view was added by windowManager for popup player
windowManager.removeViewImmediate(getView());
}
}
}
public class LocalBinder extends Binder { public class LocalBinder extends Binder {
public MainPlayer getService() { public PlayerService getService() {
return MainPlayer.this; return PlayerService.this;
} }
public Player getPlayer() { public Player getPlayer() {
return MainPlayer.this.player; return PlayerService.this.player;
} }
} }
} }

View file

@ -1,520 +0,0 @@
package org.schabi.newpipe.player.event
import android.content.Context
import android.os.Handler
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.player.MainPlayer
import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.helper.PlayerHelper.savePopupPositionAndSizeToPrefs
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.max
import kotlin.math.min
/**
* Base gesture handling for [Player]
*
* This class contains the logic for the player gestures like View preparations
* and provides some abstract methods to make it easier separating the logic from the UI.
*/
abstract class BasePlayerGestureListener(
@JvmField
protected val player: Player,
@JvmField
protected val service: MainPlayer
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
// ///////////////////////////////////////////////////////////////////
// Abstract methods for VIDEO and POPUP
// ///////////////////////////////////////////////////////////////////
abstract fun onDoubleTap(event: MotionEvent, portion: DisplayPortion)
abstract fun onSingleTap(playerType: MainPlayer.PlayerType)
abstract fun onScroll(
playerType: MainPlayer.PlayerType,
portion: DisplayPortion,
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
)
abstract fun onScrollEnd(playerType: MainPlayer.PlayerType, event: MotionEvent)
// ///////////////////////////////////////////////////////////////////
// Abstract methods for POPUP (exclusive)
// ///////////////////////////////////////////////////////////////////
abstract fun onPopupResizingStart()
abstract fun onPopupResizingEnd()
private var initialPopupX: Int = -1
private var initialPopupY: Int = -1
private var isMovingInMain = false
private var isMovingInPopup = false
private var isResizing = false
private val tossFlingVelocity = PlayerHelper.getTossFlingVelocity()
// [popup] initial coordinates and distance between fingers
private var initPointerDistance = -1.0
private var initFirstPointerX = -1f
private var initFirstPointerY = -1f
private var initSecPointerX = -1f
private var initSecPointerY = -1f
// ///////////////////////////////////////////////////////////////////
// onTouch implementation
// ///////////////////////////////////////////////////////////////////
override fun onTouch(v: View, event: MotionEvent): Boolean {
return if (player.popupPlayerSelected()) {
onTouchInPopup(v, event)
} else {
onTouchInMain(v, event)
}
}
private fun onTouchInMain(v: View, event: MotionEvent): Boolean {
player.gestureDetector.onTouchEvent(event)
if (event.action == MotionEvent.ACTION_UP && isMovingInMain) {
isMovingInMain = false
onScrollEnd(MainPlayer.PlayerType.VIDEO, event)
}
return when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
v.parent.requestDisallowInterceptTouchEvent(player.isFullscreen)
true
}
MotionEvent.ACTION_UP -> {
v.parent.requestDisallowInterceptTouchEvent(false)
false
}
else -> true
}
}
private fun onTouchInPopup(v: View, event: MotionEvent): Boolean {
player.gestureDetector.onTouchEvent(event)
if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
}
onPopupResizingStart()
// record coordinates of fingers
initFirstPointerX = event.getX(0)
initFirstPointerY = event.getY(0)
initSecPointerX = event.getX(1)
initSecPointerY = event.getY(1)
// record distance between fingers
initPointerDistance = hypot(
initFirstPointerX - initSecPointerX.toDouble(),
initFirstPointerY - initSecPointerY.toDouble()
)
isResizing = true
}
if (event.action == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) {
if (DEBUG) {
Log.d(
TAG,
"onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" +
"[${event.rawX}, ${event.rawY}]"
)
}
return handleMultiDrag(event)
}
if (event.action == MotionEvent.ACTION_UP) {
if (DEBUG) {
Log.d(
TAG,
"onTouch() ACTION_UP > v = [$v], e1.getRaw =" +
" [${event.rawX}, ${event.rawY}]"
)
}
if (isMovingInPopup) {
isMovingInPopup = false
onScrollEnd(MainPlayer.PlayerType.POPUP, event)
}
if (isResizing) {
isResizing = false
initPointerDistance = (-1).toDouble()
initFirstPointerX = (-1).toFloat()
initFirstPointerY = (-1).toFloat()
initSecPointerX = (-1).toFloat()
initSecPointerY = (-1).toFloat()
onPopupResizingEnd()
player.changeState(player.currentState)
}
if (!player.isPopupClosing) {
savePopupPositionAndSizeToPrefs(player)
}
}
v.performClick()
return true
}
private fun handleMultiDrag(event: MotionEvent): Boolean {
if (initPointerDistance != -1.0 && event.pointerCount == 2) {
// get the movements of the fingers
val firstPointerMove = hypot(
event.getX(0) - initFirstPointerX.toDouble(),
event.getY(0) - initFirstPointerY.toDouble()
)
val secPointerMove = hypot(
event.getX(1) - initSecPointerX.toDouble(),
event.getY(1) - initSecPointerY.toDouble()
)
// minimum threshold beyond which pinch gesture will work
val minimumMove = ViewConfiguration.get(service).scaledTouchSlop
if (max(firstPointerMove, secPointerMove) > minimumMove) {
// calculate current distance between the pointers
val currentPointerDistance = hypot(
event.getX(0) - event.getX(1).toDouble(),
event.getY(0) - event.getY(1).toDouble()
)
val popupWidth = player.popupLayoutParams!!.width.toDouble()
// change co-ordinates of popup so the center stays at the same position
val newWidth = popupWidth * currentPointerDistance / initPointerDistance
initPointerDistance = currentPointerDistance
player.popupLayoutParams!!.x += ((popupWidth - newWidth) / 2.0).toInt()
player.checkPopupPositionBounds()
player.updateScreenSize()
player.changePopupSize(min(player.screenWidth.toDouble(), newWidth).toInt())
return true
}
}
return false
}
// ///////////////////////////////////////////////////////////////////
// Simple gestures
// ///////////////////////////////////////////////////////////////////
override fun onDown(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDown called with e = [$e]")
if (isDoubleTapping && isDoubleTapEnabled) {
doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
return true
}
return if (player.popupPlayerSelected())
onDownInPopup(e)
else
true
}
private fun onDownInPopup(e: MotionEvent): Boolean {
// Fix popup position when the user touch it, it may have the wrong one
// because the soft input is visible (the draggable area is currently resized).
player.updateScreenSize()
player.checkPopupPositionBounds()
player.popupLayoutParams?.let {
initialPopupX = it.x
initialPopupY = it.y
}
return super.onDown(e)
}
override fun onDoubleTap(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDoubleTap called with e = [$e]")
onDoubleTap(e, getDisplayPortion(e))
return true
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
if (isDoubleTapping)
return true
if (player.popupPlayerSelected()) {
if (player.exoPlayerIsNull())
return false
onSingleTap(MainPlayer.PlayerType.POPUP)
return true
} else {
super.onSingleTapConfirmed(e)
if (player.currentState == Player.STATE_BLOCKED)
return true
onSingleTap(MainPlayer.PlayerType.VIDEO)
}
return true
}
override fun onLongPress(e: MotionEvent?) {
if (player.popupPlayerSelected()) {
player.updateScreenSize()
player.checkPopupPositionBounds()
player.changePopupSize(player.screenWidth.toInt())
}
}
override fun onScroll(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
return if (player.popupPlayerSelected()) {
onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY)
} else {
onScrollInMain(initialEvent, movingEvent, distanceX, distanceY)
}
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
return if (player.popupPlayerSelected()) {
val absVelocityX = abs(velocityX)
val absVelocityY = abs(velocityY)
if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) {
if (absVelocityX > tossFlingVelocity) {
player.popupLayoutParams!!.x = velocityX.toInt()
}
if (absVelocityY > tossFlingVelocity) {
player.popupLayoutParams!!.y = velocityY.toInt()
}
player.checkPopupPositionBounds()
player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
return true
}
return false
} else {
true
}
}
private fun onScrollInMain(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!player.isFullscreen) {
return false
}
val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service)
val isTouchingNavigationBar: Boolean =
initialEvent.y > (player.rootView.height - getNavigationBarHeight(service))
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false
}
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
if (
!isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
player.currentState == Player.STATE_COMPLETED
) {
return false
}
isMovingInMain = true
onScroll(
MainPlayer.PlayerType.VIDEO,
getDisplayHalfPortion(initialEvent),
initialEvent,
movingEvent,
distanceX,
distanceY
)
return true
}
private fun onScrollInPopup(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (isResizing) {
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
}
if (!isMovingInPopup) {
player.closeOverlayButton.animate(true, 200)
}
isMovingInPopup = true
val diffX: Float = (movingEvent.rawX - initialEvent.rawX)
var posX: Float = (initialPopupX + diffX)
val diffY: Float = (movingEvent.rawY - initialEvent.rawY)
var posY: Float = (initialPopupY + diffY)
if (posX > player.screenWidth - player.popupLayoutParams!!.width) {
posX = (player.screenWidth - player.popupLayoutParams!!.width)
} else if (posX < 0) {
posX = 0f
}
if (posY > player.screenHeight - player.popupLayoutParams!!.height) {
posY = (player.screenHeight - player.popupLayoutParams!!.height)
} else if (posY < 0) {
posY = 0f
}
player.popupLayoutParams!!.x = posX.toInt()
player.popupLayoutParams!!.y = posY.toInt()
onScroll(
MainPlayer.PlayerType.POPUP,
getDisplayHalfPortion(initialEvent),
initialEvent,
movingEvent,
distanceX,
distanceY
)
player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
return true
}
// ///////////////////////////////////////////////////////////////////
// Multi double tapping
// ///////////////////////////////////////////////////////////////////
var doubleTapControls: DoubleTapListener? = null
private set
private val isDoubleTapEnabled: Boolean
get() = doubleTapDelay > 0
var isDoubleTapping = false
private set
fun doubleTapControls(listener: DoubleTapListener) = apply {
doubleTapControls = listener
}
private var doubleTapDelay = DOUBLE_TAP_DELAY
private val doubleTapHandler: Handler = Handler()
private val doubleTapRunnable = Runnable {
if (DEBUG)
Log.d(TAG, "doubleTapRunnable called")
isDoubleTapping = false
doubleTapControls?.onDoubleTapFinished()
}
fun startMultiDoubleTap(e: MotionEvent) {
if (!isDoubleTapping) {
if (DEBUG)
Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
keepInDoubleTapMode()
doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
}
}
fun keepInDoubleTapMode() {
if (DEBUG)
Log.d(TAG, "keepInDoubleTapMode called")
isDoubleTapping = true
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay)
}
fun endMultiDoubleTap() {
if (DEBUG)
Log.d(TAG, "endMultiDoubleTap called")
isDoubleTapping = false
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapControls?.onDoubleTapFinished()
}
// ///////////////////////////////////////////////////////////////////
// Utils
// ///////////////////////////////////////////////////////////////////
private fun getDisplayPortion(e: MotionEvent): DisplayPortion {
return if (player.playerType == MainPlayer.PlayerType.POPUP && player.popupLayoutParams != null) {
when {
e.x < player.popupLayoutParams!!.width / 3.0 -> DisplayPortion.LEFT
e.x > player.popupLayoutParams!!.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
} else /* MainPlayer.PlayerType.VIDEO */ {
when {
e.x < player.rootView.width / 3.0 -> DisplayPortion.LEFT
e.x > player.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
}
}
// Currently needed for scrolling since there is no action more the middle portion
private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
return if (player.playerType == MainPlayer.PlayerType.POPUP) {
when {
e.x < player.popupLayoutParams!!.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
} else /* MainPlayer.PlayerType.VIDEO */ {
when {
e.x < player.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
}
}
private fun getNavigationBarHeight(context: Context): Int {
val resId = context.resources
.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resId > 0) {
context.resources.getDimensionPixelSize(resId)
} else 0
}
private fun getStatusBarHeight(context: Context): Int {
val resId = context.resources
.getIdentifier("status_bar_height", "dimen", "android")
return if (resId > 0) {
context.resources.getDimensionPixelSize(resId)
} else 0
}
companion object {
private const val TAG = "BasePlayerGestListener"
private val DEBUG = Player.DEBUG
private const val DOUBLE_TAP_DELAY = 550L
private const val MOVEMENT_THRESHOLD = 40
}
}

View file

@ -1,6 +1,5 @@
package org.schabi.newpipe.player.event; package org.schabi.newpipe.player.event;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;

View file

@ -1,256 +0,0 @@
package org.schabi.newpipe.player.event;
import static org.schabi.newpipe.ktx.AnimationType.ALPHA;
import static org.schabi.newpipe.ktx.AnimationType.SCALE_AND_ALPHA;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_DURATION;
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.player.Player.STATE_PLAYING;
import android.app.Activity;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.helper.PlayerHelper;
/**
* GestureListener for the player
*
* While {@link BasePlayerGestureListener} contains the logic behind the single gestures
* this class focuses on the visual aspect like hiding and showing the controls or changing
* volume/brightness during scrolling for specific events.
*/
public class PlayerGestureListener
extends BasePlayerGestureListener
implements View.OnTouchListener {
private static final String TAG = PlayerGestureListener.class.getSimpleName();
private static final boolean DEBUG = MainActivity.DEBUG;
private final int maxVolume;
public PlayerGestureListener(final Player player, final MainPlayer service) {
super(player, service);
maxVolume = player.getAudioReactor().getMaxVolume();
}
@Override
public void onDoubleTap(@NonNull final MotionEvent event,
@NonNull final DisplayPortion portion) {
if (DEBUG) {
Log.d(TAG, "onDoubleTap called with playerType = ["
+ player.getPlayerType() + "], portion = [" + portion + "]");
}
if (player.isSomePopupMenuVisible()) {
player.hideControls(0, 0);
}
if (portion == DisplayPortion.LEFT || portion == DisplayPortion.RIGHT) {
startMultiDoubleTap(event);
} else if (portion == DisplayPortion.MIDDLE) {
player.playPause();
}
}
@Override
public void onSingleTap(@NonNull final MainPlayer.PlayerType playerType) {
if (DEBUG) {
Log.d(TAG, "onSingleTap called with playerType = [" + player.getPlayerType() + "]");
}
if (player.isControlsVisible()) {
player.hideControls(150, 0);
return;
}
// -- Controls are not visible --
// When player is completed show controls and don't hide them later
if (player.getCurrentState() == Player.STATE_COMPLETED) {
player.showControls(0);
} else {
player.showControlsThenHide();
}
}
@Override
public void onScroll(@NonNull final MainPlayer.PlayerType playerType,
@NonNull final DisplayPortion portion,
@NonNull final MotionEvent initialEvent,
@NonNull final MotionEvent movingEvent,
final float distanceX, final float distanceY) {
if (DEBUG) {
Log.d(TAG, "onScroll called with playerType = ["
+ player.getPlayerType() + "], portion = [" + portion + "]");
}
if (playerType == MainPlayer.PlayerType.VIDEO) {
// -- Brightness and Volume control --
final boolean isBrightnessGestureEnabled =
PlayerHelper.isBrightnessGestureEnabled(service);
final boolean isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service);
if (isBrightnessGestureEnabled && isVolumeGestureEnabled) {
if (portion == DisplayPortion.LEFT_HALF) {
onScrollMainBrightness(distanceX, distanceY);
} else /* DisplayPortion.RIGHT_HALF */ {
onScrollMainVolume(distanceX, distanceY);
}
} else if (isBrightnessGestureEnabled) {
onScrollMainBrightness(distanceX, distanceY);
} else if (isVolumeGestureEnabled) {
onScrollMainVolume(distanceX, distanceY);
}
} else /* MainPlayer.PlayerType.POPUP */ {
// -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
final View closingOverlayView = player.getClosingOverlayView();
final boolean showClosingOverlayView = player.isInsideClosingRadius(movingEvent);
// Check if an view is in expected state and if not animate it into the correct state
final int expectedVisibility = showClosingOverlayView ? View.VISIBLE : View.GONE;
if (closingOverlayView.getVisibility() != expectedVisibility) {
animate(closingOverlayView, showClosingOverlayView, 200);
}
}
}
private void onScrollMainVolume(final float distanceX, final float distanceY) {
// If we just started sliding, change the progress bar to match the system volume
if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
final float volumePercent = player
.getAudioReactor().getVolume() / (float) maxVolume;
player.getVolumeProgressBar().setProgress(
(int) (volumePercent * player.getMaxGestureLength()));
}
player.getVolumeProgressBar().incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) player
.getVolumeProgressBar().getProgress() / player.getMaxGestureLength();
final int currentVolume = (int) (maxVolume * currentProgressPercent);
player.getAudioReactor().setVolume(currentVolume);
if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
}
player.getVolumeImageView().setImageDrawable(
AppCompatResources.getDrawable(service, currentProgressPercent <= 0
? R.drawable.ic_volume_off
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down
: R.drawable.ic_volume_up)
);
if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
animate(player.getVolumeRelativeLayout(), true, 200, SCALE_AND_ALPHA);
}
if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
player.getBrightnessRelativeLayout().setVisibility(View.GONE);
}
}
private void onScrollMainBrightness(final float distanceX, final float distanceY) {
final Activity parent = player.getParentActivity();
if (parent == null) {
return;
}
final Window window = parent.getWindow();
final WindowManager.LayoutParams layoutParams = window.getAttributes();
final ProgressBar bar = player.getBrightnessProgressBar();
final float oldBrightness = layoutParams.screenBrightness;
bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness))));
bar.incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) bar.getProgress() / bar.getMax();
layoutParams.screenBrightness = currentProgressPercent;
window.setAttributes(layoutParams);
// Save current brightness level
PlayerHelper.setScreenBrightness(parent, currentProgressPercent);
if (DEBUG) {
Log.d(TAG, "onScroll().brightnessControl, "
+ "currentBrightness = " + currentProgressPercent);
}
player.getBrightnessImageView().setImageDrawable(
AppCompatResources.getDrawable(service,
currentProgressPercent < 0.25
? R.drawable.ic_brightness_low
: currentProgressPercent < 0.75
? R.drawable.ic_brightness_medium
: R.drawable.ic_brightness_high)
);
if (player.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
animate(player.getBrightnessRelativeLayout(), true, 200, SCALE_AND_ALPHA);
}
if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
player.getVolumeRelativeLayout().setVisibility(View.GONE);
}
}
@Override
public void onScrollEnd(@NonNull final MainPlayer.PlayerType playerType,
@NonNull final MotionEvent event) {
if (DEBUG) {
Log.d(TAG, "onScrollEnd called with playerType = ["
+ player.getPlayerType() + "]");
}
if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
if (playerType == MainPlayer.PlayerType.VIDEO) {
if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
animate(player.getVolumeRelativeLayout(), false, 200, SCALE_AND_ALPHA,
200);
}
if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
animate(player.getBrightnessRelativeLayout(), false, 200, SCALE_AND_ALPHA,
200);
}
} else /* Popup-Player */ {
if (player.isInsideClosingRadius(event)) {
player.closePopup();
} else if (!player.isPopupClosing()) {
animate(player.getCloseOverlayButton(), false, 200);
animate(player.getClosingOverlayView(), false, 200);
}
}
}
@Override
public void onPopupResizingStart() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingStart called");
}
player.getLoadingPanel().setVisibility(View.GONE);
player.hideControls(0, 0);
animate(player.getFastSeekOverlay(), false, 0);
animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0);
}
@Override
public void onPopupResizingEnd() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingEnd called");
}
}
}

View file

@ -3,6 +3,8 @@ package org.schabi.newpipe.player.event;
import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackException;
public interface PlayerServiceEventListener extends PlayerEventListener { public interface PlayerServiceEventListener extends PlayerEventListener {
void onViewCreated();
void onFullscreenStateChanged(boolean fullscreen); void onFullscreenStateChanged(boolean fullscreen);
void onScreenRotationButtonClicked(); void onScreenRotationButtonClicked();

View file

@ -1,11 +1,11 @@
package org.schabi.newpipe.player.event; package org.schabi.newpipe.player.event;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener { public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
void onServiceConnected(Player player, void onServiceConnected(Player player,
MainPlayer playerService, PlayerService playerService,
boolean playAfterConnect); boolean playAfterConnect);
void onServiceDisconnected(); void onServiceDisconnected();
} }

View file

@ -0,0 +1,182 @@
package org.schabi.newpipe.player.gesture
import android.os.Handler
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import org.schabi.newpipe.databinding.PlayerBinding
import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.ui.VideoPlayerUi
/**
* Base gesture handling for [Player]
*
* This class contains the logic for the player gestures like View preparations
* and provides some abstract methods to make it easier separating the logic from the UI.
*/
abstract class BasePlayerGestureListener(
private val playerUi: VideoPlayerUi,
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
protected val player: Player = playerUi.player
protected val binding: PlayerBinding = playerUi.binding
override fun onTouch(v: View, event: MotionEvent): Boolean {
playerUi.gestureDetector.onTouchEvent(event)
return false
}
private fun onDoubleTap(
event: MotionEvent,
portion: DisplayPortion
) {
if (DEBUG) {
Log.d(
TAG,
"onDoubleTap called with playerType = [" +
player.playerType + "], portion = [" + portion + "]"
)
}
if (playerUi.isSomePopupMenuVisible) {
playerUi.hideControls(0, 0)
}
if (portion === DisplayPortion.LEFT || portion === DisplayPortion.RIGHT) {
startMultiDoubleTap(event)
} else if (portion === DisplayPortion.MIDDLE) {
player.playPause()
}
}
protected fun onSingleTap() {
if (playerUi.isControlsVisible) {
playerUi.hideControls(150, 0)
return
}
// -- Controls are not visible --
// When player is completed show controls and don't hide them later
if (player.currentState == Player.STATE_COMPLETED) {
playerUi.showControls(0)
} else {
playerUi.showControlsThenHide()
}
}
open fun onScrollEnd(event: MotionEvent) {
if (DEBUG) {
Log.d(
TAG,
"onScrollEnd called with playerType = [" +
player.playerType + "]"
)
}
if (playerUi.isControlsVisible && player.currentState == Player.STATE_PLAYING) {
playerUi.hideControls(
VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
VideoPlayerUi.DEFAULT_CONTROLS_HIDE_TIME
)
}
}
// ///////////////////////////////////////////////////////////////////
// Simple gestures
// ///////////////////////////////////////////////////////////////////
override fun onDown(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDown called with e = [$e]")
if (isDoubleTapping && isDoubleTapEnabled) {
doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
return true
}
return if (onDownNotDoubleTapping(e)) super.onDown(e) else true
}
/**
* @return true if `super.onDown(e)` should be called, false otherwise
*/
open fun onDownNotDoubleTapping(e: MotionEvent): Boolean {
return false // do not call super.onDown(e) by default, overridden for popup player
}
override fun onDoubleTap(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDoubleTap called with e = [$e]")
onDoubleTap(e, getDisplayPortion(e))
return true
}
// ///////////////////////////////////////////////////////////////////
// Multi double tapping
// ///////////////////////////////////////////////////////////////////
private var doubleTapControls: DoubleTapListener? = null
private val isDoubleTapEnabled: Boolean
get() = doubleTapDelay > 0
var isDoubleTapping = false
private set
fun doubleTapControls(listener: DoubleTapListener) = apply {
doubleTapControls = listener
}
private var doubleTapDelay = DOUBLE_TAP_DELAY
private val doubleTapHandler: Handler = Handler()
private val doubleTapRunnable = Runnable {
if (DEBUG)
Log.d(TAG, "doubleTapRunnable called")
isDoubleTapping = false
doubleTapControls?.onDoubleTapFinished()
}
private fun startMultiDoubleTap(e: MotionEvent) {
if (!isDoubleTapping) {
if (DEBUG)
Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
keepInDoubleTapMode()
doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
}
}
fun keepInDoubleTapMode() {
if (DEBUG)
Log.d(TAG, "keepInDoubleTapMode called")
isDoubleTapping = true
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay)
}
fun endMultiDoubleTap() {
if (DEBUG)
Log.d(TAG, "endMultiDoubleTap called")
isDoubleTapping = false
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapControls?.onDoubleTapFinished()
}
// ///////////////////////////////////////////////////////////////////
// Utils
// ///////////////////////////////////////////////////////////////////
abstract fun getDisplayPortion(e: MotionEvent): DisplayPortion
// Currently needed for scrolling since there is no action more the middle portion
abstract fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion
companion object {
private const val TAG = "BasePlayerGestListener"
private val DEBUG = Player.DEBUG
private const val DOUBLE_TAP_DELAY = 550L
}
}

View file

@ -1,4 +1,4 @@
package org.schabi.newpipe.player.event; package org.schabi.newpipe.player.gesture;
import android.content.Context; import android.content.Context;
import android.graphics.Rect; import android.graphics.Rect;

View file

@ -1,4 +1,4 @@
package org.schabi.newpipe.player.event package org.schabi.newpipe.player.gesture
enum class DisplayPortion { enum class DisplayPortion {
LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF

View file

@ -1,4 +1,4 @@
package org.schabi.newpipe.player.event package org.schabi.newpipe.player.gesture
interface DoubleTapListener { interface DoubleTapListener {
fun onDoubleTapStarted(portion: DisplayPortion) {} fun onDoubleTapStarted(portion: DisplayPortion) {}

View file

@ -0,0 +1,232 @@
package org.schabi.newpipe.player.gesture
import android.app.Activity
import android.content.Context
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.widget.ProgressBar
import androidx.appcompat.content.res.AppCompatResources
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.ui.MainPlayerUi
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
/**
* GestureListener for the player
*
* While [BasePlayerGestureListener] contains the logic behind the single gestures
* this class focuses on the visual aspect like hiding and showing the controls or changing
* volume/brightness during scrolling for specific events.
*/
class MainPlayerGestureListener(
private val playerUi: MainPlayerUi
) : BasePlayerGestureListener(playerUi), OnTouchListener {
private val maxVolume: Int = player.audioReactor.maxVolume
private var isMoving = false
override fun onTouch(v: View, event: MotionEvent): Boolean {
super.onTouch(v, event)
if (event.action == MotionEvent.ACTION_UP && isMoving) {
isMoving = false
onScrollEnd(event)
}
return when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
v.parent.requestDisallowInterceptTouchEvent(playerUi.isFullscreen)
true
}
MotionEvent.ACTION_UP -> {
v.parent.requestDisallowInterceptTouchEvent(false)
false
}
else -> true
}
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
if (isDoubleTapping)
return true
super.onSingleTapConfirmed(e)
if (player.currentState != Player.STATE_BLOCKED)
onSingleTap()
return true
}
private fun onScrollVolume(distanceY: Float) {
// If we just started sliding, change the progress bar to match the system volume
if (binding.volumeRelativeLayout.visibility != View.VISIBLE) {
val volumePercent: Float = player.audioReactor.volume / maxVolume.toFloat()
binding.volumeProgressBar.progress = (volumePercent * MAX_GESTURE_LENGTH).toInt()
}
binding.volumeProgressBar.incrementProgressBy(distanceY.toInt())
val currentProgressPercent: Float =
binding.volumeProgressBar.progress.toFloat() / MAX_GESTURE_LENGTH
val currentVolume = (maxVolume * currentProgressPercent).toInt()
player.audioReactor.volume = currentVolume
if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume")
}
binding.volumeImageView.setImageDrawable(
AppCompatResources.getDrawable(
player.context,
when {
currentProgressPercent <= 0 -> R.drawable.ic_volume_off
currentProgressPercent < 0.25 -> R.drawable.ic_volume_mute
currentProgressPercent < 0.75 -> R.drawable.ic_volume_down
else -> R.drawable.ic_volume_up
}
)
)
if (binding.volumeRelativeLayout.visibility != View.VISIBLE) {
binding.volumeRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
}
if (binding.brightnessRelativeLayout.visibility == View.VISIBLE) {
binding.volumeRelativeLayout.visibility = View.GONE
}
}
private fun onScrollBrightness(distanceY: Float) {
val parent: Activity = playerUi.parentActivity
val window = parent.window
val layoutParams = window.attributes
val bar: ProgressBar = binding.brightnessProgressBar
val oldBrightness = layoutParams.screenBrightness
bar.progress = (bar.max * max(0f, min(1f, oldBrightness))).toInt()
bar.incrementProgressBy(distanceY.toInt())
val currentProgressPercent = bar.progress.toFloat() / bar.max
layoutParams.screenBrightness = currentProgressPercent
window.attributes = layoutParams
// Save current brightness level
PlayerHelper.setScreenBrightness(parent, currentProgressPercent)
if (DEBUG) {
Log.d(
TAG,
"onScroll().brightnessControl, " +
"currentBrightness = " + currentProgressPercent
)
}
binding.brightnessImageView.setImageDrawable(
AppCompatResources.getDrawable(
player.context,
if (currentProgressPercent < 0.25) R.drawable.ic_brightness_low else if (currentProgressPercent < 0.75) R.drawable.ic_brightness_medium else R.drawable.ic_brightness_high
)
)
if (binding.brightnessRelativeLayout.visibility != View.VISIBLE) {
binding.brightnessRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
}
if (binding.volumeRelativeLayout.visibility == View.VISIBLE) {
binding.volumeRelativeLayout.visibility = View.GONE
}
}
override fun onScrollEnd(event: MotionEvent) {
super.onScrollEnd(event)
if (binding.volumeRelativeLayout.visibility == View.VISIBLE) {
binding.volumeRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
}
if (binding.brightnessRelativeLayout.visibility == View.VISIBLE) {
binding.brightnessRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
}
}
override fun onScroll(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!playerUi.isFullscreen) {
return false
}
val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(player.context)
val isTouchingNavigationBar: Boolean =
initialEvent.y > (binding.root.height - getNavigationBarHeight(player.context))
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false
}
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
if (
!isMoving && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
player.currentState == Player.STATE_COMPLETED
) {
return false
}
isMoving = true
// -- Brightness and Volume control --
val isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(player.context)
val isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(player.context)
if (isBrightnessGestureEnabled && isVolumeGestureEnabled) {
if (getDisplayHalfPortion(initialEvent) === DisplayPortion.LEFT_HALF) {
onScrollBrightness(distanceY)
} else /* DisplayPortion.RIGHT_HALF */ {
onScrollVolume(distanceY)
}
} else if (isBrightnessGestureEnabled) {
onScrollBrightness(distanceY)
} else if (isVolumeGestureEnabled) {
onScrollVolume(distanceY)
}
return true
}
override fun getDisplayPortion(e: MotionEvent): DisplayPortion {
return when {
e.x < binding.root.width / 3.0 -> DisplayPortion.LEFT
e.x > binding.root.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
}
override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
return when {
e.x < binding.root.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
}
companion object {
private val TAG = MainPlayerGestureListener::class.java.simpleName
private val DEBUG = MainActivity.DEBUG
private const val MOVEMENT_THRESHOLD = 40
const val MAX_GESTURE_LENGTH = 0.75f
private fun getNavigationBarHeight(context: Context): Int {
val resId = context.resources
.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resId > 0) {
context.resources.getDimensionPixelSize(resId)
} else 0
}
private fun getStatusBarHeight(context: Context): Int {
val resId = context.resources
.getIdentifier("status_bar_height", "dimen", "android")
return if (resId > 0) {
context.resources.getDimensionPixelSize(resId)
} else 0
}
}
}

View file

@ -0,0 +1,287 @@
package org.schabi.newpipe.player.gesture
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.ui.PopupPlayerUi
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.max
import kotlin.math.min
class PopupPlayerGestureListener(
private val playerUi: PopupPlayerUi,
) : BasePlayerGestureListener(playerUi) {
private var isMoving = false
private var initialPopupX: Int = -1
private var initialPopupY: Int = -1
private var isResizing = false
// initial coordinates and distance between fingers
private var initPointerDistance = -1.0
private var initFirstPointerX = -1f
private var initFirstPointerY = -1f
private var initSecPointerX = -1f
private var initSecPointerY = -1f
override fun onTouch(v: View, event: MotionEvent): Boolean {
super.onTouch(v, event)
if (event.pointerCount == 2 && !isMoving && !isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
}
onPopupResizingStart()
// record coordinates of fingers
initFirstPointerX = event.getX(0)
initFirstPointerY = event.getY(0)
initSecPointerX = event.getX(1)
initSecPointerY = event.getY(1)
// record distance between fingers
initPointerDistance = hypot(
initFirstPointerX - initSecPointerX.toDouble(),
initFirstPointerY - initSecPointerY.toDouble()
)
isResizing = true
}
if (event.action == MotionEvent.ACTION_MOVE && !isMoving && isResizing) {
if (DEBUG) {
Log.d(
TAG,
"onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" +
"[${event.rawX}, ${event.rawY}]"
)
}
return handleMultiDrag(event)
}
if (event.action == MotionEvent.ACTION_UP) {
if (DEBUG) {
Log.d(
TAG,
"onTouch() ACTION_UP > v = [$v], e1.getRaw =" +
" [${event.rawX}, ${event.rawY}]"
)
}
if (isMoving) {
isMoving = false
onScrollEnd(event)
}
if (isResizing) {
isResizing = false
initPointerDistance = (-1).toDouble()
initFirstPointerX = (-1).toFloat()
initFirstPointerY = (-1).toFloat()
initSecPointerX = (-1).toFloat()
initSecPointerY = (-1).toFloat()
onPopupResizingEnd()
player.changeState(player.currentState)
}
if (!playerUi.isPopupClosing) {
PlayerHelper.savePopupPositionAndSizeToPrefs(playerUi)
}
}
v.performClick()
return true
}
override fun onScrollEnd(event: MotionEvent) {
super.onScrollEnd(event)
if (playerUi.isInsideClosingRadius(event)) {
playerUi.closePopup()
} else if (!playerUi.isPopupClosing) {
playerUi.closeOverlayBinding.closeButton.animate(false, 200)
binding.closingOverlay.animate(false, 200)
}
}
private fun handleMultiDrag(event: MotionEvent): Boolean {
if (initPointerDistance != -1.0 && event.pointerCount == 2) {
// get the movements of the fingers
val firstPointerMove = hypot(
event.getX(0) - initFirstPointerX.toDouble(),
event.getY(0) - initFirstPointerY.toDouble()
)
val secPointerMove = hypot(
event.getX(1) - initSecPointerX.toDouble(),
event.getY(1) - initSecPointerY.toDouble()
)
// minimum threshold beyond which pinch gesture will work
val minimumMove = ViewConfiguration.get(player.context).scaledTouchSlop
if (max(firstPointerMove, secPointerMove) > minimumMove) {
// calculate current distance between the pointers
val currentPointerDistance = hypot(
event.getX(0) - event.getX(1).toDouble(),
event.getY(0) - event.getY(1).toDouble()
)
val popupWidth = playerUi.popupLayoutParams.width.toDouble()
// change co-ordinates of popup so the center stays at the same position
val newWidth = popupWidth * currentPointerDistance / initPointerDistance
initPointerDistance = currentPointerDistance
playerUi.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt()
playerUi.checkPopupPositionBounds()
playerUi.updateScreenSize()
playerUi.changePopupSize(min(playerUi.screenWidth.toDouble(), newWidth).toInt())
return true
}
}
return false
}
private fun onPopupResizingStart() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingStart called")
}
binding.loadingPanel.visibility = View.GONE
playerUi.hideControls(0, 0)
binding.fastSeekOverlay.animate(false, 0)
binding.currentDisplaySeek.animate(false, 0, AnimationType.ALPHA, 0)
}
private fun onPopupResizingEnd() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingEnd called")
}
}
override fun onLongPress(e: MotionEvent?) {
playerUi.updateScreenSize()
playerUi.checkPopupPositionBounds()
playerUi.changePopupSize(playerUi.screenWidth)
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
return if (player.popupPlayerSelected()) {
val absVelocityX = abs(velocityX)
val absVelocityY = abs(velocityY)
if (absVelocityX.coerceAtLeast(absVelocityY) > TOSS_FLING_VELOCITY) {
if (absVelocityX > TOSS_FLING_VELOCITY) {
playerUi.popupLayoutParams.x = velocityX.toInt()
}
if (absVelocityY > TOSS_FLING_VELOCITY) {
playerUi.popupLayoutParams.y = velocityY.toInt()
}
playerUi.checkPopupPositionBounds()
playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams)
return true
}
return false
} else {
true
}
}
override fun onDownNotDoubleTapping(e: MotionEvent): Boolean {
// Fix popup position when the user touch it, it may have the wrong one
// because the soft input is visible (the draggable area is currently resized).
playerUi.updateScreenSize()
playerUi.checkPopupPositionBounds()
playerUi.popupLayoutParams.let {
initialPopupX = it.x
initialPopupY = it.y
}
return true // we want `super.onDown(e)` to be called
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
if (isDoubleTapping)
return true
if (player.exoPlayerIsNull())
return false
onSingleTap()
return true
}
override fun onScroll(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (isResizing) {
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
}
if (!isMoving) {
playerUi.closeOverlayBinding.closeButton.animate(true, 200)
}
isMoving = true
val diffX: Float = (movingEvent.rawX - initialEvent.rawX)
var posX: Float = (initialPopupX + diffX)
val diffY: Float = (movingEvent.rawY - initialEvent.rawY)
var posY: Float = (initialPopupY + diffY)
if (posX > playerUi.screenWidth - playerUi.popupLayoutParams.width) {
posX = (playerUi.screenWidth - playerUi.popupLayoutParams.width).toFloat()
} else if (posX < 0) {
posX = 0f
}
if (posY > playerUi.screenHeight - playerUi.popupLayoutParams.height) {
posY = (playerUi.screenHeight - playerUi.popupLayoutParams.height).toFloat()
} else if (posY < 0) {
posY = 0f
}
playerUi.popupLayoutParams.x = posX.toInt()
playerUi.popupLayoutParams.y = posY.toInt()
// -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
val showClosingOverlayView: Boolean = playerUi.isInsideClosingRadius(movingEvent)
// Check if an view is in expected state and if not animate it into the correct state
val expectedVisibility = if (showClosingOverlayView) View.VISIBLE else View.GONE
if (binding.closingOverlay.visibility != expectedVisibility) {
binding.closingOverlay.animate(showClosingOverlayView, 200)
}
playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams)
return true
}
override fun getDisplayPortion(e: MotionEvent): DisplayPortion {
return when {
e.x < playerUi.popupLayoutParams.width / 3.0 -> DisplayPortion.LEFT
e.x > playerUi.popupLayoutParams.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
}
override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
return when {
e.x < playerUi.popupLayoutParams.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
}
companion object {
private val TAG = PopupPlayerGestureListener::class.java.simpleName
private val DEBUG = MainActivity.DEBUG
private const val TOSS_FLING_VELOCITY = 2500
}
}

View file

@ -26,7 +26,7 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding; import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener; import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
import org.schabi.newpipe.util.SliderStrategy; import org.schabi.newpipe.util.SliderStrategy;
@ -207,7 +207,7 @@ public class PlaybackParameterDialog extends DialogFragment {
? View.VISIBLE ? View.VISIBLE
: View.GONE); : View.GONE);
animateRotation(binding.pitchToogleControlModes, animateRotation(binding.pitchToogleControlModes,
Player.DEFAULT_CONTROLS_DURATION, VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
isCurrentlyVisible ? 180 : 0); isCurrentlyVisible ? 180 : 0);
}); });

View file

@ -3,7 +3,6 @@ package org.schabi.newpipe.player.helper;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
import static org.schabi.newpipe.player.Player.PLAYER_TYPE; import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
@ -11,6 +10,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLA
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE; import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
import static org.schabi.newpipe.player.ui.PopupPlayerUi.IDLE_WINDOW_FLAGS;
import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
@ -49,11 +49,12 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.utils.Utils; import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.player.ui.PopupPlayerUi;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -339,10 +340,6 @@ public final class PlayerHelper {
return true; return true;
} }
public static int getTossFlingVelocity() {
return 2500;
}
@NonNull @NonNull
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) { public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
final CaptioningManager captioningManager = ContextCompat.getSystemService(context, final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
@ -452,10 +449,10 @@ public final class PlayerHelper {
// Utils used by player // Utils used by player
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
public static MainPlayer.PlayerType retrievePlayerTypeFromIntent(final Intent intent) { public static PlayerService.PlayerType retrievePlayerTypeFromIntent(final Intent intent) {
// If you want to open popup from the app just include Constants.POPUP_ONLY into an extra // If you want to open popup from the app just include Constants.POPUP_ONLY into an extra
return MainPlayer.PlayerType.values()[ return PlayerService.PlayerType.values()[
intent.getIntExtra(PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal())]; intent.getIntExtra(PLAYER_TYPE, PlayerService.PlayerType.MAIN.ordinal())];
} }
public static boolean isPlaybackResumeEnabled(final Player player) { public static boolean isPlaybackResumeEnabled(final Player player) {
@ -529,19 +526,20 @@ public final class PlayerHelper {
} }
/** /**
* @param player {@code screenWidth} and {@code screenHeight} must have been initialized * @param playerUi {@code screenWidth} and {@code screenHeight} must have been initialized
* @return the popup starting layout params * @return the popup starting layout params
*/ */
@SuppressLint("RtlHardcoded") @SuppressLint("RtlHardcoded")
public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs( public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs(
final Player player) { final PopupPlayerUi playerUi) {
final boolean popupRememberSizeAndPos = player.getPrefs().getBoolean( final SharedPreferences prefs = playerUi.getPlayer().getPrefs();
player.getContext().getString(R.string.popup_remember_size_pos_key), true); final Context context = playerUi.getPlayer().getContext();
final float defaultSize =
player.getContext().getResources().getDimension(R.dimen.popup_default_width); final boolean popupRememberSizeAndPos = prefs.getBoolean(
context.getString(R.string.popup_remember_size_pos_key), true);
final float defaultSize = context.getResources().getDimension(R.dimen.popup_default_width);
final float popupWidth = popupRememberSizeAndPos final float popupWidth = popupRememberSizeAndPos
? player.getPrefs().getFloat(player.getContext().getString( ? prefs.getFloat(context.getString(R.string.popup_saved_width_key), defaultSize)
R.string.popup_saved_width_key), defaultSize)
: defaultSize; : defaultSize;
final float popupHeight = getMinimumVideoHeight(popupWidth); final float popupHeight = getMinimumVideoHeight(popupWidth);
@ -553,27 +551,26 @@ public final class PlayerHelper {
popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
final int centerX = (int) (player.getScreenWidth() / 2f - popupWidth / 2f); final int centerX = (int) (playerUi.getScreenWidth() / 2f - popupWidth / 2f);
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f); final int centerY = (int) (playerUi.getScreenHeight() / 2f - popupHeight / 2f);
popupLayoutParams.x = popupRememberSizeAndPos popupLayoutParams.x = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString( ? prefs.getInt(context.getString(R.string.popup_saved_x_key), centerX) : centerX;
R.string.popup_saved_x_key), centerX) : centerX;
popupLayoutParams.y = popupRememberSizeAndPos popupLayoutParams.y = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString( ? prefs.getInt(context.getString(R.string.popup_saved_y_key), centerY) : centerY;
R.string.popup_saved_y_key), centerY) : centerY;
return popupLayoutParams; return popupLayoutParams;
} }
public static void savePopupPositionAndSizeToPrefs(final Player player) { public static void savePopupPositionAndSizeToPrefs(final PopupPlayerUi playerUi) {
if (player.getPopupLayoutParams() != null) { if (playerUi.getPopupLayoutParams() != null) {
player.getPrefs().edit() final Context context = playerUi.getPlayer().getContext();
.putFloat(player.getContext().getString(R.string.popup_saved_width_key), playerUi.getPlayer().getPrefs().edit()
player.getPopupLayoutParams().width) .putFloat(context.getString(R.string.popup_saved_width_key),
.putInt(player.getContext().getString(R.string.popup_saved_x_key), playerUi.getPopupLayoutParams().width)
player.getPopupLayoutParams().x) .putInt(context.getString(R.string.popup_saved_x_key),
.putInt(player.getContext().getString(R.string.popup_saved_y_key), playerUi.getPopupLayoutParams().x)
player.getPopupLayoutParams().y) .putInt(context.getString(R.string.popup_saved_y_key),
playerUi.getPopupLayoutParams().y)
.apply(); .apply();
} }
} }

View file

@ -16,7 +16,7 @@ import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.App; import org.schabi.newpipe.App;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.PlayerServiceEventListener; import org.schabi.newpipe.player.event.PlayerServiceEventListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
@ -42,17 +42,17 @@ public final class PlayerHolder {
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection(); private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
private boolean bound; private boolean bound;
@Nullable private MainPlayer playerService; @Nullable private PlayerService playerService;
@Nullable private Player player; @Nullable private Player player;
/** /**
* Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service, * Returns the current {@link PlayerService.PlayerType} of the {@link PlayerService} service,
* otherwise `null` if no service running. * otherwise `null` if no service running.
* *
* @return Current PlayerType * @return Current PlayerType
*/ */
@Nullable @Nullable
public MainPlayer.PlayerType getType() { public PlayerService.PlayerType getType() {
if (player == null) { if (player == null) {
return null; return null;
} }
@ -122,7 +122,7 @@ public final class PlayerHolder {
// and NullPointerExceptions inside the service because the service will be // and NullPointerExceptions inside the service because the service will be
// bound twice. Prevent it with unbinding first // bound twice. Prevent it with unbinding first
unbind(context); unbind(context);
ContextCompat.startForegroundService(context, new Intent(context, MainPlayer.class)); ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class));
serviceConnection.doPlayAfterConnect(playAfterConnect); serviceConnection.doPlayAfterConnect(playAfterConnect);
bind(context); bind(context);
} }
@ -130,7 +130,7 @@ public final class PlayerHolder {
public void stopService() { public void stopService() {
final Context context = getCommonContext(); final Context context = getCommonContext();
unbind(context); unbind(context);
context.stopService(new Intent(context, MainPlayer.class)); context.stopService(new Intent(context, PlayerService.class));
} }
class PlayerServiceConnection implements ServiceConnection { class PlayerServiceConnection implements ServiceConnection {
@ -156,7 +156,7 @@ public final class PlayerHolder {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Player service is connected"); Log.d(TAG, "Player service is connected");
} }
final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service; final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service;
playerService = localBinder.getService(); playerService = localBinder.getService();
player = localBinder.getPlayer(); player = localBinder.getPlayer();
@ -172,7 +172,7 @@ public final class PlayerHolder {
Log.d(TAG, "bind() called"); Log.d(TAG, "bind() called");
} }
final Intent serviceIntent = new Intent(context, MainPlayer.class); final Intent serviceIntent = new Intent(context, PlayerService.class);
bound = context.bindService(serviceIntent, serviceConnection, bound = context.bindService(serviceIntent, serviceConnection,
Context.BIND_AUTO_CREATE); Context.BIND_AUTO_CREATE);
if (!bound) { if (!bound) {
@ -211,6 +211,13 @@ public final class PlayerHolder {
private final PlayerServiceEventListener internalListener = private final PlayerServiceEventListener internalListener =
new PlayerServiceEventListener() { new PlayerServiceEventListener() {
@Override
public void onViewCreated() {
if (listener != null) {
listener.onViewCreated();
}
}
@Override @Override
public void onFullscreenStateChanged(final boolean fullscreen) { public void onFullscreenStateChanged(final boolean fullscreen) {
if (listener != null) { if (listener != null) {

View file

@ -1,47 +0,0 @@
package org.schabi.newpipe.player.listeners.view
import android.util.Log
import android.view.View
import androidx.appcompat.widget.PopupMenu
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.helper.PlaybackParameterDialog
/**
* Click listener for the playbackSpeed textview of the player
*/
class PlaybackSpeedClickListener(
private val player: Player,
private val playbackSpeedPopupMenu: PopupMenu
) : View.OnClickListener {
companion object {
private const val TAG: String = "PlaybSpeedClickListener"
}
override fun onClick(v: View) {
if (MainActivity.DEBUG) {
Log.d(TAG, "onPlaybackSpeedClicked() called")
}
if (player.videoPlayerSelected()) {
PlaybackParameterDialog.newInstance(
player.playbackSpeed.toDouble(),
player.playbackPitch.toDouble(),
player.playbackSkipSilence
) { speed: Float, pitch: Float, skipSilence: Boolean ->
player.setPlaybackParameters(
speed,
pitch,
skipSilence
)
}
.show(player.parentActivity!!.supportFragmentManager, null)
} else {
playbackSpeedPopupMenu.show()
player.isSomePopupMenuVisible = true
}
player.manageControlsAfterOnClick(v)
}
}

View file

@ -1,41 +0,0 @@
package org.schabi.newpipe.player.listeners.view
import android.annotation.SuppressLint
import android.util.Log
import android.view.View
import androidx.appcompat.widget.PopupMenu
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.extractor.MediaFormat
import org.schabi.newpipe.player.Player
/**
* Click listener for the qualityTextView of the player
*/
class QualityClickListener(
private val player: Player,
private val qualityPopupMenu: PopupMenu
) : View.OnClickListener {
companion object {
private const val TAG: String = "QualityClickListener"
}
@SuppressLint("SetTextI18n") // we don't need I18N because of a " "
override fun onClick(v: View) {
if (MainActivity.DEBUG) {
Log.d(TAG, "onQualitySelectorClicked() called")
}
qualityPopupMenu.show()
player.isSomePopupMenuVisible = true
val videoStream = player.selectedVideoStream
if (videoStream != null) {
player.binding.qualityTextView.text =
MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.getResolution()
}
player.saveWasPlaying()
player.manageControlsAfterOnClick(v)
}
}

View file

@ -8,6 +8,9 @@ import android.support.v4.media.MediaMetadataCompat;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback; import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.ui.VideoPlayerUi;
import java.util.Optional;
public class PlayerMediaSession implements MediaSessionCallback { public class PlayerMediaSession implements MediaSessionCallback {
private final Player player; private final Player player;
@ -89,7 +92,7 @@ public class PlayerMediaSession implements MediaSessionCallback {
public void play() { public void play() {
player.play(); player.play();
// hide the player controls even if the play command came from the media session // hide the player controls even if the play command came from the media session
player.hideControls(0, 0); player.UIs().get(VideoPlayerUi.class).ifPresent(playerUi -> playerUi.hideControls(0, 0));
} }
@Override @Override

View file

@ -0,0 +1,937 @@
package org.schabi.newpipe.player.ui;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.player.Player.STATE_COMPLETED;
import static org.schabi.newpipe.player.Player.STATE_PAUSED;
import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PAUSE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAction;
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
import android.content.Intent;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Build;
import android.os.Handler;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.video.VideoSize;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PlayerBinding;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamSegment;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.info_list.StreamSegmentAdapter;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
import org.schabi.newpipe.player.gesture.BasePlayerGestureListener;
import org.schabi.newpipe.player.gesture.MainPlayerGestureListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder;
import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import java.util.List;
import java.util.Objects;
public final class MainPlayerUi extends VideoPlayerUi {
private static final String TAG = MainPlayerUi.class.getSimpleName();
private boolean isFullscreen = false;
private boolean isVerticalVideo = false;
private boolean fragmentIsVisible = false;
private ContentObserver settingsContentObserver;
private PlayQueueAdapter playQueueAdapter;
private StreamSegmentAdapter segmentAdapter;
private boolean isQueueVisible = false;
private boolean areSegmentsVisible = false;
// fullscreen player
private ItemTouchHelper itemTouchHelper;
public MainPlayerUi(@NonNull final Player player,
@NonNull final PlayerBinding playerBinding) {
super(player, playerBinding);
}
/**
* Open fullscreen on tablets where the option to have the main player start automatically in
* fullscreen mode is on. Rotating the device to landscape is already done in {@link
* VideoDetailFragment#openVideoPlayer(boolean)} when the thumbnail is clicked, and that's
* enough for phones, but not for tablets since the mini player can be also shown in landscape.
*/
private void directlyOpenFullscreenIfNeeded() {
if (PlayerHelper.isStartMainPlayerFullscreenEnabled(player.getService())
&& DeviceUtils.isTablet(player.getService())
&& PlayerHelper.globalScreenOrientationLocked(player.getService())) {
player.getFragmentListener().ifPresent(
PlayerServiceEventListener::onScreenRotationButtonClicked);
}
}
@Override
public void setupAfterIntent() {
// needed for tablets, check the function for a better explanation
directlyOpenFullscreenIfNeeded();
super.setupAfterIntent();
binding.getRoot().setVisibility(View.VISIBLE);
initVideoPlayer();
// Android TV: without it focus will frame the whole player
binding.playPauseButton.requestFocus();
// Note: This is for automatically playing (when "Resume playback" is off), see #6179
if (player.getPlayWhenReady()) {
player.play();
} else {
player.pause();
}
}
@Override
BasePlayerGestureListener buildGestureListener() {
return new MainPlayerGestureListener(this);
}
@Override
protected void initListeners() {
super.initListeners();
binding.queueButton.setOnClickListener(v -> onQueueClicked());
binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked());
binding.addToPlaylistButton.setOnClickListener(v ->
player.onAddToPlaylistClicked(getParentActivity().getSupportFragmentManager()));
settingsContentObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(final boolean selfChange) {
setupScreenRotationButton();
}
};
context.getContentResolver().registerContentObserver(
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
settingsContentObserver);
binding.getRoot().addOnLayoutChangeListener(this::onLayoutChange);
}
@Override
public void initPlayback() {
super.initPlayback();
if (playQueueAdapter != null) {
playQueueAdapter.dispose();
}
playQueueAdapter = new PlayQueueAdapter(context,
Objects.requireNonNull(player.getPlayQueue()));
segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener());
}
@Override
public void removeViewFromParent() {
// view was added to fragment
final ViewParent parent = binding.getRoot().getParent();
if (parent instanceof ViewGroup) {
((ViewGroup) parent).removeView(binding.getRoot());
}
}
@Override
public void destroy() {
super.destroy();
context.getContentResolver().unregisterContentObserver(settingsContentObserver);
// Exit from fullscreen when user closes the player via notification
if (isFullscreen) {
toggleFullscreen();
}
removeViewFromParent();
}
@Override
public void destroyPlayer() {
super.destroyPlayer();
if (playQueueAdapter != null) {
playQueueAdapter.unsetSelectedListener();
playQueueAdapter.dispose();
}
}
@Override
public void smoothStopForImmediateReusing() {
super.smoothStopForImmediateReusing();
// Android TV will handle back button in case controls will be visible
// (one more additional unneeded click while the player is hidden)
hideControls(0, 0);
closeItemsList();
}
private void initVideoPlayer() {
// restore last resize mode
setResizeMode(PlayerHelper.retrieveResizeModeFromPrefs(player));
binding.getRoot().setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
}
@Override
protected void setupElementsVisibility() {
super.setupElementsVisibility();
closeItemsList();
showHideKodiButton();
binding.fullScreenButton.setVisibility(View.GONE);
setupScreenRotationButton();
binding.resizeTextView.setVisibility(View.VISIBLE);
binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.VISIBLE);
binding.moreOptionsButton.setVisibility(View.VISIBLE);
binding.topControls.setOrientation(LinearLayout.VERTICAL);
binding.primaryControls.getLayoutParams().width
= LinearLayout.LayoutParams.MATCH_PARENT;
binding.secondaryControls.setVisibility(View.INVISIBLE);
binding.moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(context,
R.drawable.ic_expand_more));
binding.share.setVisibility(View.VISIBLE);
binding.openInBrowser.setVisibility(View.VISIBLE);
binding.switchMute.setVisibility(View.VISIBLE);
binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE);
// Top controls have a large minHeight which is allows to drag the player
// down in fullscreen mode (just larger area to make easy to locate by finger)
binding.topControls.setClickable(true);
binding.topControls.setFocusable(true);
if (isFullscreen) {
binding.titleTextView.setVisibility(View.VISIBLE);
binding.channelTextView.setVisibility(View.VISIBLE);
} else {
binding.titleTextView.setVisibility(View.GONE);
binding.channelTextView.setVisibility(View.GONE);
}
}
@Override
protected void setupElementsSize(final Resources resources) {
setupElementsSize(
resources.getDimensionPixelSize(R.dimen.player_main_buttons_min_width),
resources.getDimensionPixelSize(R.dimen.player_main_top_padding),
resources.getDimensionPixelSize(R.dimen.player_main_controls_padding),
resources.getDimensionPixelSize(R.dimen.player_main_buttons_padding)
);
}
/*//////////////////////////////////////////////////////////////////////////
// Broadcast receiver
//////////////////////////////////////////////////////////////////////////*/
//region Broadcast receiver
@Override
public void onBroadcastReceived(final Intent intent) {
super.onBroadcastReceived(intent);
if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) {
// Close it because when changing orientation from portrait
// (in fullscreen mode) the size of queue layout can be larger than the screen size
closeItemsList();
} else if (ACTION_PLAY_PAUSE.equals(intent.getAction())) {
// Ensure that we have audio-only stream playing when a user
// started to play from notification's play button from outside of the app
if (!fragmentIsVisible) {
onFragmentStopped();
}
} else if (VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED.equals(intent.getAction())) {
fragmentIsVisible = false;
onFragmentStopped();
} else if (VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED.equals(intent.getAction())) {
// Restore video source when user returns to the fragment
fragmentIsVisible = true;
player.useVideoSource(true);
// When a user returns from background, the system UI will always be shown even if
// controls are invisible: hide it in that case
if (!isControlsVisible()) {
hideSystemUIIfNeeded();
}
}
}
//endregion
/*//////////////////////////////////////////////////////////////////////////
// Fragment binding
//////////////////////////////////////////////////////////////////////////*/
//region Fragment binding
@Override
public void onFragmentListenerSet() {
super.onFragmentListenerSet();
fragmentIsVisible = true;
// Apply window insets because Android will not do it when orientation changes
// from landscape to portrait
if (!isFullscreen) {
binding.playbackControlRoot.setPadding(0, 0, 0, 0);
}
binding.itemsListPanel.setPadding(0, 0, 0, 0);
player.getFragmentListener().ifPresent(PlayerServiceEventListener::onViewCreated);
}
/**
* This will be called when a user goes to another app/activity, turns off a screen.
* We don't want to interrupt playback and don't want to see notification so
* next lines of code will enable audio-only playback only if needed
*/
private void onFragmentStopped() {
if (player.isPlaying() || player.isLoading()) {
switch (getMinimizeOnExitAction(context)) {
case MINIMIZE_ON_EXIT_MODE_BACKGROUND:
player.useVideoSource(false);
break;
case MINIMIZE_ON_EXIT_MODE_POPUP:
player.setRecovery();
NavigationHelper.playOnPopupPlayer(getParentActivity(),
player.getPlayQueue(), true);
break;
case MINIMIZE_ON_EXIT_MODE_NONE: default:
player.pause();
break;
}
}
}
//endregion
private void showHideKodiButton() {
// show kodi button if it supports the current service and it is enabled in settings
@Nullable final PlayQueue playQueue = player.getPlayQueue();
binding.playWithKodi.setVisibility(playQueue != null && playQueue.getItem() != null
&& KoreUtils.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId())
? View.VISIBLE : View.GONE);
}
@Override
public void onUpdateProgress(final int currentProgress,
final int duration,
final int bufferPercent) {
super.onUpdateProgress(currentProgress, duration, bufferPercent);
if (areSegmentsVisible) {
segmentAdapter.selectSegmentAt(getNearestStreamSegmentPosition(currentProgress));
}
if (isQueueVisible) {
updateQueueTime(currentProgress);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Controls showing / hiding
//////////////////////////////////////////////////////////////////////////*/
//region Controls showing / hiding
protected void showOrHideButtons() {
super.showOrHideButtons();
@Nullable final PlayQueue playQueue = player.getPlayQueue();
if (playQueue == null) {
return;
}
final boolean showQueue = playQueue.getStreams().size() > 1;
final boolean showSegment = !player.getCurrentStreamInfo()
.map(StreamInfo::getStreamSegments)
.map(List::isEmpty)
.orElse(/*no stream info=*/true);
binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE);
binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f);
binding.segmentsButton.setVisibility(showSegment ? View.VISIBLE : View.GONE);
binding.segmentsButton.setAlpha(showSegment ? 1.0f : 0.0f);
}
@Override
public void showSystemUIPartially() {
if (isFullscreen) {
final Window window = getParentActivity().getWindow();
window.setStatusBarColor(Color.TRANSPARENT);
window.setNavigationBarColor(Color.TRANSPARENT);
final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
window.getDecorView().setSystemUiVisibility(visibility);
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
}
@Override
public void hideSystemUIIfNeeded() {
player.getFragmentListener().ifPresent(PlayerServiceEventListener::hideSystemUiIfNeeded);
}
/**
* Calculate the maximum allowed height for the {@link R.id.endScreen}
* to prevent it from enlarging the player.
* <p>
* The calculating follows these rules:
* <ul>
* <li>
* Show at least stream title and content creator on TVs and tablets
* when in landscape (always the case for TVs) and not in fullscreen mode.
* This requires to have at least <code>85dp</code> free space for {@link R.id.detail_root}
* and additional space for the stream title text size
* ({@link R.id.detail_title_root_layout}).
* The text size is <code>15sp</code> on tablets and <code>16sp</code> on TVs,
* see {@link R.id.titleTextView}.
* </li>
* <li>
* Otherwise, the max thumbnail height is the screen height.
* TODO investigate why this is done on popup player, too
* </li>
* </ul>
*
* @param bitmap the bitmap that needs to be resized to fit the end screen
* @return the maximum height for the end screen thumbnail
*/
@Override
protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap) {
final int screenHeight = context.getResources().getDisplayMetrics().heightPixels;
if (DeviceUtils.isTv(context) && !isFullscreen()) {
final int videoInfoHeight =
DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(16, context);
return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight);
} else if (DeviceUtils.isTablet(context) && isLandscape() && !isFullscreen()) {
final int videoInfoHeight =
DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(15, context);
return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight);
} else { // fullscreen player: max height is the device height
return Math.min(bitmap.getHeight(), screenHeight);
}
}
//endregion
@Override
public void onPlaying() {
super.onPlaying();
checkLandscape();
}
@Override
public void onCompleted() {
super.onCompleted();
if (isFullscreen) {
toggleFullscreen();
}
}
@Override
protected void setupSubtitleView(float captionScale) {
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels);
final float captionRatioInverse = 20f + 4f * (1.0f - captionScale);
binding.subtitleView.setFixedTextSize(
TypedValue.COMPLEX_UNIT_PX, minimumLength / captionRatioInverse);
}
/*//////////////////////////////////////////////////////////////////////////
// Gestures
//////////////////////////////////////////////////////////////////////////*/
//region Gestures
@SuppressWarnings("checkstyle:ParameterNumber")
private void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
final int ol, final int ot, final int or, final int ob) {
if (l != ol || t != ot || r != or || b != ob) {
// Use smaller value to be consistent between screen orientations
// (and to make usage easier)
final int width = r - l;
final int height = b - t;
final int min = Math.min(width, height);
final int maxGestureLength = (int) (min * MainPlayerGestureListener.MAX_GESTURE_LENGTH);
if (DEBUG) {
Log.d(TAG, "maxGestureLength = " + maxGestureLength);
}
binding.volumeProgressBar.setMax(maxGestureLength);
binding.brightnessProgressBar.setMax(maxGestureLength);
setInitialGestureValues();
binding.itemsListPanel.getLayoutParams().height
= height - binding.itemsListPanel.getTop();
}
}
private void setInitialGestureValues() {
if (player.getAudioReactor() != null) {
final float currentVolumeNormalized =
(float) player.getAudioReactor().getVolume()
/ player.getAudioReactor().getMaxVolume();
binding.volumeProgressBar.setProgress(
(int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized));
}
}
//endregion
/*//////////////////////////////////////////////////////////////////////////
// Play queue, segments and streams
//////////////////////////////////////////////////////////////////////////*/
//region Play queue, segments and streams
@Override
public void onMetadataChanged(@NonNull final StreamInfo info) {
super.onMetadataChanged(info);
showHideKodiButton();
if (areSegmentsVisible) {
if (segmentAdapter.setItems(info)) {
final int adapterPosition = getNearestStreamSegmentPosition(
player.getExoPlayer().getCurrentPosition());
segmentAdapter.selectSegmentAt(adapterPosition);
binding.itemsList.scrollToPosition(adapterPosition);
} else {
closeItemsList();
}
}
}
@Override
public void onPlayQueueEdited() {
super.onPlayQueueEdited();
showOrHideButtons();
}
private void onQueueClicked() {
isQueueVisible = true;
hideSystemUIIfNeeded();
buildQueue();
binding.itemsListHeaderTitle.setVisibility(View.GONE);
binding.itemsListHeaderDuration.setVisibility(View.VISIBLE);
binding.shuffleButton.setVisibility(View.VISIBLE);
binding.repeatButton.setVisibility(View.VISIBLE);
binding.addToPlaylistButton.setVisibility(View.VISIBLE);
hideControls(0, 0);
binding.itemsListPanel.requestFocus();
animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
AnimationType.SLIDE_AND_ALPHA);
@Nullable final PlayQueue playQueue = player.getPlayQueue();
if (playQueue != null) {
binding.itemsList.scrollToPosition(playQueue.getIndex());
}
updateQueueTime((int) player.getExoPlayer().getCurrentPosition());
}
private void buildQueue() {
binding.itemsList.setAdapter(playQueueAdapter);
binding.itemsList.setClickable(true);
binding.itemsList.setLongClickable(true);
binding.itemsList.clearOnScrollListeners();
binding.itemsList.addOnScrollListener(getQueueScrollListener());
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(binding.itemsList);
playQueueAdapter.setSelectedListener(getOnSelectedListener());
binding.itemsListClose.setOnClickListener(view -> closeItemsList());
}
private void onSegmentsClicked() {
areSegmentsVisible = true;
hideSystemUIIfNeeded();
buildSegments();
binding.itemsListHeaderTitle.setVisibility(View.VISIBLE);
binding.itemsListHeaderDuration.setVisibility(View.GONE);
binding.shuffleButton.setVisibility(View.GONE);
binding.repeatButton.setVisibility(View.GONE);
binding.addToPlaylistButton.setVisibility(View.GONE);
hideControls(0, 0);
binding.itemsListPanel.requestFocus();
animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
AnimationType.SLIDE_AND_ALPHA);
final int adapterPosition = getNearestStreamSegmentPosition(
player.getExoPlayer().getCurrentPosition());
segmentAdapter.selectSegmentAt(adapterPosition);
binding.itemsList.scrollToPosition(adapterPosition);
}
private void buildSegments() {
binding.itemsList.setAdapter(segmentAdapter);
binding.itemsList.setClickable(true);
binding.itemsList.setLongClickable(false);
binding.itemsList.clearOnScrollListeners();
if (itemTouchHelper != null) {
itemTouchHelper.attachToRecyclerView(null);
}
player.getCurrentStreamInfo().ifPresent(segmentAdapter::setItems);
binding.shuffleButton.setVisibility(View.GONE);
binding.repeatButton.setVisibility(View.GONE);
binding.addToPlaylistButton.setVisibility(View.GONE);
binding.itemsListClose.setOnClickListener(view -> closeItemsList());
}
public void closeItemsList() {
if (isQueueVisible || areSegmentsVisible) {
isQueueVisible = false;
areSegmentsVisible = false;
if (itemTouchHelper != null) {
itemTouchHelper.attachToRecyclerView(null);
}
animate(binding.itemsListPanel, false, DEFAULT_CONTROLS_DURATION,
AnimationType.SLIDE_AND_ALPHA, 0, () -> {
// Even when queueLayout is GONE it receives touch events
// and ruins normal behavior of the app. This line fixes it
binding.itemsListPanel.setTranslationY(
-binding.itemsListPanel.getHeight() * 5);
});
// clear focus, otherwise a white rectangle remains on top of the player
binding.itemsListClose.clearFocus();
binding.playPauseButton.requestFocus();
}
}
private OnScrollBelowItemsListener getQueueScrollListener() {
return new OnScrollBelowItemsListener() {
@Override
public void onScrolledDown(final RecyclerView recyclerView) {
@Nullable final PlayQueue playQueue = player.getPlayQueue();
if (playQueue != null && !playQueue.isComplete()) {
playQueue.fetch();
} else if (binding != null) {
binding.itemsList.clearOnScrollListeners();
}
}
};
}
private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
return (item, seconds) -> {
segmentAdapter.selectSegment(item);
player.seekTo(seconds * 1000L);
player.triggerProgressUpdate();
};
}
private int getNearestStreamSegmentPosition(final long playbackPosition) {
//noinspection SimplifyOptionalCallChains
if (!player.getCurrentStreamInfo().isPresent()) {
return 0;
}
int nearestPosition = 0;
final List<StreamSegment> segments
= player.getCurrentStreamInfo().get().getStreamSegments();
for (int i = 0; i < segments.size(); i++) {
if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
break;
}
nearestPosition++;
}
return Math.max(0, nearestPosition - 1);
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new PlayQueueItemTouchCallback() {
@Override
public void onMove(final int sourceIndex, final int targetIndex) {
@Nullable final PlayQueue playQueue = player.getPlayQueue();
if (playQueue != null) {
playQueue.move(sourceIndex, targetIndex);
}
}
@Override
public void onSwiped(final int index) {
@Nullable final PlayQueue playQueue = player.getPlayQueue();
if (playQueue != null && index != -1) {
playQueue.remove(index);
}
}
};
}
private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() {
return new PlayQueueItemBuilder.OnSelectedListener() {
@Override
public void selected(final PlayQueueItem item, final View view) {
player.selectQueueItem(item);
}
@Override
public void held(final PlayQueueItem item, final View view) {
@Nullable final PlayQueue playQueue = player.getPlayQueue();
@Nullable final AppCompatActivity parentActivity = getParentActivity();
if (playQueue != null && parentActivity != null && playQueue.indexOf(item) != -1) {
openPopupMenu(player.getPlayQueue(), item, view, true,
parentActivity.getSupportFragmentManager(), context);
}
}
@Override
public void onStartDrag(final PlayQueueItemHolder viewHolder) {
if (itemTouchHelper != null) {
itemTouchHelper.startDrag(viewHolder);
}
}
};
}
private void updateQueueTime(final int currentTime) {
@Nullable final PlayQueue playQueue = player.getPlayQueue();
if (playQueue == null) {
return;
}
final int currentStream = playQueue.getIndex();
int before = 0;
int after = 0;
final List<PlayQueueItem> streams = playQueue.getStreams();
final int nStreams = streams.size();
for (int i = 0; i < nStreams; i++) {
if (i < currentStream) {
before += streams.get(i).getDuration();
} else {
after += streams.get(i).getDuration();
}
}
before *= 1000;
after *= 1000;
binding.itemsListHeaderDuration.setText(
String.format("%s/%s",
getTimeString(currentTime + before),
getTimeString(before + after)
));
}
@Override
protected boolean isAnyListViewOpen() {
return isQueueVisible || areSegmentsVisible;
}
@Override
public boolean isFullscreen() {
return isFullscreen;
}
public boolean isVerticalVideo() {
return isVerticalVideo;
}
//endregion
/*//////////////////////////////////////////////////////////////////////////
// Click listeners
//////////////////////////////////////////////////////////////////////////*/
//region Click listeners
@Override
public void onClick(final View v) {
if (v.getId() == binding.screenRotationButton.getId()) {
// Only if it's not a vertical video or vertical video but in landscape with locked
// orientation a screen orientation can be changed automatically
if (!isVerticalVideo || (isLandscape() && globalScreenOrientationLocked(context))) {
player.getFragmentListener().ifPresent(
PlayerServiceEventListener::onScreenRotationButtonClicked);
} else {
toggleFullscreen();
}
}
// call it later since it calls manageControlsAfterOnClick at the end
super.onClick(v);
}
@Override
protected void onPlaybackSpeedClicked() {
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
player.getPlaybackSkipSilence(), (speed, pitch, skipSilence)
-> player.setPlaybackParameters(speed, pitch, skipSilence))
.show(getParentActivity().getSupportFragmentManager(), null);
}
@Override
public boolean onLongClick(final View v) {
if (v.getId() == binding.moreOptionsButton.getId() && isFullscreen) {
player.getFragmentListener().ifPresent(
PlayerServiceEventListener::onMoreOptionsLongClicked);
hideControls(0, 0);
hideSystemUIIfNeeded();
return true;
}
return super.onLongClick(v);
}
@Override
public boolean onKeyDown(final int keyCode) {
if (keyCode == KeyEvent.KEYCODE_SPACE && isFullscreen) {
player.playPause();
if (player.isPlaying()) {
hideControls(0, 0);
}
return true;
}
return super.onKeyDown(keyCode);
}
//endregion
/*//////////////////////////////////////////////////////////////////////////
// Video size, resize, orientation, fullscreen
//////////////////////////////////////////////////////////////////////////*/
//region Video size, resize, orientation, fullscreen
private void setupScreenRotationButton() {
binding.screenRotationButton.setVisibility(globalScreenOrientationLocked(context)
|| isVerticalVideo || DeviceUtils.isTablet(context)
? View.VISIBLE : View.GONE);
binding.screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(context,
isFullscreen ? R.drawable.ic_fullscreen_exit
: R.drawable.ic_fullscreen));
}
@Override
public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
super.onVideoSizeChanged(videoSize);
isVerticalVideo = videoSize.width < videoSize.height;
if (globalScreenOrientationLocked(context)
&& isFullscreen
&& isLandscape() == isVerticalVideo
&& !DeviceUtils.isTv(context)
&& !DeviceUtils.isTablet(context)) {
// set correct orientation
player.getFragmentListener().ifPresent(
PlayerServiceEventListener::onScreenRotationButtonClicked);
}
setupScreenRotationButton();
}
public void toggleFullscreen() {
if (DEBUG) {
Log.d(TAG, "toggleFullscreen() called");
}
final PlayerServiceEventListener fragmentListener
= player.getFragmentListener().orElse(null);
if (fragmentListener == null || player.exoPlayerIsNull()) {
return;
}
isFullscreen = !isFullscreen;
if (!isFullscreen) {
// Apply window insets because Android will not do it when orientation changes
// from landscape to portrait (open vertical video to reproduce)
binding.playbackControlRoot.setPadding(0, 0, 0, 0);
} else {
// Android needs tens milliseconds to send new insets but a user is able to see
// how controls changes it's position from `0` to `nav bar height` padding.
// So just hide the controls to hide this visual inconsistency
hideControls(0, 0);
}
fragmentListener.onFullscreenStateChanged(isFullscreen);
if (isFullscreen) {
binding.titleTextView.setVisibility(View.VISIBLE);
binding.channelTextView.setVisibility(View.VISIBLE);
binding.playerCloseButton.setVisibility(View.GONE);
} else {
binding.titleTextView.setVisibility(View.GONE);
binding.channelTextView.setVisibility(View.GONE);
binding.playerCloseButton.setVisibility(View.VISIBLE);
}
setupScreenRotationButton();
}
public void checkLandscape() {
// check if landscape is correct
final boolean videoInLandscapeButNotInFullscreen
= isLandscape() && !isFullscreen && !player.isAudioOnly();
final boolean notPaused = player.getCurrentState() != STATE_COMPLETED
&& player.getCurrentState() != STATE_PAUSED;
if (videoInLandscapeButNotInFullscreen
&& notPaused
&& !DeviceUtils.isTablet(context)) {
toggleFullscreen();
}
}
//endregion
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
//region Getters
public PlayerBinding getBinding() {
return binding;
}
public AppCompatActivity getParentActivity() {
return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext();
}
public boolean isLandscape() {
// DisplayMetrics from activity context knows about MultiWindow feature
// while DisplayMetrics from app context doesn't
return DeviceUtils.isLandscape(getParentActivity());
}
//endregion
}

View file

@ -0,0 +1,26 @@
package org.schabi.newpipe.player.ui;
import androidx.annotation.NonNull;
import org.schabi.newpipe.player.NotificationUtil;
import org.schabi.newpipe.player.Player;
public final class NotificationPlayerUi extends PlayerUi {
boolean foregroundNotificationAlreadyCreated = false;
public NotificationPlayerUi(@NonNull final Player player) {
super(player);
}
@Override
public void initPlayer() {
super.initPlayer();
if (!foregroundNotificationAlreadyCreated) {
NotificationUtil.getInstance()
.createNotificationAndStartForeground(player, player.getService());
foregroundNotificationAlreadyCreated = true;
}
}
// TODO TODO on destroy remove foreground
}

View file

@ -0,0 +1,120 @@
package org.schabi.newpipe.player.ui;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.video.VideoSize;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.Player;
import java.util.List;
public abstract class PlayerUi {
private static final String TAG = PlayerUi.class.getSimpleName();
@NonNull protected Context context;
@NonNull protected Player player;
public PlayerUi(@NonNull final Player player) {
this.context = player.getContext();
this.player = player;
}
@NonNull
public Player getPlayer() {
return player;
}
public void setupAfterIntent() {
}
public void initPlayer() {
}
public void initPlayback() {
}
public void destroyPlayer() {
}
public void destroy() {
}
public void smoothStopForImmediateReusing() {
}
public void onFragmentListenerSet() {
}
public void onBroadcastReceived(final Intent intent) {
}
public void onUpdateProgress(final int currentProgress,
final int duration,
final int bufferPercent) {
}
public void onPrepared() {
}
public void onBlocked() {
}
public void onPlaying() {
}
public void onBuffering() {
}
public void onPaused() {
}
public void onPausedSeek() {
}
public void onCompleted() {
}
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
}
public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
}
public void onMuteUnmuteChanged(final boolean isMuted) {
}
public void onTextTracksChanged(@NonNull final Tracks currentTracks) {
}
public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) {
}
public void onRenderedFirstFrame() {
}
public void onCues(@NonNull final List<Cue> cues) {
}
public void onMetadataChanged(@NonNull final StreamInfo info) {
}
public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
}
public void onPlayQueueEdited() {
}
public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
}
}

View file

@ -0,0 +1,36 @@
package org.schabi.newpipe.player.ui;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
public final class PlayerUiList {
final List<PlayerUi> playerUis = new ArrayList<>();
public void add(final PlayerUi playerUi) {
playerUis.add(playerUi);
}
public <T> void destroyAll(final Class<T> playerUiType) {
playerUis.stream()
.filter(playerUiType::isInstance)
.forEach(playerUi -> {
playerUi.destroyPlayer();
playerUi.destroy();
});
playerUis.removeIf(playerUiType::isInstance);
}
public <T> Optional<T> get(final Class<T> playerUiType) {
return playerUis.stream()
.filter(playerUiType::isInstance)
.map(playerUiType::cast)
.findFirst();
}
public void call(final Consumer<PlayerUi> consumer) {
//noinspection SimplifyStreamApiCallChains
playerUis.stream().forEach(consumer);
}
}

View file

@ -0,0 +1,460 @@
package org.schabi.newpipe.player.ui;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.player.helper.PlayerHelper.buildCloseOverlayLayoutParams;
import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight;
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePopupLayoutParamsFromPrefs;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.AnticipateInterpolator;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PlayerBinding;
import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.gesture.BasePlayerGestureListener;
import org.schabi.newpipe.player.gesture.PopupPlayerGestureListener;
import org.schabi.newpipe.player.helper.PlayerHelper;
public final class PopupPlayerUi extends VideoPlayerUi {
private static final String TAG = PopupPlayerUi.class.getSimpleName();
/*//////////////////////////////////////////////////////////////////////////
// Popup player
//////////////////////////////////////////////////////////////////////////*/
private PlayerPopupCloseOverlayBinding closeOverlayBinding;
private boolean isPopupClosing = false;
private int screenWidth;
private int screenHeight;
/*//////////////////////////////////////////////////////////////////////////
// Popup player window manager
//////////////////////////////////////////////////////////////////////////*/
public static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
public static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
private WindowManager.LayoutParams popupLayoutParams; // null if player is not popup
private final WindowManager windowManager;
public PopupPlayerUi(@NonNull final Player player,
@NonNull final PlayerBinding playerBinding) {
super(player, playerBinding);
windowManager = ContextCompat.getSystemService(context, WindowManager.class);
}
@Override
public void setupAfterIntent() {
setupElementsVisibility();
binding.getRoot().setVisibility(View.VISIBLE);
initPopup();
initPopupCloseOverlay();
binding.playPauseButton.requestFocus();
}
@Override
BasePlayerGestureListener buildGestureListener() {
return new PopupPlayerGestureListener(this);
}
@SuppressLint("RtlHardcoded")
private void initPopup() {
if (DEBUG) {
Log.d(TAG, "initPopup() called");
}
// Popup is already added to windowManager
if (popupHasParent()) {
return;
}
updateScreenSize();
popupLayoutParams = retrievePopupLayoutParamsFromPrefs(this);
binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
checkPopupPositionBounds();
binding.loadingPanel.setMinimumWidth(popupLayoutParams.width);
binding.loadingPanel.setMinimumHeight(popupLayoutParams.height);
windowManager.addView(binding.getRoot(), popupLayoutParams);
// Popup doesn't have aspectRatio selector, using FIT automatically
setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
}
@SuppressLint("RtlHardcoded")
private void initPopupCloseOverlay() {
if (DEBUG) {
Log.d(TAG, "initPopupCloseOverlay() called");
}
// closeOverlayView is already added to windowManager
if (closeOverlayBinding != null) {
return;
}
closeOverlayBinding = PlayerPopupCloseOverlayBinding.inflate(LayoutInflater.from(context));
final WindowManager.LayoutParams closeOverlayLayoutParams = buildCloseOverlayLayoutParams();
closeOverlayBinding.closeButton.setVisibility(View.GONE);
windowManager.addView(closeOverlayBinding.getRoot(), closeOverlayLayoutParams);
}
@Override
protected void setupElementsVisibility() {
binding.fullScreenButton.setVisibility(View.VISIBLE);
binding.screenRotationButton.setVisibility(View.GONE);
binding.resizeTextView.setVisibility(View.GONE);
binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.GONE);
binding.queueButton.setVisibility(View.GONE);
binding.segmentsButton.setVisibility(View.GONE);
binding.moreOptionsButton.setVisibility(View.GONE);
binding.topControls.setOrientation(LinearLayout.HORIZONTAL);
binding.primaryControls.getLayoutParams().width
= LinearLayout.LayoutParams.WRAP_CONTENT;
binding.secondaryControls.setAlpha(1.0f);
binding.secondaryControls.setVisibility(View.VISIBLE);
binding.secondaryControls.setTranslationY(0);
binding.share.setVisibility(View.GONE);
binding.playWithKodi.setVisibility(View.GONE);
binding.openInBrowser.setVisibility(View.GONE);
binding.switchMute.setVisibility(View.GONE);
binding.playerCloseButton.setVisibility(View.GONE);
binding.topControls.bringToFront();
binding.topControls.setClickable(false);
binding.topControls.setFocusable(false);
binding.bottomControls.bringToFront();
super.setupElementsVisibility();
}
@Override
protected void setupElementsSize(final Resources resources) {
setupElementsSize(
0,
0,
resources.getDimensionPixelSize(R.dimen.player_popup_controls_padding),
resources.getDimensionPixelSize(R.dimen.player_popup_buttons_padding)
);
}
@Override
public void removeViewFromParent() {
// view was added by windowManager for popup player
windowManager.removeViewImmediate(binding.getRoot());
}
@Override
public void destroy() {
super.destroy();
removePopupFromView();
}
/*//////////////////////////////////////////////////////////////////////////
// Broadcast receiver
//////////////////////////////////////////////////////////////////////////*/
//region Broadcast receiver
@Override
public void onBroadcastReceived(final Intent intent) {
super.onBroadcastReceived(intent);
if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) {
updateScreenSize();
changePopupSize(popupLayoutParams.width);
checkPopupPositionBounds();
} else if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
// Use only audio source when screen turns off while popup player is playing
if (player.isPlaying() || player.isLoading()) {
player.useVideoSource(false);
}
} else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
// Restore video source when screen turns on and user is watching video in popup player
if (player.isPlaying() || player.isLoading()) {
player.useVideoSource(true);
}
}
}
//endregion
/**
* Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
* that goes from (0, 0) to (screenWidth, screenHeight).
* <p>
* If it's out of these boundaries, {@link #popupLayoutParams}' position is changed
* and {@code true} is returned to represent this change.
* </p>
*/
public void checkPopupPositionBounds() {
if (DEBUG) {
Log.d(TAG, "checkPopupPositionBounds() called with: "
+ "screenWidth = [" + screenWidth + "], "
+ "screenHeight = [" + screenHeight + "]");
}
if (popupLayoutParams == null) {
return;
}
if (popupLayoutParams.x < 0) {
popupLayoutParams.x = 0;
} else if (popupLayoutParams.x > screenWidth - popupLayoutParams.width) {
popupLayoutParams.x = screenWidth - popupLayoutParams.width;
}
if (popupLayoutParams.y < 0) {
popupLayoutParams.y = 0;
} else if (popupLayoutParams.y > screenHeight - popupLayoutParams.height) {
popupLayoutParams.y = screenHeight - popupLayoutParams.height;
}
}
public void updateScreenSize() {
final DisplayMetrics metrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(metrics);
screenWidth = metrics.widthPixels;
screenHeight = metrics.heightPixels;
if (DEBUG) {
Log.d(TAG, "updateScreenSize() called: screenWidth = ["
+ screenWidth + "], screenHeight = [" + screenHeight + "]");
}
}
/**
* Changes the size of the popup based on the width.
* @param width the new width, height is calculated with
* {@link PlayerHelper#getMinimumVideoHeight(float)}
*/
public void changePopupSize(final int width) {
if (DEBUG) {
Log.d(TAG, "changePopupSize() called with: width = [" + width + "]");
}
if (anyPopupViewIsNull()) {
return;
}
final float minimumWidth = context.getResources().getDimension(R.dimen.popup_minimum_width);
final int actualWidth = (int) (width > screenWidth ? screenWidth
: (width < minimumWidth ? minimumWidth : width));
final int actualHeight = (int) getMinimumVideoHeight(width);
if (DEBUG) {
Log.d(TAG, "updatePopupSize() updated values:"
+ " width = [" + actualWidth + "], height = [" + actualHeight + "]");
}
popupLayoutParams.width = actualWidth;
popupLayoutParams.height = actualHeight;
binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
windowManager.updateViewLayout(binding.getRoot(), popupLayoutParams);
}
private void changePopupWindowFlags(final int flags) {
if (DEBUG) {
Log.d(TAG, "changePopupWindowFlags() called with: flags = [" + flags + "]");
}
if (!anyPopupViewIsNull()) {
popupLayoutParams.flags = flags;
windowManager.updateViewLayout(binding.getRoot(), popupLayoutParams);
}
}
public void closePopup() {
if (DEBUG) {
Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing);
}
if (isPopupClosing) {
return;
}
isPopupClosing = true;
player.saveStreamProgressState();
windowManager.removeView(binding.getRoot());
animatePopupOverlayAndFinishService();
}
public boolean isPopupClosing() {
return isPopupClosing;
}
public void removePopupFromView() {
if (windowManager != null) {
// wrap in try-catch since it could sometimes generate errors randomly
try {
if (popupHasParent()) {
windowManager.removeView(binding.getRoot());
}
} catch (final IllegalArgumentException e) {
Log.w(TAG, "Failed to remove popup from window manager", e);
}
try {
final boolean closeOverlayHasParent = closeOverlayBinding != null
&& closeOverlayBinding.getRoot().getParent() != null;
if (closeOverlayHasParent) {
windowManager.removeView(closeOverlayBinding.getRoot());
}
} catch (final IllegalArgumentException e) {
Log.w(TAG, "Failed to remove popup overlay from window manager", e);
}
}
}
private void animatePopupOverlayAndFinishService() {
final int targetTranslationY =
(int) (closeOverlayBinding.closeButton.getRootView().getHeight()
- closeOverlayBinding.closeButton.getY());
closeOverlayBinding.closeButton.animate().setListener(null).cancel();
closeOverlayBinding.closeButton.animate()
.setInterpolator(new AnticipateInterpolator())
.translationY(targetTranslationY)
.setDuration(400)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(final Animator animation) {
end();
}
@Override
public void onAnimationEnd(final Animator animation) {
end();
}
private void end() {
windowManager.removeView(closeOverlayBinding.getRoot());
closeOverlayBinding = null;
player.getService().stopService();
}
}).start();
}
@Override
protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap) {
// no need for the end screen thumbnail to be resized on popup player: it's only needed
// for the main player so that it is enlarged correctly inside the fragment
return bitmap.getHeight();
}
private boolean popupHasParent() {
return binding != null
&& binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams
&& binding.getRoot().getParent() != null;
}
private boolean anyPopupViewIsNull() {
return popupLayoutParams == null || windowManager == null
|| binding.getRoot().getParent() == null;
}
@Override
public void onPlaying() {
super.onPlaying();
changePopupWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS);
}
@Override
public void onPaused() {
super.onPaused();
changePopupWindowFlags(IDLE_WINDOW_FLAGS);
}
@Override
public void onCompleted() {
super.onCompleted();
changePopupWindowFlags(IDLE_WINDOW_FLAGS);
}
@Override
protected void setupSubtitleView(final float captionScale) {
final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f;
binding.subtitleView.setFractionalTextSize(
SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio);
}
@Override
protected void onPlaybackSpeedClicked() {
playbackSpeedPopupMenu.show();
isSomePopupMenuVisible = true;
}
/*//////////////////////////////////////////////////////////////////////////
// Gestures
//////////////////////////////////////////////////////////////////////////*/
//region Gestures
private int distanceFromCloseButton(@NonNull final MotionEvent popupMotionEvent) {
final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft()
+ closeOverlayBinding.closeButton.getWidth() / 2;
final int closeOverlayButtonY = closeOverlayBinding.closeButton.getTop()
+ closeOverlayBinding.closeButton.getHeight() / 2;
final float fingerX = popupLayoutParams.x + popupMotionEvent.getX();
final float fingerY = popupLayoutParams.y + popupMotionEvent.getY();
return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2)
+ Math.pow(closeOverlayButtonY - fingerY, 2));
}
private float getClosingRadius() {
final int buttonRadius = closeOverlayBinding.closeButton.getWidth() / 2;
// 20% wider than the button itself
return buttonRadius * 1.2f;
}
public boolean isInsideClosingRadius(@NonNull final MotionEvent popupMotionEvent) {
return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius();
}
//endregion
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
//region Gestures
public PlayerPopupCloseOverlayBinding getCloseOverlayBinding() {
return closeOverlayBinding;
}
public WindowManager.LayoutParams getPopupLayoutParams() {
return popupLayoutParams;
}
public WindowManager getWindowManager() {
return windowManager;
}
public int getScreenHeight() {
return screenHeight;
}
public int getScreenWidth() {
return screenWidth;
}
//endregion
}

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,7 @@ import androidx.preference.PreferenceViewHolder;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding; import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding; import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.NotificationConstants; import org.schabi.newpipe.player.NotificationConstants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
@ -61,7 +61,7 @@ public class NotificationActionsPreference extends Preference {
public void onDetached() { public void onDetached() {
super.onDetached(); super.onDetached();
saveChanges(); saveChanges();
getContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION)); getContext().sendBroadcast(new Intent(PlayerService.ACTION_RECREATE_NOTIFICATION));
} }

View file

@ -50,8 +50,8 @@ import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment;
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.MainPlayer.PlayerType; import org.schabi.newpipe.player.PlayerService.PlayerType;
import org.schabi.newpipe.player.PlayQueueActivity; import org.schabi.newpipe.player.PlayQueueActivity;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
@ -91,7 +91,7 @@ public final class NavigationHelper {
intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey); intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey);
} }
} }
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal()); intent.putExtra(Player.PLAYER_TYPE, PlayerService.PlayerType.MAIN.ordinal());
intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback); intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
return intent; return intent;
@ -163,8 +163,8 @@ public final class NavigationHelper {
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback);
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.POPUP.ordinal()); intent.putExtra(Player.PLAYER_TYPE, PlayerService.PlayerType.POPUP.ordinal());
ContextCompat.startForegroundService(context, intent); ContextCompat.startForegroundService(context, intent);
} }
@ -174,8 +174,8 @@ public final class NavigationHelper {
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT) Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
.show(); .show();
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback);
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal()); intent.putExtra(Player.PLAYER_TYPE, PlayerService.PlayerType.AUDIO.ordinal());
ContextCompat.startForegroundService(context, intent); ContextCompat.startForegroundService(context, intent);
} }
@ -184,7 +184,7 @@ public final class NavigationHelper {
final PlayQueue queue, final PlayQueue queue,
final PlayerType playerType) { final PlayerType playerType) {
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
final Intent intent = getPlayerEnqueueIntent(context, MainPlayer.class, queue); final Intent intent = getPlayerEnqueueIntent(context, PlayerService.class, queue);
intent.putExtra(Player.PLAYER_TYPE, playerType.ordinal()); intent.putExtra(Player.PLAYER_TYPE, playerType.ordinal());
ContextCompat.startForegroundService(context, intent); ContextCompat.startForegroundService(context, intent);
@ -194,7 +194,7 @@ public final class NavigationHelper {
PlayerType playerType = PlayerHolder.getInstance().getType(); PlayerType playerType = PlayerHolder.getInstance().getType();
if (!PlayerHolder.getInstance().isPlayerOpen()) { if (!PlayerHolder.getInstance().isPlayerOpen()) {
Log.e(TAG, "Enqueueing but no player is open; defaulting to background player"); Log.e(TAG, "Enqueueing but no player is open; defaulting to background player");
playerType = MainPlayer.PlayerType.AUDIO; playerType = PlayerService.PlayerType.AUDIO;
} }
enqueueOnPlayer(context, queue, playerType); enqueueOnPlayer(context, queue, playerType);
@ -205,10 +205,10 @@ public final class NavigationHelper {
PlayerType playerType = PlayerHolder.getInstance().getType(); PlayerType playerType = PlayerHolder.getInstance().getType();
if (!PlayerHolder.getInstance().isPlayerOpen()) { if (!PlayerHolder.getInstance().isPlayerOpen()) {
Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player"); Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player");
playerType = MainPlayer.PlayerType.AUDIO; playerType = PlayerService.PlayerType.AUDIO;
} }
Toast.makeText(context, R.string.enqueued_next, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.enqueued_next, Toast.LENGTH_SHORT).show();
final Intent intent = getPlayerEnqueueNextIntent(context, MainPlayer.class, queue); final Intent intent = getPlayerEnqueueNextIntent(context, PlayerService.class, queue);
intent.putExtra(Player.PLAYER_TYPE, playerType.ordinal()); intent.putExtra(Player.PLAYER_TYPE, playerType.ordinal());
ContextCompat.startForegroundService(context, intent); ContextCompat.startForegroundService(context, intent);
@ -414,14 +414,14 @@ public final class NavigationHelper {
final boolean switchingPlayers) { final boolean switchingPlayers) {
final boolean autoPlay; final boolean autoPlay;
@Nullable final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType(); @Nullable final PlayerService.PlayerType playerType = PlayerHolder.getInstance().getType();
if (!PlayerHolder.getInstance().isPlayerOpen()) { if (!PlayerHolder.getInstance().isPlayerOpen()) {
// no player open // no player open
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context); autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
} else if (switchingPlayers) { } else if (switchingPlayers) {
// switching player to main player // switching player to main player
autoPlay = PlayerHolder.getInstance().isPlaying(); // keep play/pause state autoPlay = PlayerHolder.getInstance().isPlaying(); // keep play/pause state
} else if (playerType == MainPlayer.PlayerType.VIDEO) { } else if (playerType == PlayerService.PlayerType.MAIN) {
// opening new stream while already playing in main player // opening new stream while already playing in main player
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context); autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
} else { } else {
@ -436,7 +436,7 @@ public final class NavigationHelper {
// Situation when user switches from players to main player. All needed data is // Situation when user switches from players to main player. All needed data is
// here, we can start watching (assuming newQueue equals playQueue). // here, we can start watching (assuming newQueue equals playQueue).
// Starting directly in fullscreen if the previous player type was popup. // Starting directly in fullscreen if the previous player type was popup.
detailFragment.openVideoPlayer(playerType == MainPlayer.PlayerType.POPUP detailFragment.openVideoPlayer(playerType == PlayerService.PlayerType.POPUP
|| PlayerHelper.isStartMainPlayerFullscreenEnabled(context)); || PlayerHelper.isStartMainPlayerFullscreenEnabled(context));
} else { } else {
detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue); detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue);

View file

@ -12,8 +12,8 @@ import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.START
import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet
import org.schabi.newpipe.MainActivity import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.player.event.DisplayPortion import org.schabi.newpipe.player.gesture.DisplayPortion
import org.schabi.newpipe.player.event.DoubleTapListener import org.schabi.newpipe.player.gesture.DoubleTapListener
class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) : class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
ConstraintLayout(context, attrs), DoubleTapListener { ConstraintLayout(context, attrs), DoubleTapListener {

View file

@ -25,7 +25,7 @@
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
app:behavior_hideable="true" app:behavior_hideable="true"
app:behavior_peekHeight="0dp" app:behavior_peekHeight="0dp"
app:layout_behavior="org.schabi.newpipe.player.event.CustomBottomSheetBehavior" /> app:layout_behavior="org.schabi.newpipe.player.gesture.CustomBottomSheetBehavior" />
</org.schabi.newpipe.views.FocusAwareCoordinator> </org.schabi.newpipe.views.FocusAwareCoordinator>