+ * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed and {@code true} is returned
+ * to represent this change.
+ *
+ * @return if the popup was out of bounds and have been moved back to it
+ */
+ public boolean checkPopupPositionBounds(final float boundaryWidth, final float boundaryHeight) {
+ if (DEBUG) {
+ Log.d(TAG, "checkPopupPositionBounds() called with: boundaryWidth = ["
+ + boundaryWidth + "], boundaryHeight = [" + boundaryHeight + "]");
+ }
+
+ if (popupLayoutParams.x < 0) {
+ popupLayoutParams.x = 0;
+ return true;
+ } else if (popupLayoutParams.x > boundaryWidth - popupLayoutParams.width) {
+ popupLayoutParams.x = (int) (boundaryWidth - popupLayoutParams.width);
+ return true;
+ }
+
+ if (popupLayoutParams.y < 0) {
+ popupLayoutParams.y = 0;
+ return true;
+ } else if (popupLayoutParams.y > boundaryHeight - popupLayoutParams.height) {
+ popupLayoutParams.y = (int) (boundaryHeight - popupLayoutParams.height);
+ return true;
+ }
+
+ return false;
+ }
+
+ public void savePositionAndSize() {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(service);
+ sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply();
+ sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply();
+ sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply();
+ }
+
+ private float getMinimumVideoHeight(float width) {
+ //if (DEBUG) Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + width + "], returned: " + height);
+ return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
+ }
+
+ public void updateScreenSize() {
+ 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);
+
+ popupWidth = service.getResources().getDimension(R.dimen.popup_default_width);
+ popupHeight = getMinimumVideoHeight(popupWidth);
+
+ minimumWidth = service.getResources().getDimension(R.dimen.popup_minimum_width);
+ minimumHeight = getMinimumVideoHeight(minimumWidth);
+
+ maximumWidth = screenWidth;
+ maximumHeight = screenHeight;
+ }
+
+ public void updatePopupSize(int width, int height) {
+ if (DEBUG) Log.d(TAG, "updatePopupSize() called with: width = [" + width + "], height = [" + height + "]");
+
+ if (popupLayoutParams == null || windowManager == null || !popupPlayerSelected() || getRootView().getParent() == null)
+ return;
+
+ width = (int) (width > maximumWidth ? maximumWidth : width < minimumWidth ? minimumWidth : width);
+
+ if (height == -1) height = (int) getMinimumVideoHeight(width);
+ else height = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : height);
+
+ popupLayoutParams.width = width;
+ popupLayoutParams.height = height;
+ popupWidth = width;
+ popupHeight = height;
+
+ if (DEBUG) Log.d(TAG, "updatePopupSize() updated values: width = [" + width + "], height = [" + height + "]");
+ windowManager.updateViewLayout(getRootView(), popupLayoutParams);
+ }
+
+ private void updateWindowFlags(final int flags) {
+ if (popupLayoutParams == null || windowManager == null || !popupPlayerSelected() || getRootView().getParent() == null)
+ return;
+
+ popupLayoutParams.flags = flags;
+ windowManager.updateViewLayout(getRootView(), popupLayoutParams);
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Misc
+ //////////////////////////////////////////////////////////////////////////*/
+
+ public void closePopup() {
+ if (DEBUG) Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing);
+ if (isPopupClosing) return;
+ isPopupClosing = true;
+
+ savePlaybackState();
+ windowManager.removeView(getRootView());
+
+ animateOverlayAndFinishService();
+ }
+
+ private void animateOverlayAndFinishService() {
+ final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() - closeOverlayButton.getY());
+
+ closeOverlayButton.animate().setListener(null).cancel();
+ closeOverlayButton.animate()
+ .setInterpolator(new AnticipateInterpolator())
+ .translationY(targetTranslationY)
+ .setDuration(400)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ end();
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ end();
+ }
+
+ private void end() {
+ windowManager.removeView(closeOverlayView);
+
+ service.onDestroy();
+ }
+ }).start();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Manipulations with listener
+ ///////////////////////////////////////////////////////////////////////////
+
+ public void setFragmentListener(PlayerServiceEventListener listener) {
+ fragmentListener = listener;
+ updateMetadata();
+ updatePlayback();
+ triggerProgressUpdate();
+ }
+
+ public void removeFragmentListener(PlayerServiceEventListener listener) {
+ if (fragmentListener == listener) {
+ fragmentListener = null;
+ }
+ }
+
+ /*package-private*/ void setActivityListener(PlayerEventListener listener) {
+ activityListener = listener;
+ updateMetadata();
+ updatePlayback();
+ triggerProgressUpdate();
+ }
+
+ /*package-private*/ void removeActivityListener(PlayerEventListener listener) {
+ if (activityListener == listener) {
+ activityListener = null;
+ }
+ }
+
+ private void updateMetadata() {
+ if (fragmentListener != null && getCurrentMetadata() != null) {
+ fragmentListener.onMetadataUpdate(getCurrentMetadata().getMetadata());
+ }
+ if (activityListener != null && getCurrentMetadata() != null) {
+ activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata());
+ }
+ }
+
+ private void updatePlayback() {
+ if (fragmentListener != null && simpleExoPlayer != null && playQueue != null) {
+ fragmentListener.onPlaybackUpdate(currentState, getRepeatMode(),
+ playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters());
+ }
+ if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
+ activityListener.onPlaybackUpdate(currentState, getRepeatMode(),
+ playQueue.isShuffled(), getPlaybackParameters());
+ }
+ }
+
+ private void updateProgress(int currentProgress, int duration, int bufferPercent) {
+ if (fragmentListener != null) {
+ fragmentListener.onProgressUpdate(currentProgress, duration, bufferPercent);
+ }
+ if (activityListener != null) {
+ activityListener.onProgressUpdate(currentProgress, duration, bufferPercent);
+ }
+ }
+
+ void stopActivityBinding() {
+ if (fragmentListener != null) {
+ fragmentListener.onServiceStopped();
+ fragmentListener = null;
+ }
+ if (activityListener != null) {
+ activityListener.onServiceStopped();
+ activityListener = null;
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Getters
+ ///////////////////////////////////////////////////////////////////////////
+
+ public RelativeLayout getVolumeRelativeLayout() {
+ return volumeRelativeLayout;
+ }
+
+ public ProgressBar getVolumeProgressBar() {
+ return volumeProgressBar;
+ }
+
+ public ImageView getVolumeImageView() {
+ return volumeImageView;
+ }
+
+ public RelativeLayout getBrightnessRelativeLayout() {
+ return brightnessRelativeLayout;
+ }
+
+ public ProgressBar getBrightnessProgressBar() {
+ return brightnessProgressBar;
+ }
+
+ public ImageView getBrightnessImageView() {
+ return brightnessImageView;
+ }
+
+ public ImageButton getPlayPauseButton() {
+ return playPauseButton;
+ }
+
+ public int getMaxGestureLength() {
+ return maxGestureLength;
+ }
+
+ public TextView getResizingIndicator() {
+ return resizingIndicator;
+ }
+
+ public GestureDetector getGestureDetector() {
+ return gestureDetector;
+ }
+
+ public WindowManager.LayoutParams getPopupLayoutParams() {
+ return popupLayoutParams;
+ }
+
+ public float getScreenWidth() {
+ return screenWidth;
+ }
+
+ public float getScreenHeight() {
+ return screenHeight;
+ }
+
+ public float getPopupWidth() {
+ return popupWidth;
+ }
+
+ public float getPopupHeight() {
+ return popupHeight;
+ }
+
+ public void setPopupWidth(float width) {
+ popupWidth = width;
+ }
+
+ public void setPopupHeight(float height) {
+ popupHeight = height;
+ }
+
+ public View getCloseOverlayButton() {
+ return closeOverlayButton;
+ }
+
+ public View getCloseOverlayView() {
+ return closeOverlayView;
+ }
+
+ public View getClosingOverlayView() {
+ return closingOverlayView;
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java
new file mode 100644
index 000000000..85db3b201
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java
@@ -0,0 +1,47 @@
+package org.schabi.newpipe.player.event;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import org.schabi.newpipe.R;
+
+public class CustomBottomSheetBehavior extends BottomSheetBehavior {
+
+ public CustomBottomSheetBehavior(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent event) {
+ // Without overriding scrolling will not work in detail_content_root_layout
+ ViewGroup controls = child.findViewById(R.id.detail_content_root_layout);
+ if (controls != null) {
+ Rect rect = new Rect();
+ controls.getGlobalVisibleRect(rect);
+ if (rect.contains((int) event.getX(), (int) event.getY())) return false;
+ }
+
+ // Without overriding scrolling will not work on relatedStreamsLayout
+ ViewGroup relatedStreamsLayout = child.findViewById(R.id.relatedStreamsLayout);
+ if (relatedStreamsLayout != null) {
+ Rect rect = new Rect();
+ relatedStreamsLayout.getGlobalVisibleRect(rect);
+ if (rect.contains((int) event.getX(), (int) event.getY())) return false;
+ }
+
+ ViewGroup playQueue = child.findViewById(R.id.playQueue);
+ if (playQueue != null) {
+ Rect rect = new Rect();
+ playQueue.getGlobalVisibleRect(rect);
+ if (rect.contains((int) event.getX(), (int) event.getY())) return false;
+ }
+
+ return super.onInterceptTouchEvent(parent, child, event);
+ }
+
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java
new file mode 100644
index 000000000..72462beff
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java
@@ -0,0 +1,462 @@
+package org.schabi.newpipe.player.event;
+
+import android.app.Activity;
+import android.util.Log;
+import android.view.*;
+import androidx.appcompat.content.res.AppCompatResources;
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.player.BasePlayer;
+import org.schabi.newpipe.player.MainPlayer;
+import org.schabi.newpipe.player.VideoPlayerImpl;
+import org.schabi.newpipe.player.helper.PlayerHelper;
+
+import static org.schabi.newpipe.player.BasePlayer.*;
+import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
+import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
+import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA;
+import static org.schabi.newpipe.util.AnimationUtils.animateView;
+
+public class PlayerGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
+ private static final String TAG = ".PlayerGestureListener";
+ private static final boolean DEBUG = BasePlayer.DEBUG;
+
+ private VideoPlayerImpl playerImpl;
+ private MainPlayer service;
+
+ private int initialPopupX, initialPopupY;
+
+ private boolean isMovingInMain, isMovingInPopup;
+
+ private boolean isResizing;
+
+ private int tossFlingVelocity;
+
+ private final boolean isVolumeGestureEnabled;
+ private final boolean isBrightnessGestureEnabled;
+ private final int maxVolume;
+ private static final int MOVEMENT_THRESHOLD = 40;
+
+
+ public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) {
+ this.playerImpl = playerImpl;
+ this.service = service;
+ this.tossFlingVelocity = PlayerHelper.getTossFlingVelocity(service);
+
+ isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service);
+ isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(service);
+ maxVolume = playerImpl.getAudioReactor().getMaxVolume();
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Helpers
+ //////////////////////////////////////////////////////////////////////////*/
+
+ /*
+ * Main and popup players' gesture listeners is too different.
+ * So it will be better to have different implementations of them
+ * */
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
+
+ if (playerImpl.popupPlayerSelected()) return onDoubleTapInPopup(e);
+ else return onDoubleTapInMain(e);
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
+
+ if (playerImpl.popupPlayerSelected()) return onSingleTapConfirmedInPopup(e);
+ else return onSingleTapConfirmedInMain(e);
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]");
+
+ if (playerImpl.popupPlayerSelected()) return onDownInPopup(e);
+ else return true;
+ }
+ @Override
+ public void onLongPress(MotionEvent e) {
+ if (DEBUG) Log.d(TAG, "onLongPress() called with: e = [" + e + "]");
+
+ if (playerImpl.popupPlayerSelected()) onLongPressInPopup(e);
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) {
+ if (playerImpl.popupPlayerSelected()) return onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY);
+ else return onScrollInMain(initialEvent, movingEvent, distanceX, distanceY);
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ if (DEBUG) Log.d(TAG, "onFling() called with velocity: dX=[" + velocityX + "], dY=[" + velocityY + "]");
+
+ if (playerImpl.popupPlayerSelected()) return onFlingInPopup(e1, e2, velocityX, velocityY);
+ else return true;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (DEBUG && false) Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]");
+
+ if (playerImpl.popupPlayerSelected()) return onTouchInPopup(v, event);
+ else return onTouchInMain(v, event);
+ }
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Main player listener
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private boolean onDoubleTapInMain(MotionEvent e) {
+ if (e.getX() > playerImpl.getRootView().getWidth() * 2 / 3) {
+ playerImpl.onFastForward();
+ } else if (e.getX() < playerImpl.getRootView().getWidth() / 3) {
+ playerImpl.onFastRewind();
+ } else {
+ playerImpl.getPlayPauseButton().performClick();
+ }
+
+ return true;
+ }
+
+
+ private boolean onSingleTapConfirmedInMain(MotionEvent e) {
+ if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
+
+ if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) return true;
+
+ if (playerImpl.isControlsVisible()) {
+ playerImpl.hideControls(150, 0);
+ } else {
+ if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) {
+ playerImpl.showControls(0);
+ } else {
+ playerImpl.showControlsThenHide();
+ }
+ if (playerImpl.isInFullscreen()) {
+ int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_FULLSCREEN;
+ playerImpl.getParentActivity().getWindow().getDecorView().setSystemUiVisibility(visibility);
+ playerImpl.getParentActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
+ playerImpl.getParentActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ }
+ return true;
+ }
+
+ private boolean onScrollInMain(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) {
+ if (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) return false;
+
+ //noinspection PointlessBooleanExpression
+ if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " +
+ ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" +
+ ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" +
+ ", distanceXy = [" + distanceX + ", " + distanceY + "]");
+
+ final boolean insideThreshold = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD;
+ if (!isMovingInMain && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY))
+ || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) {
+ return false;
+ }
+
+ isMovingInMain = true;
+
+ boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled;
+ boolean acceptVolumeArea = acceptAnyArea || initialEvent.getX() > playerImpl.getRootView().getWidth() / 2;
+ boolean acceptBrightnessArea = acceptAnyArea || !acceptVolumeArea;
+
+ if (isVolumeGestureEnabled && acceptVolumeArea) {
+ playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY);
+ float currentProgressPercent =
+ (float) playerImpl.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength();
+ int currentVolume = (int) (maxVolume * currentProgressPercent);
+ playerImpl.getAudioReactor().setVolume(currentVolume);
+
+ if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
+
+ final int resId =
+ currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_72dp
+ : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp
+ : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp
+ : R.drawable.ic_volume_up_white_72dp;
+
+ playerImpl.getVolumeImageView().setImageDrawable(
+ AppCompatResources.getDrawable(service, resId)
+ );
+
+ if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
+ animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200);
+ }
+ if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
+ playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE);
+ }
+ } else if (isBrightnessGestureEnabled && acceptBrightnessArea) {
+ Activity parent = playerImpl.getParentActivity();
+ if (parent == null) return true;
+
+ Window window = parent.getWindow();
+
+ playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY);
+ float currentProgressPercent =
+ (float) playerImpl.getBrightnessProgressBar().getProgress() / playerImpl.getMaxGestureLength();
+ WindowManager.LayoutParams layoutParams = window.getAttributes();
+ layoutParams.screenBrightness = currentProgressPercent;
+ window.setAttributes(layoutParams);
+
+ if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentProgressPercent);
+
+ final int resId =
+ currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_72dp
+ : currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp
+ : R.drawable.ic_brightness_high_white_72dp;
+
+ playerImpl.getBrightnessImageView().setImageDrawable(
+ AppCompatResources.getDrawable(service, resId)
+ );
+
+ if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
+ animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200);
+ }
+ if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
+ playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE);
+ }
+ }
+ return true;
+ }
+
+ private void onScrollEndInMain() {
+ if (DEBUG) Log.d(TAG, "onScrollEnd() called");
+
+ if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
+ animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
+ }
+ if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
+ animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
+ }
+
+ if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
+ playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
+ }
+ }
+
+ private boolean onTouchInMain(View v, MotionEvent event) {
+ playerImpl.getGestureDetector().onTouchEvent(event);
+ if (event.getAction() == MotionEvent.ACTION_UP && isMovingInMain) {
+ isMovingInMain = false;
+ onScrollEndInMain();
+ }
+ // This hack allows to stop receiving touch events on appbar while touching video player view
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ v.getParent().requestDisallowInterceptTouchEvent(playerImpl.isInFullscreen());
+ return true;
+ case MotionEvent.ACTION_UP:
+ v.getParent().requestDisallowInterceptTouchEvent(false);
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup player listener
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private boolean onDoubleTapInPopup(MotionEvent e) {
+ if (playerImpl == null || !playerImpl.isPlaying()) return false;
+
+ playerImpl.hideControls(0, 0);
+
+ if (e.getX() > playerImpl.getPopupWidth() / 2) {
+ playerImpl.onFastForward();
+ } else {
+ playerImpl.onFastRewind();
+ }
+
+ return true;
+ }
+
+ private boolean onSingleTapConfirmedInPopup(MotionEvent e) {
+ if (playerImpl == null || playerImpl.getPlayer() == null) return false;
+ if (playerImpl.isControlsVisible()) {
+ playerImpl.hideControls(100, 100);
+ } else {
+ playerImpl.showControlsThenHide();
+
+ }
+ playerImpl.onPlayPause();
+ return true;
+ }
+
+ private boolean onDownInPopup(MotionEvent e) {
+ // 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).
+ playerImpl.checkPopupPositionBounds(playerImpl.getCloseOverlayView().getWidth(), playerImpl.getCloseOverlayView().getHeight());
+
+ initialPopupX = playerImpl.getPopupLayoutParams().x;
+ initialPopupY = playerImpl.getPopupLayoutParams().y;
+ playerImpl.setPopupWidth(playerImpl.getPopupLayoutParams().width);
+ playerImpl.setPopupHeight(playerImpl.getPopupLayoutParams().height);
+ return super.onDown(e);
+ }
+
+ private void onLongPressInPopup(MotionEvent e) {
+ playerImpl.updateScreenSize();
+ playerImpl.checkPopupPositionBounds();
+ playerImpl.updatePopupSize((int) playerImpl.getScreenWidth(), -1);
+ }
+
+ private boolean onScrollInPopup(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) {
+ if (isResizing || playerImpl == null) return super.onScroll(initialEvent, movingEvent, distanceX, distanceY);
+
+ if (!isMovingInPopup) {
+ animateView(playerImpl.getCloseOverlayButton(), true, 200);
+ }
+
+ isMovingInPopup = true;
+
+ float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()), posX = (int) (initialPopupX + diffX);
+ float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()), posY = (int) (initialPopupY + diffY);
+
+ if (posX > (playerImpl.getScreenWidth() - playerImpl.getPopupWidth())) posX = (int) (playerImpl.getScreenWidth() - playerImpl.getPopupWidth());
+ else if (posX < 0) posX = 0;
+
+ if (posY > (playerImpl.getScreenHeight() - playerImpl.getPopupHeight())) posY = (int) (playerImpl.getScreenHeight() - playerImpl.getPopupHeight());
+ else if (posY < 0) posY = 0;
+
+ playerImpl.getPopupLayoutParams().x = (int) posX;
+ playerImpl.getPopupLayoutParams().y = (int) posY;
+
+ final View closingOverlayView = playerImpl.getClosingOverlayView();
+ if (playerImpl.isInsideClosingRadius(movingEvent)) {
+ if (closingOverlayView.getVisibility() == View.GONE) {
+ animateView(closingOverlayView, true, 250);
+ }
+ } else {
+ if (closingOverlayView.getVisibility() == View.VISIBLE) {
+ animateView(closingOverlayView, false, 0);
+ }
+ }
+
+ //noinspection PointlessBooleanExpression
+ if (DEBUG && false) {
+ Log.d(TAG, "PopupVideoPlayer.onScroll = " +
+ ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + ", e1.getX,Y = [" + initialEvent.getX() + ", " + initialEvent.getY() + "]" +
+ ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + ", e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "]" +
+ ", distanceX,Y = [" + distanceX + ", " + distanceY + "]" +
+ ", posX,Y = [" + posX + ", " + posY + "]" +
+ ", popupW,H = [" + playerImpl.getPopupWidth() + " x " + playerImpl.getPopupHeight() + "]");
+ }
+ playerImpl.windowManager.updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams());
+ return true;
+ }
+
+ private void onScrollEndInPopup(MotionEvent event) {
+ if (playerImpl == null) return;
+ if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
+ playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
+ }
+
+ if (playerImpl.isInsideClosingRadius(event)) {
+ playerImpl.closePopup();
+ } else {
+ animateView(playerImpl.getClosingOverlayView(), false, 0);
+
+ if (!playerImpl.isPopupClosing) {
+ animateView(playerImpl.getCloseOverlayButton(), false, 200);
+ }
+ }
+ }
+
+ private boolean onFlingInPopup(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ if (playerImpl == null) return false;
+
+ final float absVelocityX = Math.abs(velocityX);
+ final float absVelocityY = Math.abs(velocityY);
+ if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) {
+ if (absVelocityX > tossFlingVelocity) playerImpl.getPopupLayoutParams().x = (int) velocityX;
+ if (absVelocityY > tossFlingVelocity) playerImpl.getPopupLayoutParams().y = (int) velocityY;
+ playerImpl.checkPopupPositionBounds();
+ playerImpl.windowManager.updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams());
+ return true;
+ }
+ return false;
+ }
+
+ private boolean onTouchInPopup(View v, MotionEvent event) {
+ playerImpl.getGestureDetector().onTouchEvent(event);
+ if (playerImpl == null) return false;
+ if (event.getPointerCount() == 2 && !isMovingInPopup && !isResizing) {
+ if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.");
+ playerImpl.showAndAnimateControl(-1, true);
+ playerImpl.getLoadingPanel().setVisibility(View.GONE);
+
+ playerImpl.hideControls(0, 0);
+ animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0);
+ animateView(playerImpl.getResizingIndicator(), true, 200, 0);
+ isResizing = true;
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) {
+ if (DEBUG) Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
+ return handleMultiDrag(event);
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ if (DEBUG)
+ Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
+ if (isMovingInPopup) {
+ isMovingInPopup = false;
+ onScrollEndInPopup(event);
+ }
+
+ if (isResizing) {
+ isResizing = false;
+ animateView(playerImpl.getResizingIndicator(), false, 100, 0);
+ playerImpl.changeState(playerImpl.getCurrentState());
+ }
+
+ if (!playerImpl.isPopupClosing) {
+ playerImpl.savePositionAndSize();
+ }
+ }
+
+ v.performClick();
+ return true;
+ }
+
+ private boolean handleMultiDrag(final MotionEvent event) {
+ if (event.getPointerCount() != 2) return false;
+
+ final float firstPointerX = event.getX(0);
+ final float secondPointerX = event.getX(1);
+
+ final float diff = Math.abs(firstPointerX - secondPointerX);
+ if (firstPointerX > secondPointerX) {
+ // second pointer is the anchor (the leftmost pointer)
+ playerImpl.getPopupLayoutParams().x = (int) (event.getRawX() - diff);
+ } else {
+ // first pointer is the anchor
+ playerImpl.getPopupLayoutParams().x = (int) event.getRawX();
+ }
+
+ playerImpl.checkPopupPositionBounds();
+ playerImpl.updateScreenSize();
+
+ final int width = (int) Math.min(playerImpl.getScreenWidth(), diff);
+ playerImpl.updatePopupSize(width, -1);
+
+ return true;
+ }
+
+}
+
+
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java
new file mode 100644
index 000000000..7422f9442
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java
@@ -0,0 +1,15 @@
+package org.schabi.newpipe.player.event;
+
+import com.google.android.exoplayer2.ExoPlaybackException;
+
+public interface PlayerServiceEventListener extends PlayerEventListener {
+ void onFullscreenStateChanged(boolean fullscreen);
+
+ void onMoreOptionsLongClicked();
+
+ void onPlayerError(ExoPlaybackException error);
+
+ boolean isFragmentStopped();
+
+ void hideSystemUIIfNeeded();
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index 4feed74fe..457b72120 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -80,8 +80,10 @@ public class PlaybackParameterDialog extends DialogFragment {
public static PlaybackParameterDialog newInstance(final double playbackTempo,
final double playbackPitch,
- final boolean playbackSkipSilence) {
+ final boolean playbackSkipSilence,
+ Callback callback) {
PlaybackParameterDialog dialog = new PlaybackParameterDialog();
+ dialog.callback = callback;
dialog.initialTempo = playbackTempo;
dialog.initialPitch = playbackPitch;
@@ -99,9 +101,9 @@ public class PlaybackParameterDialog extends DialogFragment {
@Override
public void onAttach(Context context) {
super.onAttach(context);
- if (context != null && context instanceof Callback) {
+ if (context instanceof Callback) {
callback = (Callback) context;
- } else {
+ } else if (callback == null) {
dismiss();
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsContentObserver.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsContentObserver.java
new file mode 100644
index 000000000..534fb26c3
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsContentObserver.java
@@ -0,0 +1,29 @@
+package org.schabi.newpipe.settings;
+
+import android.database.ContentObserver;
+import android.os.Handler;
+
+public class SettingsContentObserver extends ContentObserver {
+ private OnChangeListener listener;
+
+ public interface OnChangeListener {
+ void onSettingsChanged();
+ }
+
+ public SettingsContentObserver(Handler handler, OnChangeListener listener) {
+ super(handler);
+ this.listener = listener;
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return super.deliverSelfNotifications();
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ if (listener != null)
+ listener.onSettingsChanged();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
index e2b03c8e8..07ef0b6ac 100644
--- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
@@ -10,6 +10,7 @@ import android.os.Build;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
@@ -44,14 +45,9 @@ 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.BackgroundPlayer;
-import org.schabi.newpipe.player.BackgroundPlayerActivity;
-import org.schabi.newpipe.player.BasePlayer;
-import org.schabi.newpipe.player.MainVideoPlayer;
-import org.schabi.newpipe.player.PopupVideoPlayer;
-import org.schabi.newpipe.player.PopupVideoPlayerActivity;
-import org.schabi.newpipe.player.VideoPlayer;
+import org.schabi.newpipe.player.*;
import org.schabi.newpipe.player.playqueue.PlayQueue;
+import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.settings.SettingsActivity;
import java.util.ArrayList;
@@ -78,6 +74,9 @@ public class NavigationHelper {
if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality);
intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback);
+ int playerType = intent.getIntExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_VIDEO);
+ intent.putExtra(VideoPlayer.PLAYER_TYPE, playerType);
+
return intent;
}
@@ -117,10 +116,13 @@ public class NavigationHelper {
.putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence);
}
- public static void playOnMainPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
- final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue, resumePlayback);
- playerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(playerIntent);
+ public static void playOnMainPlayer(final AppCompatActivity activity, final PlayQueue queue, final boolean resumePlayback) {
+ playOnMainPlayer(activity.getSupportFragmentManager(), queue, resumePlayback);
+ }
+
+ public static void playOnMainPlayer(final FragmentManager fragmentManager, final PlayQueue queue, boolean autoPlay) {
+ PlayQueueItem currentStream = queue.getItem();
+ NavigationHelper.openVideoDetailFragment(fragmentManager, currentStream.getServiceId(), currentStream.getUrl(), currentStream.getTitle(), autoPlay, queue);
}
public static void playOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
@@ -130,12 +132,16 @@ public class NavigationHelper {
}
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
- startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue, resumePlayback));
+ Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
+ intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP);
+ startService(context, intent);
}
public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show();
- startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue, resumePlayback));
+ Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
+ intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO);
+ startService(context, intent);
}
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
@@ -149,8 +155,9 @@ public class NavigationHelper {
}
Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show();
- startService(context,
- getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend, resumePlayback));
+ Intent intent = getPlayerEnqueueIntent(context, MainPlayer.class, queue, selectOnAppend, resumePlayback);
+ intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP);
+ startService(context, intent);
}
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
@@ -159,8 +166,9 @@ public class NavigationHelper {
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend, final boolean resumePlayback) {
Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show();
- startService(context,
- getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend, resumePlayback));
+ Intent intent = getPlayerEnqueueIntent(context, MainPlayer.class, queue, selectOnAppend, resumePlayback);
+ intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO);
+ startService(context, intent);
}
public static void startService(@NonNull final Context context, @NonNull final Intent intent) {
@@ -281,29 +289,35 @@ public class NavigationHelper {
}
public static void openVideoDetailFragment(FragmentManager fragmentManager, int serviceId, String url, String title) {
- openVideoDetailFragment(fragmentManager, serviceId, url, title, false);
+ openVideoDetailFragment(fragmentManager, serviceId, url, title, true, null);
}
- public static void openVideoDetailFragment(FragmentManager fragmentManager, int serviceId, String url, String title, boolean autoPlay) {
- Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_holder);
+ public static void openVideoDetailFragment(FragmentManager fragmentManager, int serviceId, String url, String title, boolean autoPlay, PlayQueue playQueue) {
+ Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_player_holder);
if (title == null) title = "";
if (fragment instanceof VideoDetailFragment && fragment.isVisible()) {
+ expandMainPlayer(fragment.getActivity());
VideoDetailFragment detailFragment = (VideoDetailFragment) fragment;
detailFragment.setAutoplay(autoPlay);
- detailFragment.selectAndLoadVideo(serviceId, url, title);
+ detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue);
return;
}
- VideoDetailFragment instance = VideoDetailFragment.getInstance(serviceId, url, title);
+ VideoDetailFragment instance = VideoDetailFragment.getInstance(serviceId, url, title, playQueue);
instance.setAutoplay(autoPlay);
defaultTransaction(fragmentManager)
- .replace(R.id.fragment_holder, instance)
- .addToBackStack(null)
+ .replace(R.id.fragment_player_holder, instance)
+ .runOnCommit(() -> expandMainPlayer(instance.getActivity()))
.commit();
}
+ public static void expandMainPlayer(Context context) {
+ final Intent intent = new Intent(VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER);
+ context.sendBroadcast(intent);
+ }
+
public static void openChannelFragment(
FragmentManager fragmentManager,
int serviceId,
@@ -458,10 +472,6 @@ public class NavigationHelper {
return getServicePlayerActivityIntent(context, BackgroundPlayerActivity.class);
}
- public static Intent getPopupPlayerActivityIntent(final Context context) {
- return getServicePlayerActivityIntent(context, PopupVideoPlayerActivity.class);
- }
-
private static Intent getServicePlayerActivityIntent(final Context context,
final Class activityClass) {
Intent intent = new Intent(context, activityClass);
diff --git a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java
index c5c78a726..17768cd08 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java
@@ -17,6 +17,6 @@ public class ShareUtils {
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
intent.putExtra(Intent.EXTRA_TEXT, url);
- context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_dialog_title)));
+ context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_dialog_title)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
}
diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/activity_main_player.xml
index b535db2b8..cbcc6eeb0 100644
--- a/app/src/main/res/layout-large-land/activity_main_player.xml
+++ b/app/src/main/res/layout-large-land/activity_main_player.xml
@@ -2,9 +2,10 @@