2022-04-08 09:35:14 +02:00
|
|
|
package org.schabi.newpipe.player.ui;
|
|
|
|
|
2022-07-09 17:50:12 +02:00
|
|
|
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
2022-04-08 09:35:14 +02:00
|
|
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
|
|
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
|
2022-10-30 03:48:03 +01:00
|
|
|
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
|
2022-04-08 09:35:14 +02:00
|
|
|
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.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;
|
2022-04-14 18:40:55 +02:00
|
|
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
|
2022-04-08 09:35:14 +02:00
|
|
|
|
2022-04-09 10:48:34 +02:00
|
|
|
import android.app.Activity;
|
|
|
|
import android.content.Context;
|
2022-04-08 09:35:14 +02:00
|
|
|
import android.content.Intent;
|
|
|
|
import android.content.res.Resources;
|
|
|
|
import android.database.ContentObserver;
|
|
|
|
import android.graphics.Bitmap;
|
|
|
|
import android.graphics.Color;
|
|
|
|
import android.os.Handler;
|
2022-07-09 17:50:12 +02:00
|
|
|
import android.os.Looper;
|
2022-04-08 09:35:14 +02:00
|
|
|
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;
|
2023-02-14 08:27:04 +01:00
|
|
|
import android.view.WindowManager;
|
2022-04-08 09:35:14 +02:00
|
|
|
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;
|
2022-04-09 10:48:34 +02:00
|
|
|
import androidx.fragment.app.FragmentActivity;
|
2022-04-08 09:35:14 +02:00
|
|
|
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;
|
2022-10-24 05:14:41 +02:00
|
|
|
import org.schabi.newpipe.info_list.StreamSegmentItem;
|
2022-04-08 09:35:14 +02:00
|
|
|
import org.schabi.newpipe.ktx.AnimationType;
|
2022-04-16 16:01:23 +02:00
|
|
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
2022-04-08 09:35:14 +02:00
|
|
|
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;
|
2022-10-30 03:48:03 +01:00
|
|
|
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
2022-04-08 09:35:14 +02:00
|
|
|
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;
|
2022-10-24 05:14:41 +02:00
|
|
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
2022-04-08 09:35:14 +02:00
|
|
|
|
2022-11-02 16:09:33 +01:00
|
|
|
import java.util.Collections;
|
2022-04-08 09:35:14 +02:00
|
|
|
import java.util.List;
|
|
|
|
import java.util.Objects;
|
2022-04-09 10:48:34 +02:00
|
|
|
import java.util.Optional;
|
2022-04-08 09:35:14 +02:00
|
|
|
|
2022-04-09 10:48:34 +02:00
|
|
|
public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutChangeListener {
|
2022-04-08 09:35:14 +02:00
|
|
|
private static final String TAG = MainPlayerUi.class.getSimpleName();
|
|
|
|
|
2022-07-09 17:17:30 +02:00
|
|
|
// see the Javadoc of calculateMaxEndScreenThumbnailHeight for information
|
|
|
|
private static final int DETAIL_ROOT_MINIMUM_HEIGHT = 85; // dp
|
|
|
|
private static final int DETAIL_TITLE_TEXT_SIZE_TV = 16; // sp
|
|
|
|
private static final int DETAIL_TITLE_TEXT_SIZE_TABLET = 15; // sp
|
|
|
|
|
2022-04-08 09:35:14 +02:00
|
|
|
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;
|
|
|
|
|
2022-04-14 23:07:29 +02:00
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Constructor, setup, destroy
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
//region Constructor, setup, destroy
|
|
|
|
|
2022-04-08 09:35:14 +02:00
|
|
|
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();
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
2022-08-27 04:36:58 +02:00
|
|
|
binding.screenRotationButton.setOnClickListener(makeOnClickListener(() -> {
|
|
|
|
// 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();
|
|
|
|
}
|
|
|
|
}));
|
2022-04-08 09:35:14 +02:00
|
|
|
binding.queueButton.setOnClickListener(v -> onQueueClicked());
|
|
|
|
binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked());
|
|
|
|
|
|
|
|
binding.addToPlaylistButton.setOnClickListener(v ->
|
2022-04-09 10:48:34 +02:00
|
|
|
getParentActivity().map(FragmentActivity::getSupportFragmentManager)
|
2022-04-16 16:01:23 +02:00
|
|
|
.ifPresent(fragmentManager ->
|
|
|
|
PlaylistDialog.showForPlayQueue(player, fragmentManager)));
|
2022-04-08 09:35:14 +02:00
|
|
|
|
2022-07-09 17:50:12 +02:00
|
|
|
settingsContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
|
2022-04-08 09:35:14 +02:00
|
|
|
@Override
|
|
|
|
public void onChange(final boolean selfChange) {
|
|
|
|
setupScreenRotationButton();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
context.getContentResolver().registerContentObserver(
|
|
|
|
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
|
|
|
|
settingsContentObserver);
|
|
|
|
|
2022-04-09 10:48:34 +02:00
|
|
|
binding.getRoot().addOnLayoutChangeListener(this);
|
2022-08-27 04:36:58 +02:00
|
|
|
|
|
|
|
binding.moreOptionsButton.setOnLongClickListener(v -> {
|
|
|
|
player.getFragmentListener()
|
|
|
|
.ifPresent(PlayerServiceEventListener::onMoreOptionsLongClicked);
|
|
|
|
hideControls(0, 0);
|
|
|
|
hideSystemUIIfNeeded();
|
|
|
|
return true;
|
|
|
|
});
|
2022-04-09 10:48:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void deinitListeners() {
|
|
|
|
super.deinitListeners();
|
|
|
|
|
|
|
|
binding.queueButton.setOnClickListener(null);
|
|
|
|
binding.segmentsButton.setOnClickListener(null);
|
|
|
|
binding.addToPlaylistButton.setOnClickListener(null);
|
|
|
|
|
|
|
|
context.getContentResolver().unregisterContentObserver(settingsContentObserver);
|
|
|
|
|
|
|
|
binding.getRoot().removeOnLayoutChangeListener(this);
|
2022-04-08 09:35:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@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();
|
|
|
|
|
|
|
|
// 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));
|
2022-07-09 17:50:12 +02:00
|
|
|
binding.getRoot().setLayoutParams(new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
|
2022-04-08 09:35:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@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);
|
2022-07-09 17:50:12 +02:00
|
|
|
binding.primaryControls.getLayoutParams().width = MATCH_PARENT;
|
2022-04-08 09:35:14 +02:00
|
|
|
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);
|
|
|
|
|
2022-07-09 17:17:30 +02:00
|
|
|
binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
|
|
|
binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
2022-04-08 09:35:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@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)
|
|
|
|
);
|
|
|
|
}
|
2022-04-14 23:07:29 +02:00
|
|
|
//endregion
|
2022-04-08 09:35:14 +02:00
|
|
|
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Broadcast receiver
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
//region Broadcast receiver
|
2022-04-14 23:07:29 +02:00
|
|
|
|
2022-04-08 09:35:14 +02:00
|
|
|
@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
|
2022-04-14 23:07:29 +02:00
|
|
|
|
2022-04-08 09:35:14 +02:00
|
|
|
@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:
|
2022-04-09 10:48:34 +02:00
|
|
|
getParentActivity().ifPresent(activity -> {
|
|
|
|
player.setRecovery();
|
|
|
|
NavigationHelper.playOnPopupPlayer(activity, player.getPlayQueue(), true);
|
|
|
|
});
|
2022-04-08 09:35:14 +02:00
|
|
|
break;
|
|
|
|
case MINIMIZE_ON_EXIT_MODE_NONE: default:
|
|
|
|
player.pause();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
//endregion
|
|
|
|
|
2022-04-14 23:07:29 +02:00
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Playback states
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
//region Playback states
|
2022-04-08 09:35:14 +02:00
|
|
|
|
|
|
|
@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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-14 23:07:29 +02:00
|
|
|
@Override
|
|
|
|
public void onPlaying() {
|
|
|
|
super.onPlaying();
|
|
|
|
checkLandscape();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onCompleted() {
|
|
|
|
super.onCompleted();
|
|
|
|
if (isFullscreen) {
|
|
|
|
toggleFullscreen();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
//endregion
|
|
|
|
|
|
|
|
|
2022-04-08 09:35:14 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Controls showing / hiding
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
//region Controls showing / hiding
|
|
|
|
|
2022-04-16 16:01:23 +02:00
|
|
|
@Override
|
2022-04-08 09:35:14 +02:00
|
|
|
protected void showOrHideButtons() {
|
|
|
|
super.showOrHideButtons();
|
|
|
|
@Nullable final PlayQueue playQueue = player.getPlayQueue();
|
|
|
|
if (playQueue == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-08-28 00:26:10 +02:00
|
|
|
final boolean showQueue = !playQueue.getStreams().isEmpty();
|
2022-04-08 09:35:14 +02:00
|
|
|
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) {
|
2022-04-09 10:48:34 +02:00
|
|
|
getParentActivity().map(Activity::getWindow).ifPresent(window -> {
|
|
|
|
window.setStatusBarColor(Color.TRANSPARENT);
|
|
|
|
window.setNavigationBarColor(Color.TRANSPARENT);
|
2023-02-14 08:27:04 +01:00
|
|
|
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);
|
2022-04-09 10:48:34 +02:00
|
|
|
});
|
2022-04-08 09:35:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@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>
|
2022-07-09 17:17:30 +02:00
|
|
|
* 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
|
|
|
|
* {@link #DETAIL_ROOT_MINIMUM_HEIGHT} 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 {@link #DETAIL_TITLE_TEXT_SIZE_TABLET} on tablets and
|
|
|
|
* {@link #DETAIL_TITLE_TEXT_SIZE_TV} on TVs, see {@link R.id.titleTextView}.
|
2022-04-08 09:35:14 +02:00
|
|
|
* </li>
|
|
|
|
* <li>
|
|
|
|
* Otherwise, the max thumbnail height is the screen height.
|
|
|
|
* </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()) {
|
2022-07-09 17:17:30 +02:00
|
|
|
final int videoInfoHeight = DeviceUtils.dpToPx(DETAIL_ROOT_MINIMUM_HEIGHT, context)
|
|
|
|
+ DeviceUtils.spToPx(DETAIL_TITLE_TEXT_SIZE_TV, context);
|
2022-04-08 09:35:14 +02:00
|
|
|
return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight);
|
|
|
|
} else if (DeviceUtils.isTablet(context) && isLandscape() && !isFullscreen()) {
|
2022-07-09 17:17:30 +02:00
|
|
|
final int videoInfoHeight = DeviceUtils.dpToPx(DETAIL_ROOT_MINIMUM_HEIGHT, context)
|
|
|
|
+ DeviceUtils.spToPx(DETAIL_TITLE_TEXT_SIZE_TABLET, context);
|
2022-04-08 09:35:14 +02:00
|
|
|
return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight);
|
|
|
|
} else { // fullscreen player: max height is the device height
|
|
|
|
return Math.min(bitmap.getHeight(), screenHeight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-14 23:07:29 +02:00
|
|
|
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);
|
2022-04-08 09:35:14 +02:00
|
|
|
}
|
2022-04-14 23:07:29 +02:00
|
|
|
//endregion
|
2022-04-08 09:35:14 +02:00
|
|
|
|
|
|
|
|
2022-04-14 23:07:29 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Captions (text tracks)
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
//region Captions (text tracks)
|
2022-04-08 09:35:14 +02:00
|
|
|
|
|
|
|
@Override
|
2022-04-16 12:03:59 +02:00
|
|
|
protected void setupSubtitleView(final float captionScale) {
|
2022-04-08 09:35:14 +02:00
|
|
|
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);
|
|
|
|
}
|
2022-04-14 23:07:29 +02:00
|
|
|
//endregion
|
2022-04-08 09:35:14 +02:00
|
|
|
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Gestures
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
//region Gestures
|
|
|
|
|
|
|
|
@SuppressWarnings("checkstyle:ParameterNumber")
|
2022-04-09 10:48:34 +02:00
|
|
|
@Override
|
|
|
|
public 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) {
|
2022-04-08 09:35:14 +02:00
|
|
|
if (l != ol || t != ot || r != or || b != ob) {
|
2022-07-07 11:09:07 +02:00
|
|
|
// Use a smaller value to be consistent across screen orientations, and to make usage
|
|
|
|
// easier. Multiply by 3/4 to ensure the user does not need to move the finger up to the
|
|
|
|
// screen border, in order to reach the maximum volume/brightness.
|
2022-04-08 09:35:14 +02:00
|
|
|
final int width = r - l;
|
|
|
|
final int height = b - t;
|
|
|
|
final int min = Math.min(width, height);
|
2022-07-07 11:09:07 +02:00
|
|
|
final int maxGestureLength = (int) (min * 0.75);
|
2022-04-08 09:35:14 +02:00
|
|
|
|
|
|
|
if (DEBUG) {
|
|
|
|
Log.d(TAG, "maxGestureLength = " + maxGestureLength);
|
|
|
|
}
|
|
|
|
|
|
|
|
binding.volumeProgressBar.setMax(maxGestureLength);
|
|
|
|
binding.brightnessProgressBar.setMax(maxGestureLength);
|
|
|
|
|
|
|
|
setInitialGestureValues();
|
2022-07-15 19:53:48 +02:00
|
|
|
binding.itemsListPanel.getLayoutParams().height =
|
|
|
|
height - binding.itemsListPanel.getTop();
|
2022-04-08 09:35:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void setInitialGestureValues() {
|
|
|
|
if (player.getAudioReactor() != null) {
|
2022-04-09 10:48:34 +02:00
|
|
|
final float currentVolumeNormalized = (float) player.getAudioReactor().getVolume()
|
|
|
|
/ player.getAudioReactor().getMaxVolume();
|
2022-04-08 09:35:14 +02:00
|
|
|
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);
|
2022-10-24 05:14:41 +02:00
|
|
|
binding.itemsList.setLongClickable(true);
|
2022-04-08 09:35:14 +02:00
|
|
|
|
|
|
|
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,
|
2022-04-16 16:01:23 +02:00
|
|
|
AnimationType.SLIDE_AND_ALPHA, 0, () ->
|
2022-04-08 09:35:14 +02:00
|
|
|
// Even when queueLayout is GONE it receives touch events
|
|
|
|
// and ruins normal behavior of the app. This line fixes it
|
|
|
|
binding.itemsListPanel.setTranslationY(
|
2022-04-16 16:01:23 +02:00
|
|
|
-binding.itemsListPanel.getHeight() * 5.0f));
|
2022-04-08 09:35:14 +02:00
|
|
|
|
|
|
|
// 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() {
|
2022-10-24 05:14:41 +02:00
|
|
|
return new StreamSegmentAdapter.StreamSegmentListener() {
|
|
|
|
@Override
|
|
|
|
public void onItemClick(@NonNull final StreamSegmentItem item, final int seconds) {
|
|
|
|
segmentAdapter.selectSegment(item);
|
|
|
|
player.seekTo(seconds * 1000L);
|
|
|
|
player.triggerProgressUpdate();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onItemLongClick(@NonNull final StreamSegmentItem item, final int seconds) {
|
2022-10-30 03:48:03 +01:00
|
|
|
@Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
|
|
|
|
if (currentMetadata == null
|
|
|
|
|| currentMetadata.getServiceId() != YouTube.getServiceId()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-10-24 05:14:41 +02:00
|
|
|
final PlayQueueItem currentItem = player.getCurrentItem();
|
|
|
|
if (currentItem != null) {
|
|
|
|
String videoUrl = player.getVideoUrl();
|
|
|
|
videoUrl += ("&t=" + seconds);
|
|
|
|
ShareUtils.shareText(context, currentItem.getTitle(),
|
2023-05-01 23:11:48 +02:00
|
|
|
videoUrl, currentItem.getThumbnails());
|
2022-10-24 05:14:41 +02:00
|
|
|
}
|
|
|
|
}
|
2022-04-08 09:35:14 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private int getNearestStreamSegmentPosition(final long playbackPosition) {
|
|
|
|
int nearestPosition = 0;
|
2022-07-15 19:53:48 +02:00
|
|
|
final List<StreamSegment> segments = player.getCurrentStreamInfo()
|
2022-11-02 16:09:33 +01:00
|
|
|
.map(StreamInfo::getStreamSegments)
|
|
|
|
.orElse(Collections.emptyList());
|
2022-04-08 09:35:14 +02:00
|
|
|
|
|
|
|
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();
|
2022-04-09 10:48:34 +02:00
|
|
|
@Nullable final AppCompatActivity parentActivity = getParentActivity().orElse(null);
|
2022-04-08 09:35:14 +02:00
|
|
|
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
|
2022-04-14 23:07:29 +02:00
|
|
|
|
2022-04-08 09:35:14 +02:00
|
|
|
@Override
|
|
|
|
protected void onPlaybackSpeedClicked() {
|
2022-11-03 01:30:51 +01:00
|
|
|
getParentActivity().ifPresent(activity ->
|
|
|
|
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
|
|
|
|
player.getPlaybackPitch(), player.getPlaybackSkipSilence(),
|
|
|
|
player::setPlaybackParameters)
|
|
|
|
.show(activity.getSupportFragmentManager(), null));
|
2022-04-08 09:35:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
2022-04-14 23:07:29 +02:00
|
|
|
// Video size, orientation, fullscreen
|
2022-04-08 09:35:14 +02:00
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
2022-04-14 23:07:29 +02:00
|
|
|
//region Video size, orientation, fullscreen
|
2022-04-08 09:35:14 +02:00
|
|
|
|
|
|
|
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");
|
|
|
|
}
|
2022-07-15 19:53:48 +02:00
|
|
|
final PlayerServiceEventListener fragmentListener = player.getFragmentListener()
|
|
|
|
.orElse(null);
|
2022-04-08 09:35:14 +02:00
|
|
|
if (fragmentListener == null || player.exoPlayerIsNull()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
isFullscreen = !isFullscreen;
|
2022-04-09 10:48:34 +02:00
|
|
|
if (isFullscreen) {
|
2022-04-08 09:35:14 +02:00
|
|
|
// 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);
|
2022-04-09 10:48:34 +02:00
|
|
|
} else {
|
|
|
|
// 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);
|
2022-04-08 09:35:14 +02:00
|
|
|
}
|
|
|
|
fragmentListener.onFullscreenStateChanged(isFullscreen);
|
|
|
|
|
2022-07-09 17:17:30 +02:00
|
|
|
binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
|
|
|
binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
|
|
|
binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE);
|
2022-04-08 09:35:14 +02:00
|
|
|
setupScreenRotationButton();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void checkLandscape() {
|
|
|
|
// check if landscape is correct
|
2022-07-15 19:53:48 +02:00
|
|
|
final boolean videoInLandscapeButNotInFullscreen = isLandscape()
|
|
|
|
&& !isFullscreen
|
|
|
|
&& !player.isAudioOnly();
|
2022-04-08 09:35:14 +02:00
|
|
|
final boolean notPaused = player.getCurrentState() != STATE_COMPLETED
|
|
|
|
&& player.getCurrentState() != STATE_PAUSED;
|
|
|
|
|
|
|
|
if (videoInLandscapeButNotInFullscreen
|
|
|
|
&& notPaused
|
|
|
|
&& !DeviceUtils.isTablet(context)) {
|
|
|
|
toggleFullscreen();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
//endregion
|
|
|
|
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Getters
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
//region Getters
|
|
|
|
|
2022-11-03 01:30:51 +01:00
|
|
|
private Optional<Context> getParentContext() {
|
|
|
|
return Optional.ofNullable(binding.getRoot().getParent())
|
|
|
|
.filter(ViewGroup.class::isInstance)
|
|
|
|
.map(parent -> ((ViewGroup) parent).getContext());
|
|
|
|
}
|
|
|
|
|
2022-04-09 10:48:34 +02:00
|
|
|
public Optional<AppCompatActivity> getParentActivity() {
|
2022-11-03 01:30:51 +01:00
|
|
|
return getParentContext()
|
|
|
|
.filter(AppCompatActivity.class::isInstance)
|
|
|
|
.map(AppCompatActivity.class::cast);
|
2022-04-08 09:35:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public boolean isLandscape() {
|
|
|
|
// DisplayMetrics from activity context knows about MultiWindow feature
|
|
|
|
// while DisplayMetrics from app context doesn't
|
2022-11-03 01:30:51 +01:00
|
|
|
return DeviceUtils.isLandscape(getParentContext().orElse(player.getService()));
|
2022-04-08 09:35:14 +02:00
|
|
|
}
|
|
|
|
//endregion
|
|
|
|
}
|