diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 0b3ddc3c3..ecf235abc 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -201,7 +201,7 @@ public final class VideoDetailFragment @Nullable private MainPlayer playerService; private Player player; - private PlayerHolder playerHolder = PlayerHolder.getInstance(); + private final PlayerHolder playerHolder = PlayerHolder.getInstance(); /*////////////////////////////////////////////////////////////////////////// // Service management @@ -220,7 +220,7 @@ public final class VideoDetailFragment return; } - if (isLandscape()) { + if (DeviceUtils.isLandscape(requireContext())) { // If the video is playing but orientation changed // let's make the video in fullscreen again checkLandscape(); @@ -241,7 +241,7 @@ public final class VideoDetailFragment && isAutoplayEnabled() && player.getParentActivity() == null)) { autoPlayEnabled = true; // forcefully start playing - openVideoPlayer(); + openVideoPlayerAutoFullscreen(); } } @@ -499,7 +499,7 @@ public final class VideoDetailFragment break; case R.id.detail_thumbnail_root_layout: autoPlayEnabled = true; // forcefully start playing - openVideoPlayer(); + openVideoPlayerAutoFullscreen(); break; case R.id.detail_title_root_layout: toggleTitleAndSecondaryControls(); @@ -516,7 +516,7 @@ public final class VideoDetailFragment showSystemUi(); } else { autoPlayEnabled = true; // forcefully start playing - openVideoPlayer(); + openVideoPlayer(false); } setOverlayPlayPauseImage(isPlayerAvailable() && player.isPlaying()); @@ -762,7 +762,7 @@ public final class VideoDetailFragment private void setupFromHistoryItem(final StackItem item) { setAutoPlay(false); - hideMainPlayer(); + hideMainPlayerOnLoadingNewStream(); setInitialData(item.getServiceId(), item.getUrl(), item.getTitle() == null ? "" : item.getTitle(), item.getPlayQueue()); @@ -882,7 +882,7 @@ public final class VideoDetailFragment .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { isLoading.set(false); - hideMainPlayer(); + hideMainPlayerOnLoadingNewStream(); if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean( getString(R.string.show_age_restricted_content), false)) { hideAgeRestrictedContent(); @@ -897,8 +897,9 @@ public final class VideoDetailFragment stack.push(new StackItem(serviceId, url, title, playQueue)); } } + if (isAutoplayEnabled()) { - openVideoPlayer(); + openVideoPlayerAutoFullscreen(); } } }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM, @@ -1103,7 +1104,29 @@ public final class VideoDetailFragment } } - public void openVideoPlayer() { + /** + * Opens the video player, in fullscreen if needed. In order to open fullscreen, the activity + * is toggled to landscape orientation (which will then cause fullscreen mode). + * + * @param directlyFullscreenIfApplicable whether to open fullscreen if we are not already + * in landscape and screen orientation is locked + */ + public void openVideoPlayer(final boolean directlyFullscreenIfApplicable) { + if (directlyFullscreenIfApplicable + && !DeviceUtils.isLandscape(requireContext()) + && PlayerHelper.globalScreenOrientationLocked(requireContext())) { + // Make sure the bottom sheet turns out expanded. When this code kicks in the bottom + // sheet could not have fully expanded yet, and thus be in the STATE_SETTLING state. + // When the activity is rotated, and its state is saved and then restored, the bottom + // sheet would forget what it was doing, since even if STATE_SETTLING is restored, it + // doesn't tell which state it was settling to, and thus the bottom sheet settles to + // STATE_COLLAPSED. This can be solved by manually setting the state that will be + // restored (i.e. bottomSheetState) to STATE_EXPANDED. + bottomSheetState = BottomSheetBehavior.STATE_EXPANDED; + // toggle landscape in order to open directly in fullscreen + onScreenRotationButtonClicked(); + } + if (PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(this.getString(R.string.use_external_video_player_key), false)) { showExternalPlaybackDialog(); @@ -1112,6 +1135,18 @@ public final class VideoDetailFragment } } + /** + * If the option to start directly fullscreen is enabled, calls + * {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable = true}, so that + * if the user is not already in landscape and he has screen orientation locked the activity + * rotates and fullscreen starts. Otherwise, if the option to start directly fullscreen is + * disabled, calls {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable + * = false}, hence preventing it from going directly fullscreen. + */ + public void openVideoPlayerAutoFullscreen() { + openVideoPlayer(PlayerHelper.isStartMainPlayerFullscreenEnabled(requireContext())); + } + private void openNormalBackgroundPlayer(final boolean append) { // See UI changes while remote playQueue changes if (!isPlayerAvailable()) { @@ -1145,12 +1180,19 @@ public final class VideoDetailFragment } addVideoPlayerView(); - final Intent playerIntent = NavigationHelper - .getPlayerIntent(requireContext(), MainPlayer.class, queue, true, autoPlayEnabled); + final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(), + MainPlayer.class, queue, true, autoPlayEnabled); ContextCompat.startForegroundService(activity, playerIntent); } - private void hideMainPlayer() { + /** + * When the video detail fragment is already showing details for a video and the user opens a + * new one, the video detail fragment changes all of its old data to the new stream, so if there + * is a video player currently open it should be hidden. This method does exactly that. If + * autoplay is enabled, the underlying player is not stopped completely, since it is going to + * be reused in a few milliseconds and the flickering would be annoying. + */ + private void hideMainPlayerOnLoadingNewStream() { if (!isPlayerServiceAvailable() || playerService.getView() == null || !player.videoPlayerSelected()) { @@ -1158,8 +1200,12 @@ public final class VideoDetailFragment } removeVideoPlayerView(); - playerService.stop(isAutoplayEnabled()); - playerService.getView().setVisibility(View.GONE); + if (isAutoplayEnabled()) { + playerService.stopForImmediateReusing(); + playerService.getView().setVisibility(View.GONE); + } else { + playerHolder.stopService(); + } } private PlayQueue setupPlayQueueForIntent(final boolean append) { @@ -1252,7 +1298,7 @@ public final class VideoDetailFragment final DisplayMetrics metrics = getResources().getDisplayMetrics(); if (getView() != null) { - final int height = (isInMultiWindow() + final int height = (DeviceUtils.isInMultiWindow(activity) ? requireView() : activity.getWindow().getDecorView()).getHeight(); setHeightThumbnail(height, metrics); @@ -1275,7 +1321,7 @@ public final class VideoDetailFragment requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener); if (isPlayerAvailable() && player.isFullscreen()) { - final int height = (isInMultiWindow() + final int height = (DeviceUtils.isInMultiWindow(activity) ? requireView() : activity.getWindow().getDecorView()).getHeight(); // Height is zero when the view is not yet displayed like after orientation change @@ -1808,7 +1854,7 @@ public final class VideoDetailFragment || error.type == ExoPlaybackException.TYPE_UNEXPECTED) { // Properly exit from fullscreen toggleFullscreenIfInFullscreenMode(); - hideMainPlayer(); + hideMainPlayerOnLoadingNewStream(); } } @@ -1864,13 +1910,14 @@ public final class VideoDetailFragment // from landscape to portrait every time. // Just turn on fullscreen mode in landscape orientation // or portrait & unlocked global orientation + final boolean isLandscape = DeviceUtils.isLandscape(requireContext()); if (DeviceUtils.isTablet(activity) - && (!globalScreenOrientationLocked(activity) || isLandscape())) { + && (!globalScreenOrientationLocked(activity) || isLandscape)) { player.toggleFullscreen(); return; } - final int newOrientation = isLandscape() + final int newOrientation = isLandscape ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; @@ -1942,15 +1989,17 @@ public final class VideoDetailFragment | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + // In multiWindow mode status bar is not transparent for devices with cutout // if I include this flag. So without it is better in this case - if (!isInMultiWindow()) { + final boolean isInMultiWindow = DeviceUtils.isInMultiWindow(activity); + if (!isInMultiWindow) { visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN; } activity.getWindow().getDecorView().setSystemUiVisibility(visibility); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - && (isInMultiWindow() || (isPlayerAvailable() && player.isFullscreen()))) { + && (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) { activity.getWindow().setStatusBarColor(Color.TRANSPARENT); activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); } @@ -2022,15 +2071,6 @@ public final class VideoDetailFragment } } - private boolean isLandscape() { - return getResources().getDisplayMetrics().heightPixels < getResources() - .getDisplayMetrics().widthPixels; - } - - private boolean isInMultiWindow() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode(); - } - /* * Means that the player fragment was swiped away via BottomSheetLayout * and is empty but ready for any new actions. See cleanUp() @@ -2213,7 +2253,7 @@ public final class VideoDetailFragment setOverlayElementsClickable(false); hideSystemUiIfNeeded(); // Conditions when the player should be expanded to fullscreen - if (isLandscape() + if (DeviceUtils.isLandscape(requireContext()) && isPlayerAvailable() && player.isPlaying() && !player.isFullscreen() diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java index 7a04ec22e..a9b9f4c87 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java @@ -24,7 +24,6 @@ import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.IBinder; -import android.util.DisplayMetrics; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -36,6 +35,7 @@ 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; @@ -133,32 +133,29 @@ public final class MainPlayer extends Service { return START_NOT_STICKY; } - public void stop(final boolean autoplayEnabled) { + public void stopForImmediateReusing() { if (DEBUG) { - Log.d(TAG, "stop() called"); + Log.d(TAG, "stopForImmediateReusing() called"); } if (!player.exoPlayerIsNull()) { player.saveWasPlaying(); + // Releases wifi & cpu, disables keepScreenOn, etc. - if (!autoplayEnabled) { - player.pause(); - } // 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 - if (!autoplayEnabled) { - NotificationUtil.getInstance().cancelNotificationAndStopForeground(this); - } } } @@ -222,11 +219,8 @@ public final class MainPlayer extends Service { boolean isLandscape() { // DisplayMetrics from activity context knows about MultiWindow feature // while DisplayMetrics from app context doesn't - final DisplayMetrics metrics = (player != null - && player.getParentActivity() != null - ? player.getParentActivity().getResources() - : getResources()).getDisplayMetrics(); - return metrics.heightPixels < metrics.widthPixels; + return DeviceUtils.isLandscape(player != null && player.getParentActivity() != null + ? player.getParentActivity() : this); } @Nullable diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index ae9720474..dd5468f69 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -621,6 +621,9 @@ public final class Player implements return; } + // needed for tablets, check the function for a better explanation + directlyOpenFullscreenIfNeeded(); + final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this); final float playbackSpeed = savedParameters.speed; final float playbackPitch = savedParameters.pitch; @@ -672,6 +675,7 @@ public final class Player implements && isPlaybackResumeEnabled(this) && !samePlayQueue && !newQueue.isEmpty() + && newQueue.getItem() != null && newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem()) .observeOn(AndroidSchedulers.mainThread()) @@ -743,6 +747,22 @@ public final class Player implements NavigationHelper.sendPlayerStartedEvent(context); } + /** + * 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 (fragmentListener != null + && PlayerHelper.isStartMainPlayerFullscreenEnabled(service) + && DeviceUtils.isTablet(service) + && videoPlayerSelected() + && PlayerHelper.globalScreenOrientationLocked(service)) { + fragmentListener.onScreenRotationButtonClicked(); + } + } + private void initPlayback(@NonNull final PlayQueue queue, @RepeatMode final int repeatMode, final float playbackSpeed, @@ -3855,11 +3875,9 @@ public final class Player implements if (DEBUG) { Log.d(TAG, "toggleFullscreen() called"); } - if (popupPlayerSelected() || exoPlayerIsNull() || currentMetadata == null - || fragmentListener == null) { + if (popupPlayerSelected() || exoPlayerIsNull() || fragmentListener == null) { return; } - //changeState(STATE_BLOCKED); TODO check what this does isFullscreen = !isFullscreen; if (!isFullscreen) { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index b19e6e823..828833a8d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -239,6 +239,11 @@ public final class PlayerHelper { .getBoolean(context.getString(R.string.brightness_gesture_control_key), true); } + public static boolean isStartMainPlayerFullscreenEnabled(@NonNull final Context context) { + return getPreferences(context) + .getBoolean(context.getString(R.string.start_main_player_fullscreen_key), false); + } + public static boolean isAutoQueueEnabled(@NonNull final Context context) { return getPreferences(context) .getBoolean(context.getString(R.string.auto_queue_key), false); diff --git a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java index 8d918c162..73bc4d6bb 100644 --- a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java @@ -11,6 +11,7 @@ import android.view.KeyEvent; import androidx.annotation.Dimension; import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; @@ -130,4 +131,13 @@ public final class DeviceUtils { && !HI3798MV200 && !CVT_MT5886_EU_1G; } + + public static boolean isLandscape(final Context context) { + return context.getResources().getDisplayMetrics().heightPixels < context.getResources() + .getDisplayMetrics().widthPixels; + } + + public static boolean isInMultiWindow(final AppCompatActivity activity) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index ad9654073..eba24020f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -366,7 +366,9 @@ public final class NavigationHelper { if (switchingPlayers) { // Situation when user switches from players to main player. All needed data is // here, we can start watching (assuming newQueue equals playQueue). - detailFragment.openVideoPlayer(); + // Starting directly in fullscreen if the previous player type was popup. + detailFragment.openVideoPlayer(playerType == MainPlayer.PlayerType.POPUP + || PlayerHelper.isStartMainPlayerFullscreenEnabled(context)); } else { detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue); } diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 45400d667..013b970c1 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -73,6 +73,8 @@ @string/minimize_on_exit_popup_description + start_main_player_fullscreen_key + autoplay_key @string/autoplay_wifi_key autoplay_always_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 91e9db335..f833dc716 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -132,6 +132,8 @@ Resume playing Continue playing after interruptions (e.g. phonecalls) Download + Start main player in fullscreen + Do not start videos in the mini player, but turn to fullscreen mode directly, if auto rotation is locked. You can still access the mini player by exiting fullscreen. Autoplay Show \"Hold to append\" tip Show tip when pressing the background or the popup button in video \"Details:\" diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml index 1b595be0c..f605fbe17 100644 --- a/app/src/main/res/xml/video_audio_settings.xml +++ b/app/src/main/res/xml/video_audio_settings.xml @@ -129,6 +129,14 @@ app:singleLineTitle="false" app:iconSpaceReserved="false" /> + +