mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-11-04 01:03:00 +00:00 
			
		
		
		
	Merge pull request #8170 from Stypox/player-refactor
Refactor player and extract UI components
This commit is contained in:
		@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user