Refactor player: separate UIs and more
This commit is contained in:
parent
bc3731265e
commit
76ced59b62
38 changed files with 4242 additions and 3564 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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
|
|
@ -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) {}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
937
app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
Normal file
937
app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
120
app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
Normal file
120
app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
Normal 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) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
1523
app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
Normal file
1523
app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue