mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-11-28 21:10:31 +01:00
Merge pull request #8170 from Stypox/player-refactor
Refactor player and extract UI components
This commit is contained in:
commit
b7a44560f5
@ -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.PlayerType;
|
||||
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 PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
return playerType == null || playerType == PlayerType.MAIN;
|
||||
}
|
||||
|
||||
private void openAddToPlaylistDialog() {
|
||||
|
@ -1,5 +1,16 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
||||
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
@ -77,9 +88,9 @@ 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.Player;
|
||||
import org.schabi.newpipe.player.PlayerService;
|
||||
import org.schabi.newpipe.player.PlayerType;
|
||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
@ -87,6 +98,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 +119,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;
|
||||
@ -114,17 +128,6 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
||||
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||
|
||||
public final class VideoDetailFragment
|
||||
extends BaseStateFragment<StreamInfo>
|
||||
implements BackPressable,
|
||||
@ -202,7 +205,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 +214,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 +222,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 +231,19 @@ 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();
|
||||
}
|
||||
|
||||
if (playerIsNotStopped() && player.videoPlayerSelected()) {
|
||||
addVideoPlayerView();
|
||||
playerUi.ifPresent(MainPlayerUi::toggleFullscreen);
|
||||
}
|
||||
|
||||
//noinspection SimplifyOptionalCallChains
|
||||
if (playAfterConnect
|
||||
|| (currentInfo != null
|
||||
&& isAutoplayEnabled()
|
||||
&& player.getParentActivity() == null)) {
|
||||
&& !playerUi.isPresent())) {
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
openVideoPlayerAutoFullscreen();
|
||||
}
|
||||
@ -329,6 +330,9 @@ public final class VideoDetailFragment
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onResume() called");
|
||||
}
|
||||
|
||||
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
|
||||
|
||||
@ -518,7 +522,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 +587,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 +750,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 +762,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 +1012,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 +1092,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 +1223,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 +1238,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 +1247,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();
|
||||
}
|
||||
@ -1305,23 +1308,23 @@ public final class VideoDetailFragment
|
||||
if (!isPlayerAvailable() || getView() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if viewHolder already contains a child
|
||||
if (player.getRootView().getParent() != binding.playerPlaceholder) {
|
||||
playerService.removeViewFromParent();
|
||||
}
|
||||
setHeightThumbnail();
|
||||
|
||||
// Prevent from re-adding a view multiple times
|
||||
if (player.getRootView().getParent() == null) {
|
||||
binding.playerPlaceholder.addView(player.getRootView());
|
||||
}
|
||||
new Handler(Looper.getMainLooper()).post(() ->
|
||||
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||
playerUi.removeViewFromParent();
|
||||
binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
|
||||
playerUi.setupVideoSurfaceIfNeeded();
|
||||
}));
|
||||
}
|
||||
|
||||
private void removeVideoPlayerView() {
|
||||
makeDefaultHeightForVideoPlaceholder();
|
||||
|
||||
playerService.removeViewFromParent();
|
||||
if (player != null) {
|
||||
player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
|
||||
}
|
||||
}
|
||||
|
||||
private void makeDefaultHeightForVideoPlaceholder() {
|
||||
@ -1362,7 +1365,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 +1390,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 +1521,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);
|
||||
}
|
||||
@ -1779,6 +1783,11 @@ public final class VideoDetailFragment
|
||||
// Player event listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onViewCreated() {
|
||||
addVideoPlayerView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQueueUpdate(final PlayQueue queue) {
|
||||
playQueue = queue;
|
||||
@ -1899,15 +1908,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;
|
||||
}
|
||||
|
||||
@ -1935,7 +1939,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;
|
||||
}
|
||||
|
||||
@ -2018,7 +2022,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);
|
||||
}
|
||||
@ -2027,13 +2031,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();
|
||||
}
|
||||
@ -2056,10 +2064,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 {
|
||||
@ -2083,7 +2088,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();
|
||||
@ -2310,10 +2315,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;
|
||||
@ -2326,17 +2331,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;
|
||||
}
|
||||
@ -2410,4 +2420,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.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.PlayerType;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
@ -9,15 +9,20 @@ import android.view.Window;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Queue;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
@ -131,13 +136,13 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
||||
* @param context context used for accessing the database
|
||||
* @param streamEntities used for crating the dialog
|
||||
* @param onExec execution that should occur after a dialog got created, e.g. showing it
|
||||
* @return Disposable
|
||||
* @return the disposable that was created
|
||||
*/
|
||||
public static Disposable createCorrespondingDialog(
|
||||
final Context context,
|
||||
final List<StreamEntity> streamEntities,
|
||||
final Consumer<PlaylistDialog> onExec
|
||||
) {
|
||||
final Consumer<PlaylistDialog> onExec) {
|
||||
|
||||
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
|
||||
.hasPlaylists()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@ -147,4 +152,30 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
||||
: PlaylistCreationDialog.newInstance(streamEntities))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link PlaylistAppendDialog} when playlists exists,
|
||||
* otherwise a {@link PlaylistCreationDialog}. If the player's play queue is null or empty, no
|
||||
* dialog will be created.
|
||||
*
|
||||
* @param player the player from which to extract the context and the play queue
|
||||
* @param fragmentManager the fragment manager to use to show the dialog
|
||||
* @return the disposable that was created
|
||||
*/
|
||||
public static Disposable showForPlayQueue(
|
||||
final Player player,
|
||||
@NonNull final FragmentManager fragmentManager) {
|
||||
|
||||
final List<StreamEntity> streamEntities = Stream.of(player.getPlayQueue())
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(playQueue -> playQueue.getStreams().stream())
|
||||
.map(StreamEntity::new)
|
||||
.collect(Collectors.toList());
|
||||
if (streamEntities.isEmpty()) {
|
||||
return Disposable.empty();
|
||||
}
|
||||
|
||||
return PlaylistDialog.createCorrespondingDialog(player.getContext(), streamEntities,
|
||||
dialog -> dialog.show(fragmentManager, "PlaylistDialog"));
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,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.PlayerType;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
@ -1,259 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
||||
* Part of NewPipe
|
||||
*
|
||||
* License: GPL-3.0+
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
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.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";
|
||||
private static final boolean DEBUG = Player.DEBUG;
|
||||
|
||||
private Player player;
|
||||
private WindowManager windowManager;
|
||||
|
||||
private final IBinder mBinder = new MainPlayer.LocalBinder();
|
||||
|
||||
public enum PlayerType {
|
||||
VIDEO,
|
||||
AUDIO,
|
||||
POPUP
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Notification
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
static final String ACTION_CLOSE
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
|
||||
static final String ACTION_PLAY_PAUSE
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
|
||||
static final String ACTION_REPEAT
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
|
||||
static final String ACTION_PLAY_NEXT
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_NEXT";
|
||||
static final String ACTION_PLAY_PREVIOUS
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
|
||||
static final String ACTION_FAST_REWIND
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_REWIND";
|
||||
static final String ACTION_FAST_FORWARD
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD";
|
||||
static final String ACTION_SHUFFLE
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_SHUFFLE";
|
||||
public static final String ACTION_RECREATE_NOTIFICATION
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
if (DEBUG) {
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
|
||||
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
}
|
||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
||||
&& player.getPlayQueue() == null) {
|
||||
// Player is not working, no need to process media button's action
|
||||
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);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
public void stopForImmediateReusing() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "stopForImmediateReusing() called");
|
||||
}
|
||||
|
||||
if (!player.exoPlayerIsNull()) {
|
||||
player.saveWasPlaying();
|
||||
|
||||
// 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();
|
||||
|
||||
// Notification shows information about old stream but if a user selects
|
||||
// a stream from backStack it's not actual anymore
|
||||
// So we should hide the notification at all.
|
||||
// When autoplay enabled such notification flashing is annoying so skip this case
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskRemoved(final Intent rootIntent) {
|
||||
super.onTaskRemoved(rootIntent);
|
||||
if (!player.videoPlayerSelected()) {
|
||||
return;
|
||||
}
|
||||
onDestroy();
|
||||
// Unload from memory completely
|
||||
Runtime.getRuntime().halt(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "destroy() called");
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public void stopService() {
|
||||
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
|
||||
cleanup();
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(final Context base) {
|
||||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(final Intent intent) {
|
||||
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 Player getPlayer() {
|
||||
return MainPlayer.this.player;
|
||||
}
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
@ -51,7 +52,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
|
||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||
|
||||
protected Player player;
|
||||
private Player player;
|
||||
|
||||
private boolean serviceBound;
|
||||
private ServiceConnection serviceConnection;
|
||||
@ -126,13 +127,13 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
NavigationHelper.openSettings(this);
|
||||
return true;
|
||||
case R.id.action_append_playlist:
|
||||
player.onAddToPlaylistClicked(getSupportFragmentManager());
|
||||
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
|
||||
return true;
|
||||
case R.id.action_playback_speed:
|
||||
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 +169,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 +185,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);
|
||||
}
|
||||
@ -208,17 +206,15 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
public void onServiceConnected(final ComponentName name, final IBinder service) {
|
||||
Log.d(TAG, "Player service is connected");
|
||||
|
||||
if (service instanceof PlayerServiceBinder) {
|
||||
player = ((PlayerServiceBinder) service).getPlayerInstance();
|
||||
} else if (service instanceof MainPlayer.LocalBinder) {
|
||||
player = ((MainPlayer.LocalBinder) service).getPlayer();
|
||||
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 +237,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 +244,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
|
||||
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||
itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue);
|
||||
|
||||
player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener());
|
||||
}
|
||||
|
||||
private void buildMetadata() {
|
||||
@ -370,7 +363,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 +375,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 +438,14 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onQueueUpdate(final PlayQueue queue) {
|
||||
public void onQueueUpdate(@Nullable final PlayQueue queue) {
|
||||
if (queue == null) {
|
||||
queueControlBinding.playQueue.setAdapter(null);
|
||||
} else {
|
||||
final PlayQueueAdapter adapter = new PlayQueueAdapter(this, queue);
|
||||
adapter.setSelectedListener(getOnSelectedListener());
|
||||
queueControlBinding.playQueue.setAdapter(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -454,7 +454,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
onStateChanged(state);
|
||||
onPlayModeChanged(repeatMode, shuffled);
|
||||
onPlaybackParameterChanged(parameters);
|
||||
onMaybePlaybackAdapterChanged();
|
||||
onMaybeMuteChanged();
|
||||
}
|
||||
|
||||
@ -582,17 +581,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
149
app/src/main/java/org/schabi/newpipe/player/PlayerService.java
Normal file
149
app/src/main/java/org/schabi/newpipe/player/PlayerService.java
Normal file
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
||||
* Part of NewPipe
|
||||
*
|
||||
* License: GPL-3.0+
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
|
||||
/**
|
||||
* One service for all players.
|
||||
*/
|
||||
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 final IBinder mBinder = new PlayerService.LocalBinder();
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreate() called");
|
||||
}
|
||||
assureCorrectAppLanguage(this);
|
||||
ThemeHelper.setTheme(this);
|
||||
|
||||
player = new Player(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
|
||||
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
}
|
||||
|
||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
||||
&& player.getPlayQueue() == null) {
|
||||
// No need to process media button's actions if the player is not working, otherwise the
|
||||
// player service would strangely start with nothing to play
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
player.handleIntent(intent);
|
||||
if (player.getMediaSessionManager() != null) {
|
||||
player.getMediaSessionManager().handleMediaButtonIntent(intent);
|
||||
}
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
public void stopForImmediateReusing() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "stopForImmediateReusing() called");
|
||||
}
|
||||
|
||||
if (!player.exoPlayerIsNull()) {
|
||||
player.saveWasPlaying();
|
||||
|
||||
// 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.smoothStopForImmediateReusing();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskRemoved(final Intent rootIntent) {
|
||||
super.onTaskRemoved(rootIntent);
|
||||
if (!player.videoPlayerSelected()) {
|
||||
return;
|
||||
}
|
||||
onDestroy();
|
||||
// Unload from memory completely
|
||||
Runtime.getRuntime().halt(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "destroy() called");
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
if (player != null) {
|
||||
player.destroy();
|
||||
player = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void stopService() {
|
||||
cleanup();
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(final Context base) {
|
||||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(final Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
public class LocalBinder extends Binder {
|
||||
|
||||
public PlayerService getService() {
|
||||
return PlayerService.this;
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return PlayerService.this.player;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.os.Binder;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
class PlayerServiceBinder extends Binder {
|
||||
private final Player player;
|
||||
|
||||
PlayerServiceBinder(@NonNull final Player player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
Player getPlayerInstance() {
|
||||
return player;
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class PlayerState implements Serializable {
|
||||
|
||||
@NonNull
|
||||
private final PlayQueue playQueue;
|
||||
private final int repeatMode;
|
||||
private final float playbackSpeed;
|
||||
private final float playbackPitch;
|
||||
@Nullable
|
||||
private final String playbackQuality;
|
||||
private final boolean playbackSkipSilence;
|
||||
private final boolean wasPlaying;
|
||||
|
||||
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
|
||||
final float playbackSpeed, final float playbackPitch,
|
||||
final boolean playbackSkipSilence, final boolean wasPlaying) {
|
||||
this(playQueue, repeatMode, playbackSpeed, playbackPitch, null,
|
||||
playbackSkipSilence, wasPlaying);
|
||||
}
|
||||
|
||||
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
|
||||
final float playbackSpeed, final float playbackPitch,
|
||||
@Nullable final String playbackQuality, final boolean playbackSkipSilence,
|
||||
final boolean wasPlaying) {
|
||||
this.playQueue = playQueue;
|
||||
this.repeatMode = repeatMode;
|
||||
this.playbackSpeed = playbackSpeed;
|
||||
this.playbackPitch = playbackPitch;
|
||||
this.playbackQuality = playbackQuality;
|
||||
this.playbackSkipSilence = playbackSkipSilence;
|
||||
this.wasPlaying = wasPlaying;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Serdes
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@NonNull
|
||||
public PlayQueue getPlayQueue() {
|
||||
return playQueue;
|
||||
}
|
||||
|
||||
public int getRepeatMode() {
|
||||
return repeatMode;
|
||||
}
|
||||
|
||||
public float getPlaybackSpeed() {
|
||||
return playbackSpeed;
|
||||
}
|
||||
|
||||
public float getPlaybackPitch() {
|
||||
return playbackPitch;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPlaybackQuality() {
|
||||
return playbackQuality;
|
||||
}
|
||||
|
||||
public boolean isPlaybackSkipSilence() {
|
||||
return playbackSkipSilence;
|
||||
}
|
||||
|
||||
public boolean wasPlaying() {
|
||||
return wasPlaying;
|
||||
}
|
||||
}
|
32
app/src/main/java/org/schabi/newpipe/player/PlayerType.java
Normal file
32
app/src/main/java/org/schabi/newpipe/player/PlayerType.java
Normal file
@ -0,0 +1,32 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
public enum PlayerType {
|
||||
MAIN,
|
||||
AUDIO,
|
||||
POPUP;
|
||||
|
||||
/**
|
||||
* @return an integer representing this {@link PlayerType}, to be used to save it in intents
|
||||
* @see #retrieveFromIntent(Intent) Use retrieveFromIntent() to retrieve and convert player type
|
||||
* integers from an intent
|
||||
*/
|
||||
public int valueForIntent() {
|
||||
return ordinal();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param intent the intent to retrieve a player type from
|
||||
* @return the player type integer retrieved from the intent, converted back into a {@link
|
||||
* PlayerType}, or {@link PlayerType#MAIN} if there is no player type extra in the
|
||||
* intent
|
||||
* @throws ArrayIndexOutOfBoundsException if the intent contains an invalid player type integer
|
||||
* @see #valueForIntent() Use valueForIntent() to obtain valid player type integers
|
||||
*/
|
||||
public static PlayerType retrieveFromIntent(final Intent intent) {
|
||||
return values()[intent.getIntExtra(PLAYER_TYPE, MAIN.valueForIntent())];
|
||||
}
|
||||
}
|
@ -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,7 +0,0 @@
|
||||
package org.schabi.newpipe.player.event
|
||||
|
||||
interface DoubleTapListener {
|
||||
fun onDoubleTapStarted(portion: DisplayPortion) {}
|
||||
fun onDoubleTapProgressDown(portion: DisplayPortion) {}
|
||||
fun onDoubleTapFinished() {}
|
||||
}
|
@ -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,186 @@
|
||||
package org.schabi.newpipe.player.gesture
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
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
|
||||
}
|
||||
|
||||
if (onDownNotDoubleTapping(e)) {
|
||||
return super.onDown(e)
|
||||
}
|
||||
return 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(Looper.getMainLooper())
|
||||
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
|
@ -0,0 +1,7 @@
|
||||
package org.schabi.newpipe.player.gesture
|
||||
|
||||
interface DoubleTapListener {
|
||||
fun onDoubleTapStarted(portion: DisplayPortion)
|
||||
fun onDoubleTapProgressDown(portion: DisplayPortion)
|
||||
fun onDoubleTapFinished()
|
||||
}
|
@ -0,0 +1,234 @@
|
||||
package org.schabi.newpipe.player.gesture
|
||||
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.View.OnTouchListener
|
||||
import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.view.isVisible
|
||||
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.AudioReactor
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper
|
||||
import org.schabi.newpipe.player.ui.MainPlayerUi
|
||||
import org.schabi.newpipe.util.ThemeHelper.getAndroidDimenPx
|
||||
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 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) {
|
||||
val bar: ProgressBar = binding.volumeProgressBar
|
||||
val audioReactor: AudioReactor = player.audioReactor
|
||||
|
||||
// If we just started sliding, change the progress bar to match the system volume
|
||||
if (!binding.volumeRelativeLayout.isVisible) {
|
||||
val volumePercent: Float = audioReactor.volume / audioReactor.maxVolume.toFloat()
|
||||
bar.progress = (volumePercent * bar.max).toInt()
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
binding.volumeProgressBar.incrementProgressBy(distanceY.toInt())
|
||||
|
||||
// Update volume
|
||||
val currentProgressPercent: Float = bar.progress / bar.max.toFloat()
|
||||
val currentVolume = (audioReactor.maxVolume * currentProgressPercent).toInt()
|
||||
audioReactor.volume = currentVolume
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume")
|
||||
}
|
||||
|
||||
// Update player center image
|
||||
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
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// Make sure the correct layout is visible
|
||||
if (!binding.volumeRelativeLayout.isVisible) {
|
||||
binding.volumeRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
|
||||
}
|
||||
binding.brightnessRelativeLayout.isVisible = false
|
||||
}
|
||||
|
||||
private fun onScrollBrightness(distanceY: Float) {
|
||||
val parent: AppCompatActivity = playerUi.parentActivity.orElse(null) ?: return
|
||||
val window = parent.window
|
||||
val layoutParams = window.attributes
|
||||
val bar: ProgressBar = binding.brightnessProgressBar
|
||||
|
||||
// Update progress bar
|
||||
val oldBrightness = layoutParams.screenBrightness
|
||||
bar.progress = (bar.max * max(0f, min(1f, oldBrightness))).toInt()
|
||||
bar.incrementProgressBy(distanceY.toInt())
|
||||
|
||||
// Update brightness
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
// Update player center image
|
||||
binding.brightnessImageView.setImageDrawable(
|
||||
AppCompatResources.getDrawable(
|
||||
player.context,
|
||||
when {
|
||||
currentProgressPercent < 0.25 -> R.drawable.ic_brightness_low
|
||||
currentProgressPercent < 0.75 -> R.drawable.ic_brightness_medium
|
||||
else -> R.drawable.ic_brightness_high
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// Make sure the correct layout is visible
|
||||
if (!binding.brightnessRelativeLayout.isVisible) {
|
||||
binding.brightnessRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
|
||||
}
|
||||
binding.volumeRelativeLayout.isVisible = false
|
||||
}
|
||||
|
||||
override fun onScrollEnd(event: MotionEvent) {
|
||||
super.onScrollEnd(event)
|
||||
if (binding.volumeRelativeLayout.isVisible) {
|
||||
binding.volumeRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
|
||||
}
|
||||
if (binding.brightnessRelativeLayout.isVisible) {
|
||||
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
|
||||
}
|
||||
|
||||
// Calculate heights of status and navigation bars
|
||||
val statusBarHeight = getAndroidDimenPx(player.context, "status_bar_height")
|
||||
val navigationBarHeight = getAndroidDimenPx(player.context, "navigation_bar_height")
|
||||
|
||||
// Do not handle this event if initially it started from status or navigation bars
|
||||
val isTouchingStatusBar = initialEvent.y < statusBarHeight
|
||||
val isTouchingNavigationBar = initialEvent.y > (binding.root.height - navigationBarHeight)
|
||||
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
|
||||
}
|
||||
}
|
@ -0,0 +1,288 @@
|
||||
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.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) {
|
||||
playerUi.savePopupPositionAndSizeToPrefs()
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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,8 +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;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
|
||||
@ -15,14 +13,8 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.accessibility.CaptioningManager;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
@ -49,7 +41,6 @@ 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.Player;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
@ -76,20 +67,6 @@ public final class PlayerHelper {
|
||||
private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
|
||||
private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");
|
||||
|
||||
/**
|
||||
* Maximum opacity allowed for Android 12 and higher to allow touches on other apps when using
|
||||
* NewPipe's popup player.
|
||||
*
|
||||
* <p>
|
||||
* This value is hardcoded instead of being get dynamically with the method linked of the
|
||||
* constant documentation below, because it is not static and popup player layout parameters
|
||||
* are generated with static methods.
|
||||
* </p>
|
||||
*
|
||||
* @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
|
||||
*/
|
||||
private static final float MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER = 0.8f;
|
||||
|
||||
@Retention(SOURCE)
|
||||
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
|
||||
AUTOPLAY_TYPE_NEVER})
|
||||
@ -339,10 +316,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,12 +425,6 @@ public final class PlayerHelper {
|
||||
// Utils used by player
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static MainPlayer.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())];
|
||||
}
|
||||
|
||||
public static boolean isPlaybackResumeEnabled(final Player player) {
|
||||
return player.getPrefs().getBoolean(
|
||||
player.getContext().getString(R.string.enable_watch_history_key), true)
|
||||
@ -528,90 +495,10 @@ public final class PlayerHelper {
|
||||
.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param player {@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 float popupWidth = popupRememberSizeAndPos
|
||||
? player.getPrefs().getFloat(player.getContext().getString(
|
||||
R.string.popup_saved_width_key), defaultSize)
|
||||
: defaultSize;
|
||||
final float popupHeight = getMinimumVideoHeight(popupWidth);
|
||||
|
||||
final WindowManager.LayoutParams popupLayoutParams = new WindowManager.LayoutParams(
|
||||
(int) popupWidth, (int) popupHeight,
|
||||
popupLayoutParamType(),
|
||||
IDLE_WINDOW_FLAGS,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
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);
|
||||
popupLayoutParams.x = popupRememberSizeAndPos
|
||||
? player.getPrefs().getInt(player.getContext().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;
|
||||
|
||||
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)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
||||
public static float getMinimumVideoHeight(final float width) {
|
||||
return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() {
|
||||
final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
|
||||
|
||||
final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
popupLayoutParamType(),
|
||||
flags,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// Setting maximum opacity allowed for touch events to other apps for Android 12 and
|
||||
// higher to prevent non interaction when using other apps with the popup player
|
||||
closeOverlayLayoutParams.alpha = MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER;
|
||||
}
|
||||
|
||||
closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
||||
closeOverlayLayoutParams.softInputMode =
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
||||
return closeOverlayLayoutParams;
|
||||
}
|
||||
|
||||
public static int popupLayoutParamType() {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||
? WindowManager.LayoutParams.TYPE_PHONE
|
||||
: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
}
|
||||
|
||||
public static int retrieveSeekDurationFromPreferences(final Player player) {
|
||||
return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString(
|
||||
player.getContext().getString(R.string.seek_duration_key),
|
||||
|
@ -16,8 +16,9 @@ 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.PlayerType;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
@ -42,17 +43,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,
|
||||
* otherwise `null` if no service running.
|
||||
* Returns the current {@link PlayerType} of the {@link PlayerService} service,
|
||||
* otherwise `null` if no service is running.
|
||||
*
|
||||
* @return Current PlayerType
|
||||
*/
|
||||
@Nullable
|
||||
public MainPlayer.PlayerType getType() {
|
||||
public PlayerType getType() {
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
@ -122,7 +123,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 +131,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 +157,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 +173,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 +212,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)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.player;
|
||||
package org.schabi.newpipe.player.notification;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
@ -7,6 +7,7 @@ import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
@ -20,7 +21,34 @@ import java.util.TreeSet;
|
||||
|
||||
public final class NotificationConstants {
|
||||
|
||||
private NotificationConstants() { }
|
||||
private NotificationConstants() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Intent actions
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static final String ACTION_CLOSE
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
|
||||
public static final String ACTION_PLAY_PAUSE
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
|
||||
public static final String ACTION_REPEAT
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
|
||||
public static final String ACTION_PLAY_NEXT
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_NEXT";
|
||||
public static final String ACTION_PLAY_PREVIOUS
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
|
||||
public static final String ACTION_FAST_REWIND
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_REWIND";
|
||||
public static final String ACTION_FAST_FORWARD
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD";
|
||||
public static final String ACTION_SHUFFLE
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_SHUFFLE";
|
||||
public static final String ACTION_RECREATE_NOTIFICATION
|
||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
|
||||
|
||||
|
||||
|
||||
public static final int NOTHING = 0;
|
@ -0,0 +1,125 @@
|
||||
package org.schabi.newpipe.player.notification;
|
||||
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.ui.PlayerUi;
|
||||
|
||||
public final class NotificationPlayerUi extends PlayerUi {
|
||||
private boolean foregroundNotificationAlreadyCreated = false;
|
||||
private final NotificationUtil notificationUtil;
|
||||
|
||||
public NotificationPlayerUi(@NonNull final Player player) {
|
||||
super(player);
|
||||
notificationUtil = new NotificationUtil(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initPlayer() {
|
||||
super.initPlayer();
|
||||
if (!foregroundNotificationAlreadyCreated) {
|
||||
notificationUtil.createNotificationAndStartForeground();
|
||||
foregroundNotificationAlreadyCreated = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
notificationUtil.cancelNotificationAndStopForeground();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
|
||||
super.onThumbnailLoaded(bitmap);
|
||||
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlocked() {
|
||||
super.onBlocked();
|
||||
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaying() {
|
||||
super.onPlaying();
|
||||
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBuffering() {
|
||||
super.onBuffering();
|
||||
if (notificationUtil.shouldUpdateBufferingSlot()) {
|
||||
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPaused() {
|
||||
super.onPaused();
|
||||
|
||||
// Remove running notification when user does not want minimization to background or popup
|
||||
if (PlayerHelper.getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_NONE
|
||||
&& player.videoPlayerSelected()) {
|
||||
notificationUtil.cancelNotificationAndStopForeground();
|
||||
} else {
|
||||
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPausedSeek() {
|
||||
super.onPausedSeek();
|
||||
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
super.onCompleted();
|
||||
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
|
||||
super.onRepeatModeChanged(repeatMode);
|
||||
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
|
||||
super.onShuffleModeEnabledChanged(shuffleModeEnabled);
|
||||
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBroadcastReceived(final Intent intent) {
|
||||
super.onBroadcastReceived(intent);
|
||||
if (ACTION_RECREATE_NOTIFICATION.equals(intent.getAction())) {
|
||||
notificationUtil.createNotificationIfNeededAndUpdate(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadataChanged(@NonNull final StreamInfo info) {
|
||||
super.onMetadataChanged(info);
|
||||
notificationUtil.createNotificationIfNeededAndUpdate(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayQueueEdited() {
|
||||
super.onPlayQueueEdited();
|
||||
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||
}
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
package org.schabi.newpipe.player;
|
||||
package org.schabi.newpipe.player.notification;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.graphics.Bitmap;
|
||||
@ -19,6 +18,7 @@ import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.List;
|
||||
@ -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.notification.NotificationConstants.ACTION_CLOSE;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
|
||||
|
||||
/**
|
||||
* This is a utility class for player notifications.
|
||||
@ -45,22 +45,16 @@ public final class NotificationUtil {
|
||||
private static final boolean DEBUG = Player.DEBUG;
|
||||
private static final int NOTIFICATION_ID = 123789;
|
||||
|
||||
@Nullable private static NotificationUtil instance = null;
|
||||
|
||||
@NotificationConstants.Action
|
||||
private final int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
|
||||
|
||||
private NotificationManagerCompat notificationManager;
|
||||
private NotificationCompat.Builder notificationBuilder;
|
||||
|
||||
private NotificationUtil() {
|
||||
}
|
||||
private final Player player;
|
||||
|
||||
public static NotificationUtil getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new NotificationUtil();
|
||||
}
|
||||
return instance;
|
||||
public NotificationUtil(final Player player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
|
||||
@ -71,20 +65,18 @@ public final class NotificationUtil {
|
||||
/**
|
||||
* Creates the notification if it does not exist already and recreates it if forceRecreate is
|
||||
* true. Updates the notification with the data in the player.
|
||||
* @param player the player currently open, to take data from
|
||||
* @param forceRecreate whether to force the recreation of the notification even if it already
|
||||
* exists
|
||||
*/
|
||||
synchronized void createNotificationIfNeededAndUpdate(final Player player,
|
||||
final boolean forceRecreate) {
|
||||
public synchronized void createNotificationIfNeededAndUpdate(final boolean forceRecreate) {
|
||||
if (forceRecreate || notificationBuilder == null) {
|
||||
notificationBuilder = createNotification(player);
|
||||
notificationBuilder = createNotification();
|
||||
}
|
||||
updateNotification(player);
|
||||
updateNotification();
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||
}
|
||||
|
||||
private synchronized NotificationCompat.Builder createNotification(final Player player) {
|
||||
private synchronized NotificationCompat.Builder createNotification() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "createNotification()");
|
||||
}
|
||||
@ -93,7 +85,7 @@ public final class NotificationUtil {
|
||||
new NotificationCompat.Builder(player.getContext(),
|
||||
player.getContext().getString(R.string.notification_channel_id));
|
||||
|
||||
initializeNotificationSlots(player);
|
||||
initializeNotificationSlots();
|
||||
|
||||
// count the number of real slots, to make sure compact slots indices are not out of bound
|
||||
int nonNothingSlotCount = 5;
|
||||
@ -132,30 +124,29 @@ public final class NotificationUtil {
|
||||
|
||||
/**
|
||||
* Updates the notification builder and the button icons depending on the playback state.
|
||||
* @param player the player currently open, to take data from
|
||||
*/
|
||||
private synchronized void updateNotification(final Player player) {
|
||||
private synchronized void updateNotification() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "updateNotification()");
|
||||
}
|
||||
|
||||
// also update content intent, in case the user switched players
|
||||
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(),
|
||||
NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT));
|
||||
NOTIFICATION_ID, getIntentForNotification(), FLAG_UPDATE_CURRENT));
|
||||
notificationBuilder.setContentTitle(player.getVideoTitle());
|
||||
notificationBuilder.setContentText(player.getUploaderName());
|
||||
notificationBuilder.setTicker(player.getVideoTitle());
|
||||
updateActions(notificationBuilder, player);
|
||||
updateActions(notificationBuilder);
|
||||
final boolean showThumbnail = player.getPrefs().getBoolean(
|
||||
player.getContext().getString(R.string.show_thumbnail_key), true);
|
||||
if (showThumbnail) {
|
||||
setLargeIcon(notificationBuilder, player);
|
||||
setLargeIcon(notificationBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
boolean shouldUpdateBufferingSlot() {
|
||||
public boolean shouldUpdateBufferingSlot() {
|
||||
if (notificationBuilder == null) {
|
||||
// if there is no notification active, there is no point in updating it
|
||||
return false;
|
||||
@ -173,22 +164,22 @@ public final class NotificationUtil {
|
||||
}
|
||||
|
||||
|
||||
void createNotificationAndStartForeground(final Player player, final Service service) {
|
||||
public void createNotificationAndStartForeground() {
|
||||
if (notificationBuilder == null) {
|
||||
notificationBuilder = createNotification(player);
|
||||
notificationBuilder = createNotification();
|
||||
}
|
||||
updateNotification(player);
|
||||
updateNotification();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
service.startForeground(NOTIFICATION_ID, notificationBuilder.build(),
|
||||
player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||
} else {
|
||||
service.startForeground(NOTIFICATION_ID, notificationBuilder.build());
|
||||
player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
void cancelNotificationAndStopForeground(final Service service) {
|
||||
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||
public void cancelNotificationAndStopForeground() {
|
||||
ServiceCompat.stopForeground(player.getService(), ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||
|
||||
if (notificationManager != null) {
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
@ -202,7 +193,7 @@ public final class NotificationUtil {
|
||||
// ACTIONS
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
private void initializeNotificationSlots(final Player player) {
|
||||
private void initializeNotificationSlots() {
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
notificationSlots[i] = player.getPrefs().getInt(
|
||||
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||
@ -211,17 +202,16 @@ public final class NotificationUtil {
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private void updateActions(final NotificationCompat.Builder builder, final Player player) {
|
||||
private void updateActions(final NotificationCompat.Builder builder) {
|
||||
builder.mActions.clear();
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
addAction(builder, player, notificationSlots[i]);
|
||||
addAction(builder, notificationSlots[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void addAction(final NotificationCompat.Builder builder,
|
||||
final Player player,
|
||||
@NotificationConstants.Action final int slot) {
|
||||
final NotificationCompat.Action action = getAction(player, slot);
|
||||
final NotificationCompat.Action action = getAction(slot);
|
||||
if (action != null) {
|
||||
builder.addAction(action);
|
||||
}
|
||||
@ -229,41 +219,40 @@ public final class NotificationUtil {
|
||||
|
||||
@Nullable
|
||||
private NotificationCompat.Action getAction(
|
||||
final Player player,
|
||||
@NotificationConstants.Action final int selectedAction) {
|
||||
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
|
||||
switch (selectedAction) {
|
||||
case NotificationConstants.PREVIOUS:
|
||||
return getAction(player, baseActionIcon,
|
||||
return getAction(baseActionIcon,
|
||||
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
||||
|
||||
case NotificationConstants.NEXT:
|
||||
return getAction(player, baseActionIcon,
|
||||
return getAction(baseActionIcon,
|
||||
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
||||
|
||||
case NotificationConstants.REWIND:
|
||||
return getAction(player, baseActionIcon,
|
||||
return getAction(baseActionIcon,
|
||||
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
||||
|
||||
case NotificationConstants.FORWARD:
|
||||
return getAction(player, baseActionIcon,
|
||||
return getAction(baseActionIcon,
|
||||
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
||||
|
||||
case NotificationConstants.SMART_REWIND_PREVIOUS:
|
||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||
return getAction(player, R.drawable.exo_notification_previous,
|
||||
return getAction(R.drawable.exo_notification_previous,
|
||||
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
||||
} else {
|
||||
return getAction(player, R.drawable.exo_controls_rewind,
|
||||
return getAction(R.drawable.exo_controls_rewind,
|
||||
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
||||
}
|
||||
|
||||
case NotificationConstants.SMART_FORWARD_NEXT:
|
||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||
return getAction(player, R.drawable.exo_notification_next,
|
||||
return getAction(R.drawable.exo_notification_next,
|
||||
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
||||
} else {
|
||||
return getAction(player, R.drawable.exo_controls_fastforward,
|
||||
return getAction(R.drawable.exo_controls_fastforward,
|
||||
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
||||
}
|
||||
|
||||
@ -277,44 +266,45 @@ public final class NotificationUtil {
|
||||
null);
|
||||
}
|
||||
|
||||
// fallthrough
|
||||
case NotificationConstants.PLAY_PAUSE:
|
||||
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
||||
return getAction(player, R.drawable.ic_replay,
|
||||
return getAction(R.drawable.ic_replay,
|
||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||
} else if (player.isPlaying()
|
||||
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|
||||
|| player.getCurrentState() == Player.STATE_BLOCKED
|
||||
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
||||
return getAction(player, R.drawable.exo_notification_pause,
|
||||
return getAction(R.drawable.exo_notification_pause,
|
||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||
} else {
|
||||
return getAction(player, R.drawable.exo_notification_play,
|
||||
return getAction(R.drawable.exo_notification_play,
|
||||
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
|
||||
}
|
||||
|
||||
case NotificationConstants.REPEAT:
|
||||
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
|
||||
return getAction(player, R.drawable.exo_media_action_repeat_all,
|
||||
return getAction(R.drawable.exo_media_action_repeat_all,
|
||||
R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
|
||||
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
|
||||
return getAction(player, R.drawable.exo_media_action_repeat_one,
|
||||
return getAction(R.drawable.exo_media_action_repeat_one,
|
||||
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
|
||||
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
|
||||
return getAction(player, R.drawable.exo_media_action_repeat_off,
|
||||
return getAction(R.drawable.exo_media_action_repeat_off,
|
||||
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
|
||||
}
|
||||
|
||||
case NotificationConstants.SHUFFLE:
|
||||
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
|
||||
return getAction(player, R.drawable.exo_controls_shuffle_on,
|
||||
return getAction(R.drawable.exo_controls_shuffle_on,
|
||||
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
|
||||
} else {
|
||||
return getAction(player, R.drawable.exo_controls_shuffle_off,
|
||||
return getAction(R.drawable.exo_controls_shuffle_off,
|
||||
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
|
||||
}
|
||||
|
||||
case NotificationConstants.CLOSE:
|
||||
return getAction(player, R.drawable.ic_close,
|
||||
return getAction(R.drawable.ic_close,
|
||||
R.string.close, ACTION_CLOSE);
|
||||
|
||||
case NotificationConstants.NOTHING:
|
||||
@ -324,8 +314,7 @@ public final class NotificationUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationCompat.Action getAction(final Player player,
|
||||
@DrawableRes final int drawable,
|
||||
private NotificationCompat.Action getAction(@DrawableRes final int drawable,
|
||||
@StringRes final int title,
|
||||
final String intentAction) {
|
||||
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
|
||||
@ -333,7 +322,7 @@ public final class NotificationUtil {
|
||||
new Intent(intentAction), FLAG_UPDATE_CURRENT));
|
||||
}
|
||||
|
||||
private Intent getIntentForNotification(final Player player) {
|
||||
private Intent getIntentForNotification() {
|
||||
if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
|
||||
// Means we play in popup or audio only. Let's show the play queue
|
||||
return NavigationHelper.getPlayQueueActivityIntent(player.getContext());
|
||||
@ -353,7 +342,7 @@ public final class NotificationUtil {
|
||||
// BITMAP
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
private void setLargeIcon(final NotificationCompat.Builder builder, final Player player) {
|
||||
private void setLargeIcon(final NotificationCompat.Builder builder) {
|
||||
final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
|
||||
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
|
||||
false);
|
@ -8,6 +8,7 @@ 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;
|
||||
|
||||
public class PlayerMediaSession implements MediaSessionCallback {
|
||||
private final Player player;
|
||||
@ -89,7 +90,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
|
||||
|
979
app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
Normal file
979
app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
Normal file
@ -0,0 +1,979 @@
|
||||
package org.schabi.newpipe.player.ui;
|
||||
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
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.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 static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.database.ContentObserver;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
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.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.fragment.app.FragmentActivity;
|
||||
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.local.dialog.PlaylistDialog;
|
||||
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;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutChangeListener {
|
||||
private static final String TAG = MainPlayerUi.class.getSimpleName();
|
||||
|
||||
// see the Javadoc of calculateMaxEndScreenThumbnailHeight for information
|
||||
private static final int DETAIL_ROOT_MINIMUM_HEIGHT = 85; // dp
|
||||
private static final int DETAIL_TITLE_TEXT_SIZE_TV = 16; // sp
|
||||
private static final int DETAIL_TITLE_TEXT_SIZE_TABLET = 15; // sp
|
||||
|
||||
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;
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Constructor, setup, destroy
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Constructor, setup, destroy
|
||||
|
||||
public MainPlayerUi(@NonNull final Player player,
|
||||
@NonNull final PlayerBinding playerBinding) {
|
||||
super(player, playerBinding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open fullscreen on tablets where the option to have the main player start automatically in
|
||||
* fullscreen mode is on. Rotating the device to landscape is already done in {@link
|
||||
* VideoDetailFragment#openVideoPlayer(boolean)} when the thumbnail is clicked, and that's
|
||||
* enough for phones, but not for tablets since the mini player can be also shown in landscape.
|
||||
*/
|
||||
private void directlyOpenFullscreenIfNeeded() {
|
||||
if (PlayerHelper.isStartMainPlayerFullscreenEnabled(player.getService())
|
||||
&& DeviceUtils.isTablet(player.getService())
|
||||
&& PlayerHelper.globalScreenOrientationLocked(player.getService())) {
|
||||
player.getFragmentListener().ifPresent(
|
||||
PlayerServiceEventListener::onScreenRotationButtonClicked);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupAfterIntent() {
|
||||
// needed for tablets, check the function for a better explanation
|
||||
directlyOpenFullscreenIfNeeded();
|
||||
|
||||
super.setupAfterIntent();
|
||||
|
||||
initVideoPlayer();
|
||||
// Android TV: without it focus will frame the whole player
|
||||
binding.playPauseButton.requestFocus();
|
||||
|
||||
// Note: This is for automatically playing (when "Resume playback" is off), see #6179
|
||||
if (player.getPlayWhenReady()) {
|
||||
player.play();
|
||||
} else {
|
||||
player.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
BasePlayerGestureListener buildGestureListener() {
|
||||
return new MainPlayerGestureListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
binding.queueButton.setOnClickListener(v -> onQueueClicked());
|
||||
binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked());
|
||||
|
||||
binding.addToPlaylistButton.setOnClickListener(v ->
|
||||
getParentActivity().map(FragmentActivity::getSupportFragmentManager)
|
||||
.ifPresent(fragmentManager ->
|
||||
PlaylistDialog.showForPlayQueue(player, fragmentManager)));
|
||||
|
||||
settingsContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
|
||||
@Override
|
||||
public void onChange(final boolean selfChange) {
|
||||
setupScreenRotationButton();
|
||||
}
|
||||
};
|
||||
context.getContentResolver().registerContentObserver(
|
||||
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
|
||||
settingsContentObserver);
|
||||
|
||||
binding.getRoot().addOnLayoutChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deinitListeners() {
|
||||
super.deinitListeners();
|
||||
|
||||
binding.queueButton.setOnClickListener(null);
|
||||
binding.segmentsButton.setOnClickListener(null);
|
||||
binding.addToPlaylistButton.setOnClickListener(null);
|
||||
|
||||
context.getContentResolver().unregisterContentObserver(settingsContentObserver);
|
||||
|
||||
binding.getRoot().removeOnLayoutChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initPlayback() {
|
||||
super.initPlayback();
|
||||
|
||||
if (playQueueAdapter != null) {
|
||||
playQueueAdapter.dispose();
|
||||
}
|
||||
playQueueAdapter = new PlayQueueAdapter(context,
|
||||
Objects.requireNonNull(player.getPlayQueue()));
|
||||
segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeViewFromParent() {
|
||||
// view was added to fragment
|
||||
final ViewParent parent = binding.getRoot().getParent();
|
||||
if (parent instanceof ViewGroup) {
|
||||
((ViewGroup) parent).removeView(binding.getRoot());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
|
||||
// Exit from fullscreen when user closes the player via notification
|
||||
if (isFullscreen) {
|
||||
toggleFullscreen();
|
||||
}
|
||||
|
||||
removeViewFromParent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyPlayer() {
|
||||
super.destroyPlayer();
|
||||
|
||||
if (playQueueAdapter != null) {
|
||||
playQueueAdapter.unsetSelectedListener();
|
||||
playQueueAdapter.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void smoothStopForImmediateReusing() {
|
||||
super.smoothStopForImmediateReusing();
|
||||
// Android TV will handle back button in case controls will be visible
|
||||
// (one more additional unneeded click while the player is hidden)
|
||||
hideControls(0, 0);
|
||||
closeItemsList();
|
||||
}
|
||||
|
||||
private void initVideoPlayer() {
|
||||
// restore last resize mode
|
||||
setResizeMode(PlayerHelper.retrieveResizeModeFromPrefs(player));
|
||||
binding.getRoot().setLayoutParams(new FrameLayout.LayoutParams(MATCH_PARENT, 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 = 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);
|
||||
|
||||
binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||
binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : 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)
|
||||
);
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// 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:
|
||||
getParentActivity().ifPresent(activity -> {
|
||||
player.setRecovery();
|
||||
NavigationHelper.playOnPopupPlayer(activity, player.getPlayQueue(), true);
|
||||
});
|
||||
break;
|
||||
case MINIMIZE_ON_EXIT_MODE_NONE: default:
|
||||
player.pause();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback states
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Playback states
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaying() {
|
||||
super.onPlaying();
|
||||
checkLandscape();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
super.onCompleted();
|
||||
if (isFullscreen) {
|
||||
toggleFullscreen();
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Controls showing / hiding
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Controls showing / hiding
|
||||
|
||||
@Override
|
||||
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) {
|
||||
getParentActivity().map(Activity::getWindow).ifPresent(window -> {
|
||||
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
|
||||
* {@link #DETAIL_ROOT_MINIMUM_HEIGHT} free space for {@link R.id.detail_root} and
|
||||
* additional space for the stream title text size ({@link R.id.detail_title_root_layout}).
|
||||
* The text size is {@link #DETAIL_TITLE_TEXT_SIZE_TABLET} on tablets and
|
||||
* {@link #DETAIL_TITLE_TEXT_SIZE_TV} on TVs, see {@link R.id.titleTextView}.
|
||||
* </li>
|
||||
* <li>
|
||||
* Otherwise, the max thumbnail height is the screen height.
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* @param bitmap the bitmap that needs to be resized to fit the end screen
|
||||
* @return the maximum height for the end screen thumbnail
|
||||
*/
|
||||
@Override
|
||||
protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap) {
|
||||
final int screenHeight = context.getResources().getDisplayMetrics().heightPixels;
|
||||
|
||||
if (DeviceUtils.isTv(context) && !isFullscreen()) {
|
||||
final int videoInfoHeight = DeviceUtils.dpToPx(DETAIL_ROOT_MINIMUM_HEIGHT, context)
|
||||
+ DeviceUtils.spToPx(DETAIL_TITLE_TEXT_SIZE_TV, context);
|
||||
return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight);
|
||||
} else if (DeviceUtils.isTablet(context) && isLandscape() && !isFullscreen()) {
|
||||
final int videoInfoHeight = DeviceUtils.dpToPx(DETAIL_ROOT_MINIMUM_HEIGHT, context)
|
||||
+ DeviceUtils.spToPx(DETAIL_TITLE_TEXT_SIZE_TABLET, context);
|
||||
return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight);
|
||||
} else { // fullscreen player: max height is the device height
|
||||
return Math.min(bitmap.getHeight(), screenHeight);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Captions (text tracks)
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Captions (text tracks)
|
||||
|
||||
@Override
|
||||
protected void setupSubtitleView(final 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);
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Gestures
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Gestures
|
||||
|
||||
@SuppressWarnings("checkstyle:ParameterNumber")
|
||||
@Override
|
||||
public void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
|
||||
final int ol, final int ot, final int or, final int ob) {
|
||||
if (l != ol || t != ot || r != or || b != ob) {
|
||||
// Use a smaller value to be consistent across screen orientations, and to make usage
|
||||
// easier. Multiply by 3/4 to ensure the user does not need to move the finger up to the
|
||||
// screen border, in order to reach the maximum volume/brightness.
|
||||
final int width = r - l;
|
||||
final int height = b - t;
|
||||
final int min = Math.min(width, height);
|
||||
final int maxGestureLength = (int) (min * 0.75);
|
||||
|
||||
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.0f));
|
||||
|
||||
// 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().orElse(null);
|
||||
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() {
|
||||
final AppCompatActivity activity = getParentActivity().orElse(null);
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
|
||||
player.getPlaybackSkipSilence(), player::setPlaybackParameters)
|
||||
.show(activity.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, orientation, fullscreen
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Video size, 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) {
|
||||
// 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);
|
||||
} else {
|
||||
// Apply window insets because Android will not do it when orientation changes
|
||||
// from landscape to portrait (open vertical video to reproduce)
|
||||
binding.playbackControlRoot.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
fragmentListener.onFullscreenStateChanged(isFullscreen);
|
||||
|
||||
binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||
binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||
binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : 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 Optional<AppCompatActivity> getParentActivity() {
|
||||
final ViewParent rootParent = binding.getRoot().getParent();
|
||||
if (rootParent instanceof ViewGroup) {
|
||||
final Context activity = ((ViewGroup) rootParent).getContext();
|
||||
if (activity instanceof AppCompatActivity) {
|
||||
return Optional.of((AppCompatActivity) activity);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public boolean isLandscape() {
|
||||
// DisplayMetrics from activity context knows about MultiWindow feature
|
||||
// while DisplayMetrics from app context doesn't
|
||||
return DeviceUtils.isLandscape(
|
||||
getParentActivity().map(Context.class::cast).orElse(player.getService()));
|
||||
}
|
||||
//endregion
|
||||
}
|
211
app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
Normal file
211
app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
Normal file
@ -0,0 +1,211 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* A player UI is a component that can seamlessly connect and disconnect from the {@link Player} and
|
||||
* provide a user interface of some sort. Try to extend this class instead of adding more code to
|
||||
* {@link Player}!
|
||||
*/
|
||||
public abstract class PlayerUi {
|
||||
|
||||
@NonNull protected final Context context;
|
||||
@NonNull protected final Player player;
|
||||
|
||||
/**
|
||||
* @param player the player instance that will be usable throughout the lifetime of this UI
|
||||
*/
|
||||
protected PlayerUi(@NonNull final Player player) {
|
||||
this.context = player.getContext();
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the player instance this UI was constructed with
|
||||
*/
|
||||
@NonNull
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called after the player received an intent and processed it.
|
||||
*/
|
||||
public void setupAfterIntent() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called right after the exoplayer instance is constructed, or right after this UI is
|
||||
* constructed if the exoplayer is already available then. Note that the exoplayer instance
|
||||
* could be built and destroyed multiple times during the lifetime of the player, so this method
|
||||
* might be called multiple times.
|
||||
*/
|
||||
public void initPlayer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when playback in the exoplayer is about to start, or right after this UI is
|
||||
* constructed if the exoplayer and the play queue are already available then. The play queue
|
||||
* will therefore always be not null.
|
||||
*/
|
||||
public void initPlayback() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the exoplayer instance is about to be destroyed. Note that the exoplayer instance
|
||||
* could be built and destroyed multiple times during the lifetime of the player, so this method
|
||||
* might be called multiple times. Be sure to unset any video surface view or play queue
|
||||
* listeners! This will also be called when this UI is being discarded, just before {@link
|
||||
* #destroy()}.
|
||||
*/
|
||||
public void destroyPlayer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this UI is being discarded, either because the player is switching to a different
|
||||
* UI or because the player is shutting down completely.
|
||||
*/
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the player is smooth-stopping, that is, transitioning smoothly to a new play
|
||||
* queue after the user tapped on a new video stream while a stream was playing in the video
|
||||
* detail fragment.
|
||||
*/
|
||||
public void smoothStopForImmediateReusing() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the video detail fragment listener is connected with the player, or right after
|
||||
* this UI is constructed if the listener is already connected then.
|
||||
*/
|
||||
public void onFragmentListenerSet() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts that the player receives will also be notified to UIs here. If you want to
|
||||
* register new broadcast actions to receive here, add them to {@link
|
||||
* Player#setupBroadcastReceiver()}.
|
||||
* @param intent the broadcast intent received by the player
|
||||
*/
|
||||
public void onBroadcastReceived(final Intent intent) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when stream progress (i.e. the current time in the seekbar) or stream duration change.
|
||||
* Will surely be called every {@link Player#PROGRESS_LOOP_INTERVAL_MILLIS} while a stream is
|
||||
* playing.
|
||||
* @param currentProgress the current progress in milliseconds
|
||||
* @param duration the duration of the stream being played
|
||||
* @param bufferPercent the percentage of stream already buffered, see {@link
|
||||
* com.google.android.exoplayer2.BasePlayer#getBufferedPercentage()}
|
||||
*/
|
||||
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) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see com.google.android.exoplayer2.Player.Listener#onTracksChanged(Tracks)
|
||||
* @param currentTracks the available tracks information
|
||||
*/
|
||||
public void onTextTracksChanged(@NonNull final Tracks currentTracks) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see com.google.android.exoplayer2.Player.Listener#onPlaybackParametersChanged
|
||||
* @param playbackParameters the new playback parameters
|
||||
*/
|
||||
public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see com.google.android.exoplayer2.Player.Listener#onRenderedFirstFrame
|
||||
*/
|
||||
public void onRenderedFirstFrame() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see com.google.android.exoplayer2.text.TextOutput#onCues
|
||||
* @param cues the cues to pass to the subtitle view
|
||||
*/
|
||||
public void onCues(@NonNull final List<Cue> cues) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the stream being played changes.
|
||||
* @param info the {@link StreamInfo} metadata object, along with data about the selected and
|
||||
* available video streams (to be used to build the resolution menus, for example)
|
||||
*/
|
||||
public void onMetadataChanged(@NonNull final StreamInfo info) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the thumbnail for the current metadata was loaded.
|
||||
* @param bitmap the thumbnail to process, or null if there is no thumbnail or there was an
|
||||
* error when loading the thumbnail
|
||||
*/
|
||||
public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the play queue was edited: a stream was appended, moved or removed.
|
||||
*/
|
||||
public void onPlayQueueEdited() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param videoSize the new video size, useful to set the surface aspect ratio
|
||||
* @see com.google.android.exoplayer2.Player.Listener#onVideoSizeChanged
|
||||
*/
|
||||
public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
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<>();
|
||||
|
||||
/**
|
||||
* Adds the provided player ui to the list and calls on it the initialization functions that
|
||||
* apply based on the current player state. The preparation step needs to be done since when UIs
|
||||
* are removed and re-added, the player will not call e.g. initPlayer again since the exoplayer
|
||||
* is already initialized, but we need to notify the newly built UI that the player is ready
|
||||
* nonetheless.
|
||||
* @param playerUi the player ui to prepare and add to the list; its {@link
|
||||
* PlayerUi#getPlayer()} will be used to query information about the player
|
||||
* state
|
||||
*/
|
||||
public void addAndPrepare(final PlayerUi playerUi) {
|
||||
if (playerUi.getPlayer().getFragmentListener().isPresent()) {
|
||||
// make sure UIs know whether a service is connected or not
|
||||
playerUi.onFragmentListenerSet();
|
||||
}
|
||||
|
||||
if (!playerUi.getPlayer().exoPlayerIsNull()) {
|
||||
playerUi.initPlayer();
|
||||
if (playerUi.getPlayer().getPlayQueue() != null) {
|
||||
playerUi.initPlayback();
|
||||
}
|
||||
}
|
||||
|
||||
playerUis.add(playerUi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys all matching player UIs and removes them from the list.
|
||||
* @param playerUiType the class of the player UI to destroy; the {@link
|
||||
* Class#isInstance(Object)} method will be used, so even subclasses will be
|
||||
* destroyed and removed
|
||||
* @param <T> the class type parameter
|
||||
*/
|
||||
public <T> void destroyAll(final Class<T> playerUiType) {
|
||||
playerUis.stream()
|
||||
.filter(playerUiType::isInstance)
|
||||
.forEach(playerUi -> {
|
||||
playerUi.destroyPlayer();
|
||||
playerUi.destroy();
|
||||
});
|
||||
playerUis.removeIf(playerUiType::isInstance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param playerUiType the class of the player UI to return; the {@link
|
||||
* Class#isInstance(Object)} method will be used, so even subclasses could
|
||||
* be returned
|
||||
* @param <T> the class type parameter
|
||||
* @return the first player UI of the required type found in the list, or an empty {@link
|
||||
* Optional} otherwise
|
||||
*/
|
||||
public <T> Optional<T> get(final Class<T> playerUiType) {
|
||||
return playerUis.stream()
|
||||
.filter(playerUiType::isInstance)
|
||||
.map(playerUiType::cast)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the provided consumer on all player UIs in the list.
|
||||
* @param consumer the consumer to call with player UIs
|
||||
*/
|
||||
public void call(final Consumer<PlayerUi> consumer) {
|
||||
//noinspection SimplifyStreamApiCallChains
|
||||
playerUis.stream().forEach(consumer);
|
||||
}
|
||||
}
|
@ -0,0 +1,588 @@
|
||||
package org.schabi.newpipe.player.ui;
|
||||
|
||||
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Build;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
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();
|
||||
|
||||
/**
|
||||
* Maximum opacity allowed for Android 12 and higher to allow touches on other apps when using
|
||||
* NewPipe's popup player.
|
||||
*
|
||||
* <p>
|
||||
* This value is hardcoded instead of being get dynamically with the method linked of the
|
||||
* constant documentation below, because it is not static and popup player layout parameters
|
||||
* are generated with static methods.
|
||||
* </p>
|
||||
*
|
||||
* @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
|
||||
*/
|
||||
private static final float MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER = 0.8f;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// 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;
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Constructor, setup, destroy
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Constructor, setup, destroy
|
||||
|
||||
public PopupPlayerUi(@NonNull final Player player,
|
||||
@NonNull final PlayerBinding playerBinding) {
|
||||
super(player, playerBinding);
|
||||
windowManager = ContextCompat.getSystemService(context, WindowManager.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupAfterIntent() {
|
||||
super.setupAfterIntent();
|
||||
initPopup();
|
||||
initPopupCloseOverlay();
|
||||
}
|
||||
|
||||
@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();
|
||||
binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
|
||||
|
||||
checkPopupPositionBounds();
|
||||
|
||||
binding.loadingPanel.setMinimumWidth(popupLayoutParams.width);
|
||||
binding.loadingPanel.setMinimumHeight(popupLayoutParams.height);
|
||||
|
||||
windowManager.addView(binding.getRoot(), popupLayoutParams);
|
||||
setupVideoSurfaceIfNeeded(); // now there is a parent, we can setup video surface
|
||||
|
||||
// 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 = 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();
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// 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 (player.isPlaying() || player.isLoading()) {
|
||||
if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
|
||||
// Use only audio source when screen turns off while popup player is playing
|
||||
player.useVideoSource(false);
|
||||
} else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
|
||||
// Restore video source when screen turns on and user was watching video in popup
|
||||
player.useVideoSource(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Popup position and size
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Popup position and size
|
||||
|
||||
/**
|
||||
* 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 = Math.min((int) Math.max(width, minimumWidth), screenWidth);
|
||||
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);
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Popup closing
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Popup closing
|
||||
|
||||
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() {
|
||||
// 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();
|
||||
}
|
||||
//endregion
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback states
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Playback states
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// 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
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Popup & closing overlay layout params + saving popup position and size
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Popup & closing overlay layout params + saving popup position and size
|
||||
|
||||
/**
|
||||
* {@code screenWidth} and {@code screenHeight} must have been initialized.
|
||||
* @return the popup starting layout params
|
||||
*/
|
||||
@SuppressLint("RtlHardcoded")
|
||||
public WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs() {
|
||||
final SharedPreferences prefs = getPlayer().getPrefs();
|
||||
final Context context = 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
|
||||
? prefs.getFloat(context.getString(R.string.popup_saved_width_key), defaultSize)
|
||||
: defaultSize;
|
||||
final float popupHeight = getMinimumVideoHeight(popupWidth);
|
||||
|
||||
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
|
||||
(int) popupWidth, (int) popupHeight,
|
||||
popupLayoutParamType(),
|
||||
IDLE_WINDOW_FLAGS,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
params.gravity = Gravity.LEFT | Gravity.TOP;
|
||||
params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
||||
|
||||
final int centerX = (int) (screenWidth / 2f - popupWidth / 2f);
|
||||
final int centerY = (int) (screenHeight / 2f - popupHeight / 2f);
|
||||
params.x = popupRememberSizeAndPos
|
||||
? prefs.getInt(context.getString(R.string.popup_saved_x_key), centerX) : centerX;
|
||||
params.y = popupRememberSizeAndPos
|
||||
? prefs.getInt(context.getString(R.string.popup_saved_y_key), centerY) : centerY;
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
public void savePopupPositionAndSizeToPrefs() {
|
||||
if (getPopupLayoutParams() != null) {
|
||||
final Context context = getPlayer().getContext();
|
||||
getPlayer().getPrefs().edit()
|
||||
.putFloat(context.getString(R.string.popup_saved_width_key),
|
||||
popupLayoutParams.width)
|
||||
.putInt(context.getString(R.string.popup_saved_x_key),
|
||||
popupLayoutParams.x)
|
||||
.putInt(context.getString(R.string.popup_saved_y_key),
|
||||
popupLayoutParams.y)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() {
|
||||
final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
|
||||
|
||||
final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
popupLayoutParamType(),
|
||||
flags,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// Setting maximum opacity allowed for touch events to other apps for Android 12 and
|
||||
// higher to prevent non interaction when using other apps with the popup player
|
||||
closeOverlayLayoutParams.alpha = MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER;
|
||||
}
|
||||
|
||||
closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
||||
closeOverlayLayoutParams.softInputMode =
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
||||
return closeOverlayLayoutParams;
|
||||
}
|
||||
|
||||
public static int popupLayoutParamType() {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||
? WindowManager.LayoutParams.TYPE_PHONE
|
||||
: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
//region Getters
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
1591
app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
Normal file
1591
app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.settings.custom;
|
||||
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
@ -23,11 +25,11 @@ import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
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.NotificationConstants;
|
||||
import org.schabi.newpipe.player.notification.NotificationConstants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
@ -61,7 +63,9 @@ public class NotificationActionsPreference extends Preference {
|
||||
public void onDetached() {
|
||||
super.onDetached();
|
||||
saveChanges();
|
||||
getContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION));
|
||||
// set package to this app's package to prevent the intent from being seen outside
|
||||
getContext().sendBroadcast(new Intent(ACTION_RECREATE_NOTIFICATION)
|
||||
.setPackage(App.PACKAGE_NAME));
|
||||
}
|
||||
|
||||
|
||||
|
@ -50,10 +50,10 @@ 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.PlayQueueActivity;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.PlayerType;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
@ -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, PlayerType.MAIN.valueForIntent());
|
||||
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, PlayerType.POPUP.valueForIntent());
|
||||
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, PlayerType.AUDIO.valueForIntent());
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
}
|
||||
|
||||
@ -184,17 +184,17 @@ 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());
|
||||
intent.putExtra(Player.PLAYER_TYPE, playerType.valueForIntent());
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
}
|
||||
|
||||
public static void enqueueOnPlayer(final Context context, final PlayQueue queue) {
|
||||
PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
if (!PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
if (playerType == null) {
|
||||
Log.e(TAG, "Enqueueing but no player is open; defaulting to background player");
|
||||
playerType = MainPlayer.PlayerType.AUDIO;
|
||||
playerType = PlayerType.AUDIO;
|
||||
}
|
||||
|
||||
enqueueOnPlayer(context, queue, playerType);
|
||||
@ -203,14 +203,14 @@ public final class NavigationHelper {
|
||||
/* ENQUEUE NEXT */
|
||||
public static void enqueueNextOnPlayer(final Context context, final PlayQueue queue) {
|
||||
PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
if (!PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
if (playerType == null) {
|
||||
Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player");
|
||||
playerType = MainPlayer.PlayerType.AUDIO;
|
||||
playerType = 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());
|
||||
intent.putExtra(Player.PLAYER_TYPE, playerType.valueForIntent());
|
||||
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();
|
||||
if (!PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
@Nullable final PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
if (playerType == null) {
|
||||
// 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 == 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 == PlayerType.POPUP
|
||||
|| PlayerHelper.isStartMainPlayerFullscreenEnabled(context));
|
||||
} else {
|
||||
detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue);
|
||||
|
@ -244,6 +244,22 @@ public final class ThemeHelper {
|
||||
return AppCompatResources.getDrawable(context, typedValue.resourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a runtime dimen from the {@code android} package. Should be used for dimens for which
|
||||
* normal accessing with {@code R.dimen.} is not available.
|
||||
*
|
||||
* @param context context
|
||||
* @param name dimen resource name (e.g. navigation_bar_height)
|
||||
* @return the obtained dimension, in pixels, or 0 if the resource could not be resolved
|
||||
*/
|
||||
public static int getAndroidDimenPx(@NonNull final Context context, final String name) {
|
||||
final int resId = context.getResources().getIdentifier(name, "dimen", "android");
|
||||
if (resId <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return context.getResources().getDimensionPixelSize(resId);
|
||||
}
|
||||
|
||||
private static String getSelectedThemeKey(final Context context) {
|
||||
final String themeKey = context.getString(R.string.theme_key);
|
||||
final String defaultTheme = context.getResources().getString(R.string.default_theme_value);
|
||||
|
@ -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 {
|
||||
@ -38,14 +38,14 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
|
||||
|
||||
private var performListener: PerformListener? = null
|
||||
|
||||
fun performListener(listener: PerformListener) = apply {
|
||||
fun performListener(listener: PerformListener?) = apply {
|
||||
performListener = listener
|
||||
}
|
||||
|
||||
private var seekSecondsSupplier: () -> Int = { 0 }
|
||||
|
||||
fun seekSecondsSupplier(supplier: () -> Int) = apply {
|
||||
seekSecondsSupplier = supplier
|
||||
fun seekSecondsSupplier(supplier: (() -> Int)?) = apply {
|
||||
seekSecondsSupplier = supplier ?: { 0 }
|
||||
}
|
||||
|
||||
// Indicates whether this (double) tap is the first of a series
|
||||
|
@ -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 New Issue
Block a user