NewPipe-app-android/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java

589 lines
22 KiB
Java

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
}