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')) {
|
||||
preDebugBuild.dependsOn formatKtlint
|
||||
}
|
||||
preDebugBuild.dependsOn runCheckstyle, runKtlint
|
||||
//preDebugBuild.dependsOn runCheckstyle, runKtlint
|
||||
}
|
||||
|
||||
sonarqube {
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".player.MainPlayer"
|
||||
android:name=".player.PlayerService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
|
|
|
@ -60,7 +60,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
|||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.PlayerService;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
|
@ -630,8 +630,8 @@ public class RouterActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
// ...the player is not running or in normal Video-mode/type
|
||||
final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
return playerType == null || playerType == MainPlayer.PlayerType.VIDEO;
|
||||
final PlayerService.PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
return playerType == null || playerType == PlayerService.PlayerType.MAIN;
|
||||
}
|
||||
|
||||
private void openAddToPlaylistDialog() {
|
||||
|
|
|
@ -43,6 +43,7 @@ import androidx.appcompat.widget.Toolbar;
|
|||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
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.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.PlayerService;
|
||||
import org.schabi.newpipe.player.PlayerService.PlayerType;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||
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.PlayQueueItem;
|
||||
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.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
@ -106,6 +109,7 @@ import java.util.Iterator;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.State;
|
||||
|
@ -202,7 +206,7 @@ public final class VideoDetailFragment
|
|||
|
||||
private ContentObserver settingsContentObserver;
|
||||
@Nullable
|
||||
private MainPlayer playerService;
|
||||
private PlayerService playerService;
|
||||
private Player player;
|
||||
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
|
||||
|
||||
|
@ -211,7 +215,7 @@ public final class VideoDetailFragment
|
|||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@Override
|
||||
public void onServiceConnected(final Player connectedPlayer,
|
||||
final MainPlayer connectedPlayerService,
|
||||
final PlayerService connectedPlayerService,
|
||||
final boolean playAfterConnect) {
|
||||
player = connectedPlayer;
|
||||
playerService = connectedPlayerService;
|
||||
|
@ -219,6 +223,7 @@ public final class VideoDetailFragment
|
|||
// It will do nothing if the player is not in fullscreen mode
|
||||
hideSystemUiIfNeeded();
|
||||
|
||||
final Optional<MainPlayerUi> playerUi = player.UIs().get(MainPlayerUi.class);
|
||||
if (!player.videoPlayerSelected() && !playAfterConnect) {
|
||||
return;
|
||||
}
|
||||
|
@ -227,22 +232,23 @@ public final class VideoDetailFragment
|
|||
// If the video is playing but orientation changed
|
||||
// let's make the video in fullscreen again
|
||||
checkLandscape();
|
||||
} else if (player.isFullscreen() && !player.isVerticalVideo()
|
||||
} else if (playerUi.map(ui -> ui.isFullscreen() && !ui.isVerticalVideo()).orElse(false)
|
||||
// Tablet UI has orientation-independent fullscreen
|
||||
&& !DeviceUtils.isTablet(activity)) {
|
||||
// Device is in portrait orientation after rotation but UI is in fullscreen.
|
||||
// Return back to non-fullscreen state
|
||||
player.toggleFullscreen();
|
||||
playerUi.ifPresent(MainPlayerUi::toggleFullscreen);
|
||||
}
|
||||
|
||||
if (playerIsNotStopped() && player.videoPlayerSelected()) {
|
||||
addVideoPlayerView();
|
||||
}
|
||||
|
||||
//noinspection SimplifyOptionalCallChains
|
||||
if (playAfterConnect
|
||||
|| (currentInfo != null
|
||||
&& isAutoplayEnabled()
|
||||
&& player.getParentActivity() == null)) {
|
||||
&& !playerUi.isPresent())) {
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
openVideoPlayerAutoFullscreen();
|
||||
}
|
||||
|
@ -518,7 +524,7 @@ public final class VideoDetailFragment
|
|||
case R.id.overlay_play_pause_button:
|
||||
if (playerIsNotStopped()) {
|
||||
player.playPause();
|
||||
player.hideControls(0, 0);
|
||||
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
||||
showSystemUi();
|
||||
} else {
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
|
@ -583,12 +589,12 @@ public final class VideoDetailFragment
|
|||
if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) {
|
||||
binding.detailVideoTitleView.setMaxLines(10);
|
||||
animateRotation(binding.detailToggleSecondaryControlsView,
|
||||
Player.DEFAULT_CONTROLS_DURATION, 180);
|
||||
VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180);
|
||||
binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.detailVideoTitleView.setMaxLines(1);
|
||||
animateRotation(binding.detailToggleSecondaryControlsView,
|
||||
Player.DEFAULT_CONTROLS_DURATION, 0);
|
||||
VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 0);
|
||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||
}
|
||||
// view pager height has changed, update the tab layout
|
||||
|
@ -746,7 +752,9 @@ public final class VideoDetailFragment
|
|||
|
||||
@Override
|
||||
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
|
||||
|
@ -756,7 +764,7 @@ public final class VideoDetailFragment
|
|||
}
|
||||
|
||||
// If we are in fullscreen mode just exit from it via first back press
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
if (isFullscreen()) {
|
||||
if (!DeviceUtils.isTablet(activity)) {
|
||||
player.pause();
|
||||
}
|
||||
|
@ -1006,8 +1014,7 @@ public final class VideoDetailFragment
|
|||
getChildFragmentManager().beginTransaction()
|
||||
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
|
||||
.commitAllowingStateLoss();
|
||||
binding.relatedItemsLayout.setVisibility(
|
||||
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.VISIBLE);
|
||||
binding.relatedItemsLayout.setVisibility(isFullscreen() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1087,8 +1094,12 @@ public final class VideoDetailFragment
|
|||
private void toggleFullscreenIfInFullscreenMode() {
|
||||
// If a user watched video inside fullscreen mode and than chose another player
|
||||
// return to non-fullscreen mode
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
player.toggleFullscreen();
|
||||
if (isPlayerAvailable()) {
|
||||
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);
|
||||
|
||||
// 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();
|
||||
|
||||
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
|
||||
MainPlayer.class, queue, true, autoPlayEnabled);
|
||||
PlayerService.class, queue, true, autoPlayEnabled);
|
||||
ContextCompat.startForegroundService(activity, playerIntent);
|
||||
}
|
||||
|
||||
|
@ -1235,8 +1240,8 @@ public final class VideoDetailFragment
|
|||
* be reused in a few milliseconds and the flickering would be annoying.
|
||||
*/
|
||||
private void hideMainPlayerOnLoadingNewStream() {
|
||||
if (!isPlayerServiceAvailable()
|
||||
|| playerService.getView() == null
|
||||
//noinspection SimplifyOptionalCallChains
|
||||
if (!isPlayerServiceAvailable() || !getRoot().isPresent()
|
||||
|| !player.videoPlayerSelected()) {
|
||||
return;
|
||||
}
|
||||
|
@ -1244,7 +1249,7 @@ public final class VideoDetailFragment
|
|||
removeVideoPlayerView();
|
||||
if (isAutoplayEnabled()) {
|
||||
playerService.stopForImmediateReusing();
|
||||
playerService.getView().setVisibility(View.GONE);
|
||||
getRoot().ifPresent(view -> view.setVisibility(View.GONE));
|
||||
} else {
|
||||
playerHolder.stopService();
|
||||
}
|
||||
|
@ -1302,26 +1307,33 @@ public final class VideoDetailFragment
|
|||
}
|
||||
|
||||
private void addVideoPlayerView() {
|
||||
if (!isPlayerAvailable() || getView() == null) {
|
||||
if (!isPlayerAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if viewHolder already contains a child
|
||||
if (player.getRootView().getParent() != binding.playerPlaceholder) {
|
||||
final Optional<View> root = player.UIs().get(VideoPlayerUi.class)
|
||||
.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();
|
||||
}
|
||||
}*/
|
||||
setHeightThumbnail();
|
||||
|
||||
// Prevent from re-adding a view multiple times
|
||||
if (player.getRootView().getParent() == null) {
|
||||
binding.playerPlaceholder.addView(player.getRootView());
|
||||
if (root.isPresent() && root.get().getParent() == null) {
|
||||
binding.playerPlaceholder.addView(root.get());
|
||||
}
|
||||
}
|
||||
|
||||
private void removeVideoPlayerView() {
|
||||
makeDefaultHeightForVideoPlaceholder();
|
||||
|
||||
playerService.removeViewFromParent();
|
||||
if (player != null) {
|
||||
player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
|
||||
}
|
||||
}
|
||||
|
||||
private void makeDefaultHeightForVideoPlaceholder() {
|
||||
|
@ -1362,7 +1374,7 @@ public final class VideoDetailFragment
|
|||
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
|
||||
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
|
||||
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
if (isFullscreen()) {
|
||||
final int height = (DeviceUtils.isInMultiWindow(activity)
|
||||
? requireView()
|
||||
: activity.getWindow().getDecorView()).getHeight();
|
||||
|
@ -1387,8 +1399,9 @@ public final class VideoDetailFragment
|
|||
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
|
||||
if (isPlayerAvailable()) {
|
||||
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
|
||||
player.getSurfaceView()
|
||||
.setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight);
|
||||
player.UIs().get(VideoPlayerUi.class).ifPresent(ui ->
|
||||
ui.getBinding().surfaceView.setHeights(newHeight,
|
||||
ui.isFullscreen() ? newHeight : maxHeight));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1517,7 +1530,7 @@ public final class VideoDetailFragment
|
|||
if (binding.relatedItemsLayout != null) {
|
||||
if (showRelatedItems) {
|
||||
binding.relatedItemsLayout.setVisibility(
|
||||
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.INVISIBLE);
|
||||
isFullscreen() ? View.GONE : View.INVISIBLE);
|
||||
} else {
|
||||
binding.relatedItemsLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
@ -1778,6 +1791,14 @@ public final class VideoDetailFragment
|
|||
// 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
|
||||
public void onQueueUpdate(final PlayQueue queue) {
|
||||
playQueue = queue;
|
||||
|
@ -1898,15 +1919,10 @@ public final class VideoDetailFragment
|
|||
@Override
|
||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||
setupBrightness();
|
||||
//noinspection SimplifyOptionalCallChains
|
||||
if (!isPlayerAndPlayerServiceAvailable()
|
||||
|| playerService.getView() == null
|
||||
|| player.getParentActivity() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final View view = playerService.getView();
|
||||
final ViewGroup parent = (ViewGroup) view.getParent();
|
||||
if (parent == null) {
|
||||
|| !player.UIs().get(MainPlayerUi.class).isPresent()
|
||||
|| getRoot().map(View::getParent).orElse(null) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1934,7 +1950,7 @@ public final class VideoDetailFragment
|
|||
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
||||
if (DeviceUtils.isTablet(activity)
|
||||
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
||||
player.toggleFullscreen();
|
||||
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2017,7 +2033,7 @@ public final class VideoDetailFragment
|
|||
}
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||
|
||||
if (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen())) {
|
||||
if (isInMultiWindow || isFullscreen()) {
|
||||
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
||||
}
|
||||
|
@ -2026,13 +2042,17 @@ public final class VideoDetailFragment
|
|||
|
||||
// Listener implementation
|
||||
public void hideSystemUiIfNeeded() {
|
||||
if (isPlayerAvailable()
|
||||
&& player.isFullscreen()
|
||||
if (isFullscreen()
|
||||
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
hideSystemUi();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFullscreen() {
|
||||
return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class)
|
||||
.map(VideoPlayerUi::isFullscreen).orElse(false);
|
||||
}
|
||||
|
||||
private boolean playerIsNotStopped() {
|
||||
return isPlayerAvailable() && !player.isStopped();
|
||||
}
|
||||
|
@ -2055,10 +2075,7 @@ public final class VideoDetailFragment
|
|||
}
|
||||
|
||||
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
|
||||
if (!isPlayerAvailable()
|
||||
|| !player.videoPlayerSelected()
|
||||
|| !player.isFullscreen()
|
||||
|| bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
|
||||
if (!isFullscreen() || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
|
||||
// Apply system brightness when the player is not in fullscreen
|
||||
restoreDefaultBrightness();
|
||||
} else {
|
||||
|
@ -2082,7 +2099,7 @@ public final class VideoDetailFragment
|
|||
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
|
||||
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
|
||||
player.play();
|
||||
|
@ -2309,10 +2326,10 @@ public final class VideoDetailFragment
|
|||
if (DeviceUtils.isLandscape(requireContext())
|
||||
&& isPlayerAvailable()
|
||||
&& player.isPlaying()
|
||||
&& !player.isFullscreen()
|
||||
&& !DeviceUtils.isTablet(activity)
|
||||
&& player.videoPlayerSelected()) {
|
||||
player.toggleFullscreen();
|
||||
&& !isFullscreen()
|
||||
&& !DeviceUtils.isTablet(activity)) {
|
||||
player.UIs().get(MainPlayerUi.class)
|
||||
.ifPresent(MainPlayerUi::toggleFullscreen);
|
||||
}
|
||||
setOverlayLook(binding.appBarLayout, behavior, 1);
|
||||
break;
|
||||
|
@ -2325,17 +2342,22 @@ public final class VideoDetailFragment
|
|||
// Re-enable clicks
|
||||
setOverlayElementsClickable(true);
|
||||
if (isPlayerAvailable()) {
|
||||
player.closeItemsList();
|
||||
player.UIs().get(MainPlayerUi.class)
|
||||
.ifPresent(MainPlayerUi::closeItemsList);
|
||||
}
|
||||
setOverlayLook(binding.appBarLayout, behavior, 0);
|
||||
break;
|
||||
case BottomSheetBehavior.STATE_DRAGGING:
|
||||
case BottomSheetBehavior.STATE_SETTLING:
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
if (isFullscreen()) {
|
||||
showSystemUi();
|
||||
}
|
||||
if (isPlayerAvailable() && player.isControlsVisible()) {
|
||||
player.hideControls(0, 0);
|
||||
if (isPlayerAvailable()) {
|
||||
player.UIs().get(MainPlayerUi.class).ifPresent(ui -> {
|
||||
if (ui.isControlsVisible()) {
|
||||
ui.hideControls(0, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -2409,4 +2431,13 @@ public final class VideoDetailFragment
|
|||
boolean isPlayerAndPlayerServiceAvailable() {
|
||||
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.local.subscription.SubscriptionManager;
|
||||
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.PlayQueue;
|
||||
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.local.dialog.PlaylistDialog;
|
||||
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.PlaylistPlayQueue;
|
||||
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.local.BaseLocalListFragment;
|
||||
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.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
|
|
@ -26,14 +26,14 @@ import java.util.List;
|
|||
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_ONE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
|
||||
import static org.schabi.newpipe.player.PlayerService.ACTION_CLOSE;
|
||||
import static org.schabi.newpipe.player.PlayerService.ACTION_FAST_FORWARD;
|
||||
import static org.schabi.newpipe.player.PlayerService.ACTION_FAST_REWIND;
|
||||
import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_NEXT;
|
||||
import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PAUSE;
|
||||
import static org.schabi.newpipe.player.PlayerService.ACTION_PLAY_PREVIOUS;
|
||||
import static org.schabi.newpipe.player.PlayerService.ACTION_REPEAT;
|
||||
import static org.schabi.newpipe.player.PlayerService.ACTION_SHUFFLE;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
notificationBuilder = createNotification(player);
|
||||
}
|
||||
|
|
|
@ -51,7 +51,9 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
|
||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||
|
||||
protected Player player;
|
||||
private Player player;
|
||||
|
||||
private PlayQueueAdapter adapter = null;
|
||||
|
||||
private boolean serviceBound;
|
||||
private ServiceConnection serviceConnection;
|
||||
|
@ -132,7 +134,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
openPlaybackParameterDialog();
|
||||
return true;
|
||||
case R.id.action_mute:
|
||||
player.onMuteUnmuteButtonClicked();
|
||||
player.toggleMute();
|
||||
return true;
|
||||
case R.id.action_system_audio:
|
||||
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
|
||||
|
@ -168,7 +170,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
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);
|
||||
if (!success) {
|
||||
unbindService(serviceConnection);
|
||||
|
@ -184,10 +186,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
player.removeActivityListener(this);
|
||||
}
|
||||
|
||||
if (player != null && player.getPlayQueueAdapter() != null) {
|
||||
player.getPlayQueueAdapter().unsetSelectedListener();
|
||||
}
|
||||
queueControlBinding.playQueue.setAdapter(null);
|
||||
onQueueUpdate(null);
|
||||
if (itemTouchHelper != null) {
|
||||
itemTouchHelper.attachToRecyclerView(null);
|
||||
}
|
||||
|
@ -210,15 +209,15 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
|
||||
if (service instanceof PlayerServiceBinder) {
|
||||
player = ((PlayerServiceBinder) service).getPlayerInstance();
|
||||
} else if (service instanceof MainPlayer.LocalBinder) {
|
||||
player = ((MainPlayer.LocalBinder) service).getPlayer();
|
||||
} else if (service instanceof PlayerService.LocalBinder) {
|
||||
player = ((PlayerService.LocalBinder) service).getPlayer();
|
||||
}
|
||||
|
||||
if (player == null || player.getPlayQueue() == null
|
||||
|| player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) {
|
||||
if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
|
||||
unbind();
|
||||
finish();
|
||||
} else {
|
||||
onQueueUpdate(player.getPlayQueue());
|
||||
buildComponents();
|
||||
if (player != null) {
|
||||
player.setActivityListener(PlayQueueActivity.this);
|
||||
|
@ -241,7 +240,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
|
||||
private void buildQueue() {
|
||||
queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this));
|
||||
queueControlBinding.playQueue.setAdapter(player.getPlayQueueAdapter());
|
||||
queueControlBinding.playQueue.setClickable(true);
|
||||
queueControlBinding.playQueue.setLongClickable(true);
|
||||
queueControlBinding.playQueue.clearOnScrollListeners();
|
||||
|
@ -249,8 +247,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
|
||||
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||
itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue);
|
||||
|
||||
player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener());
|
||||
}
|
||||
|
||||
private void buildMetadata() {
|
||||
|
@ -370,7 +366,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
}
|
||||
|
||||
if (view.getId() == queueControlBinding.controlRepeat.getId()) {
|
||||
player.onRepeatClicked();
|
||||
player.cycleNextRepeatMode();
|
||||
} else if (view.getId() == queueControlBinding.controlBackward.getId()) {
|
||||
player.playPrevious();
|
||||
} else if (view.getId() == queueControlBinding.controlFastRewind.getId()) {
|
||||
|
@ -382,7 +378,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
} else if (view.getId() == queueControlBinding.controlForward.getId()) {
|
||||
player.playNext();
|
||||
} else if (view.getId() == queueControlBinding.controlShuffle.getId()) {
|
||||
player.onShuffleClicked();
|
||||
player.toggleShuffleModeEnabled();
|
||||
} else if (view.getId() == queueControlBinding.metadata.getId()) {
|
||||
scrollToSelected();
|
||||
} else if (view.getId() == queueControlBinding.liveSync.getId()) {
|
||||
|
@ -445,7 +441,15 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@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
|
||||
|
@ -454,7 +458,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
onStateChanged(state);
|
||||
onPlayModeChanged(repeatMode, shuffled);
|
||||
onPlaybackParameterChanged(parameters);
|
||||
onMaybePlaybackAdapterChanged();
|
||||
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() {
|
||||
if (menu != null && player != null) {
|
||||
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;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
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.databinding.PlayerBinding;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
|
||||
/**
|
||||
* One service for all players.
|
||||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
public final class MainPlayer extends Service {
|
||||
private static final String TAG = "MainPlayer";
|
||||
public final class PlayerService extends Service {
|
||||
private static final String TAG = PlayerService.class.getSimpleName();
|
||||
private static final boolean DEBUG = Player.DEBUG;
|
||||
|
||||
private Player player;
|
||||
private WindowManager windowManager;
|
||||
|
||||
private final IBinder mBinder = new MainPlayer.LocalBinder();
|
||||
private final IBinder mBinder = new PlayerService.LocalBinder();
|
||||
|
||||
public enum PlayerType {
|
||||
VIDEO,
|
||||
MAIN,
|
||||
AUDIO,
|
||||
POPUP
|
||||
}
|
||||
|
@ -67,7 +58,7 @@ public final class MainPlayer extends Service {
|
|||
|
||||
static final String ACTION_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";
|
||||
static final String ACTION_REPEAT
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
|
||||
|
@ -94,19 +85,12 @@ public final class MainPlayer extends Service {
|
|||
Log.d(TAG, "onCreate() called");
|
||||
}
|
||||
assureCorrectAppLanguage(this);
|
||||
windowManager = ContextCompat.getSystemService(this, WindowManager.class);
|
||||
|
||||
ThemeHelper.setTheme(this);
|
||||
createView();
|
||||
}
|
||||
|
||||
private void createView() {
|
||||
final PlayerBinding binding = PlayerBinding.inflate(LayoutInflater.from(this));
|
||||
|
||||
player = new Player(this);
|
||||
player.setupFromView(binding);
|
||||
|
||||
NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
|
||||
/*final MainPlayerUi mainPlayerUi = new MainPlayerUi(player,
|
||||
PlayerBinding.inflate(LayoutInflater.from(this)));
|
||||
player.UIs().add(mainPlayerUi);*/
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -121,11 +105,6 @@ public final class MainPlayer extends Service {
|
|||
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);
|
||||
if (player.getMediaSessionManager() != null) {
|
||||
player.getMediaSessionManager().handleMediaButtonIntent(intent);
|
||||
|
@ -144,13 +123,7 @@ public final class MainPlayer extends Service {
|
|||
// Releases wifi & cpu, disables keepScreenOn, etc.
|
||||
// We can't just pause the player here because it will make transition
|
||||
// from one stream to a new stream not smooth
|
||||
player.smoothStopPlayer();
|
||||
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();
|
||||
player.smoothStopForImmediateReusing();
|
||||
|
||||
// Notification shows information about old stream but if a user selects
|
||||
// a stream from backStack it's not actual anymore
|
||||
|
@ -180,18 +153,7 @@ public final class MainPlayer extends Service {
|
|||
|
||||
private void cleanup() {
|
||||
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 = null;
|
||||
}
|
||||
}
|
||||
|
@ -212,48 +174,14 @@ public final class MainPlayer extends Service {
|
|||
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 MainPlayer getService() {
|
||||
return MainPlayer.this;
|
||||
public PlayerService getService() {
|
||||
return PlayerService.this;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
|
||||
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;
|
||||
|
||||
public interface PlayerServiceEventListener extends PlayerEventListener {
|
||||
void onViewCreated();
|
||||
|
||||
void onFullscreenStateChanged(boolean fullscreen);
|
||||
|
||||
void onScreenRotationButtonClicked();
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package org.schabi.newpipe.player.event;
|
||||
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.PlayerService;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
|
||||
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
|
||||
void onServiceConnected(Player player,
|
||||
MainPlayer playerService,
|
||||
PlayerService playerService,
|
||||
boolean playAfterConnect);
|
||||
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.graphics.Rect;
|
|
@ -1,4 +1,4 @@
|
|||
package org.schabi.newpipe.player.event
|
||||
package org.schabi.newpipe.player.gesture
|
||||
|
||||
enum class DisplayPortion {
|
||||
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 {
|
||||
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.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.SliderStrategy;
|
||||
|
||||
|
@ -207,7 +207,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||
? View.VISIBLE
|
||||
: View.GONE);
|
||||
animateRotation(binding.pitchToogleControlModes,
|
||||
Player.DEFAULT_CONTROLS_DURATION,
|
||||
VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
|
||||
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_OFF;
|
||||
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.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
|
||||
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_NONE;
|
||||
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 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.SubtitlesStream;
|
||||
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.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.player.ui.PopupPlayerUi;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
|
@ -339,10 +340,6 @@ public final class PlayerHelper {
|
|||
return true;
|
||||
}
|
||||
|
||||
public static int getTossFlingVelocity() {
|
||||
return 2500;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
|
||||
final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
|
||||
|
@ -452,10 +449,10 @@ public final class PlayerHelper {
|
|||
// 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
|
||||
return MainPlayer.PlayerType.values()[
|
||||
intent.getIntExtra(PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal())];
|
||||
return PlayerService.PlayerType.values()[
|
||||
intent.getIntExtra(PLAYER_TYPE, PlayerService.PlayerType.MAIN.ordinal())];
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
@SuppressLint("RtlHardcoded")
|
||||
public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs(
|
||||
final Player player) {
|
||||
final boolean popupRememberSizeAndPos = player.getPrefs().getBoolean(
|
||||
player.getContext().getString(R.string.popup_remember_size_pos_key), true);
|
||||
final float defaultSize =
|
||||
player.getContext().getResources().getDimension(R.dimen.popup_default_width);
|
||||
final PopupPlayerUi playerUi) {
|
||||
final SharedPreferences prefs = playerUi.getPlayer().getPrefs();
|
||||
final Context context = playerUi.getPlayer().getContext();
|
||||
|
||||
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
|
||||
? player.getPrefs().getFloat(player.getContext().getString(
|
||||
R.string.popup_saved_width_key), defaultSize)
|
||||
? prefs.getFloat(context.getString(R.string.popup_saved_width_key), defaultSize)
|
||||
: defaultSize;
|
||||
final float popupHeight = getMinimumVideoHeight(popupWidth);
|
||||
|
||||
|
@ -553,27 +551,26 @@ public final class PlayerHelper {
|
|||
popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
||||
popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
||||
|
||||
final int centerX = (int) (player.getScreenWidth() / 2f - popupWidth / 2f);
|
||||
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
|
||||
final int centerX = (int) (playerUi.getScreenWidth() / 2f - popupWidth / 2f);
|
||||
final int centerY = (int) (playerUi.getScreenHeight() / 2f - popupHeight / 2f);
|
||||
popupLayoutParams.x = popupRememberSizeAndPos
|
||||
? player.getPrefs().getInt(player.getContext().getString(
|
||||
R.string.popup_saved_x_key), centerX) : centerX;
|
||||
? prefs.getInt(context.getString(R.string.popup_saved_x_key), centerX) : centerX;
|
||||
popupLayoutParams.y = popupRememberSizeAndPos
|
||||
? player.getPrefs().getInt(player.getContext().getString(
|
||||
R.string.popup_saved_y_key), centerY) : centerY;
|
||||
? prefs.getInt(context.getString(R.string.popup_saved_y_key), centerY) : centerY;
|
||||
|
||||
return popupLayoutParams;
|
||||
}
|
||||
|
||||
public static void savePopupPositionAndSizeToPrefs(final Player player) {
|
||||
if (player.getPopupLayoutParams() != null) {
|
||||
player.getPrefs().edit()
|
||||
.putFloat(player.getContext().getString(R.string.popup_saved_width_key),
|
||||
player.getPopupLayoutParams().width)
|
||||
.putInt(player.getContext().getString(R.string.popup_saved_x_key),
|
||||
player.getPopupLayoutParams().x)
|
||||
.putInt(player.getContext().getString(R.string.popup_saved_y_key),
|
||||
player.getPopupLayoutParams().y)
|
||||
public static void savePopupPositionAndSizeToPrefs(final PopupPlayerUi playerUi) {
|
||||
if (playerUi.getPopupLayoutParams() != null) {
|
||||
final Context context = playerUi.getPlayer().getContext();
|
||||
playerUi.getPlayer().getPrefs().edit()
|
||||
.putFloat(context.getString(R.string.popup_saved_width_key),
|
||||
playerUi.getPopupLayoutParams().width)
|
||||
.putInt(context.getString(R.string.popup_saved_x_key),
|
||||
playerUi.getPopupLayoutParams().x)
|
||||
.putInt(context.getString(R.string.popup_saved_y_key),
|
||||
playerUi.getPopupLayoutParams().y)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
|||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
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.event.PlayerServiceEventListener;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||
|
@ -42,17 +42,17 @@ public final class PlayerHolder {
|
|||
|
||||
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
|
||||
private boolean bound;
|
||||
@Nullable private MainPlayer playerService;
|
||||
@Nullable private PlayerService playerService;
|
||||
@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.
|
||||
*
|
||||
* @return Current PlayerType
|
||||
*/
|
||||
@Nullable
|
||||
public MainPlayer.PlayerType getType() {
|
||||
public PlayerService.PlayerType getType() {
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ public final class PlayerHolder {
|
|||
// and NullPointerExceptions inside the service because the service will be
|
||||
// bound twice. Prevent it with unbinding first
|
||||
unbind(context);
|
||||
ContextCompat.startForegroundService(context, new Intent(context, MainPlayer.class));
|
||||
ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class));
|
||||
serviceConnection.doPlayAfterConnect(playAfterConnect);
|
||||
bind(context);
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ public final class PlayerHolder {
|
|||
public void stopService() {
|
||||
final Context context = getCommonContext();
|
||||
unbind(context);
|
||||
context.stopService(new Intent(context, MainPlayer.class));
|
||||
context.stopService(new Intent(context, PlayerService.class));
|
||||
}
|
||||
|
||||
class PlayerServiceConnection implements ServiceConnection {
|
||||
|
@ -156,7 +156,7 @@ public final class PlayerHolder {
|
|||
if (DEBUG) {
|
||||
Log.d(TAG, "Player service is connected");
|
||||
}
|
||||
final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service;
|
||||
final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service;
|
||||
|
||||
playerService = localBinder.getService();
|
||||
player = localBinder.getPlayer();
|
||||
|
@ -172,7 +172,7 @@ public final class PlayerHolder {
|
|||
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,
|
||||
Context.BIND_AUTO_CREATE);
|
||||
if (!bound) {
|
||||
|
@ -211,6 +211,13 @@ public final class PlayerHolder {
|
|||
|
||||
private final PlayerServiceEventListener internalListener =
|
||||
new PlayerServiceEventListener() {
|
||||
@Override
|
||||
public void onViewCreated() {
|
||||
if (listener != null) {
|
||||
listener.onViewCreated();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||
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.mediasession.MediaSessionCallback;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class PlayerMediaSession implements MediaSessionCallback {
|
||||
private final Player player;
|
||||
|
@ -89,7 +92,7 @@ public class PlayerMediaSession implements MediaSessionCallback {
|
|||
public void play() {
|
||||
player.play();
|
||||
// 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
|
||||
|
|
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.databinding.ListRadioIconItemBinding;
|
||||
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.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
@ -61,7 +61,7 @@ public class NotificationActionsPreference extends Preference {
|
|||
public void onDetached() {
|
||||
super.onDetached();
|
||||
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.subscription.SubscriptionFragment;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.PlayerService;
|
||||
import org.schabi.newpipe.player.PlayerService.PlayerType;
|
||||
import org.schabi.newpipe.player.PlayQueueActivity;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
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.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal());
|
||||
intent.putExtra(Player.PLAYER_TYPE, PlayerService.PlayerType.MAIN.ordinal());
|
||||
intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
|
||||
|
||||
return intent;
|
||||
|
@ -163,8 +163,8 @@ public final class NavigationHelper {
|
|||
|
||||
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
||||
|
||||
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
|
||||
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.POPUP.ordinal());
|
||||
final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback);
|
||||
intent.putExtra(Player.PLAYER_TYPE, PlayerService.PlayerType.POPUP.ordinal());
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
}
|
||||
|
||||
|
@ -174,8 +174,8 @@ public final class NavigationHelper {
|
|||
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
|
||||
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
|
||||
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal());
|
||||
final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback);
|
||||
intent.putExtra(Player.PLAYER_TYPE, PlayerService.PlayerType.AUDIO.ordinal());
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
}
|
||||
|
||||
|
@ -184,7 +184,7 @@ public final class NavigationHelper {
|
|||
final PlayQueue queue,
|
||||
final PlayerType playerType) {
|
||||
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());
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
|
@ -194,7 +194,7 @@ public final class NavigationHelper {
|
|||
PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
if (!PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
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);
|
||||
|
@ -205,10 +205,10 @@ public final class NavigationHelper {
|
|||
PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
if (!PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
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();
|
||||
final Intent intent = getPlayerEnqueueNextIntent(context, MainPlayer.class, queue);
|
||||
final Intent intent = getPlayerEnqueueNextIntent(context, PlayerService.class, queue);
|
||||
|
||||
intent.putExtra(Player.PLAYER_TYPE, playerType.ordinal());
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
|
@ -414,14 +414,14 @@ public final class NavigationHelper {
|
|||
final boolean switchingPlayers) {
|
||||
|
||||
final boolean autoPlay;
|
||||
@Nullable final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
@Nullable final PlayerService.PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
if (!PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
// no player open
|
||||
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
|
||||
} else if (switchingPlayers) {
|
||||
// switching player to main player
|
||||
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
|
||||
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
|
||||
} else {
|
||||
|
@ -436,7 +436,7 @@ public final class NavigationHelper {
|
|||
// Situation when user switches from players to main player. All needed data is
|
||||
// here, we can start watching (assuming newQueue equals playQueue).
|
||||
// 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));
|
||||
} else {
|
||||
detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue);
|
||||
|
|
|
@ -12,8 +12,8 @@ import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.START
|
|||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.player.event.DisplayPortion
|
||||
import org.schabi.newpipe.player.event.DoubleTapListener
|
||||
import org.schabi.newpipe.player.gesture.DisplayPortion
|
||||
import org.schabi.newpipe.player.gesture.DoubleTapListener
|
||||
|
||||
class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
||||
ConstraintLayout(context, attrs), DoubleTapListener {
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
android:layout_gravity="center_horizontal"
|
||||
app:behavior_hideable="true"
|
||||
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>
|
||||
|
||||
|
|
Loading…
Reference in a new issue