diff --git a/app/src/main/java/com/github/libretube/helpers/WindowHelper.kt b/app/src/main/java/com/github/libretube/helpers/WindowHelper.kt index 8dfa48546..a77156cc2 100644 --- a/app/src/main/java/com/github/libretube/helpers/WindowHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/WindowHelper.kt @@ -1,15 +1,14 @@ package com.github.libretube.helpers -import android.app.Activity import android.os.Build +import android.view.Window import android.view.WindowManager import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import com.github.libretube.ui.extensions.toggleSystemBars object WindowHelper { - fun toggleFullscreen(activity: Activity, isFullscreen: Boolean) { - val window = activity.window + fun toggleFullscreen(window: Window, isFullscreen: Boolean) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { window.attributes.layoutInDisplayCutoutMode = if (isFullscreen) { WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES @@ -30,7 +29,7 @@ object WindowHelper { // Show the system bars when it is not fullscreen and hide them when it is fullscreen // System bars means status bar and the navigation bar // See: https://developer.android.com/training/system-ui/immersive#kotlin - activity.toggleSystemBars( + window.toggleSystemBars( types = WindowInsetsCompat.Type.systemBars(), showBars = !isFullscreen ) diff --git a/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt index 0e8356724..442fd2288 100644 --- a/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt @@ -486,8 +486,8 @@ class MainActivity : BaseActivity() { super.onConfigurationChanged(newConfig) when (newConfig.orientation) { - Configuration.ORIENTATION_PORTRAIT -> WindowHelper.toggleFullscreen(this, false) - Configuration.ORIENTATION_LANDSCAPE -> WindowHelper.toggleFullscreen(this, true) + Configuration.ORIENTATION_PORTRAIT -> WindowHelper.toggleFullscreen(window, false) + Configuration.ORIENTATION_LANDSCAPE -> WindowHelper.toggleFullscreen(window, true) } } diff --git a/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt index 18856de6f..04e522821 100644 --- a/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt @@ -56,7 +56,7 @@ class OfflinePlayerActivity : BaseActivity() { private val playerViewModel: PlayerViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { - WindowHelper.toggleFullscreen(this, true) + WindowHelper.toggleFullscreen(window, true) requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE diff --git a/app/src/main/java/com/github/libretube/ui/base/BaseActivity.kt b/app/src/main/java/com/github/libretube/ui/base/BaseActivity.kt index 96168fa3b..cc03b8dee 100644 --- a/app/src/main/java/com/github/libretube/ui/base/BaseActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/base/BaseActivity.kt @@ -13,7 +13,7 @@ import com.github.libretube.helpers.ThemeHelper * Activity that applies the LibreTube theme and the in-app language */ open class BaseActivity : AppCompatActivity() { - private val screenOrientationPref by lazy { + val screenOrientationPref by lazy { val orientationPref = PreferenceHelper.getString( PreferenceKeys.ORIENTATION, resources.getString(R.string.config_default_orientation_pref) diff --git a/app/src/main/java/com/github/libretube/ui/extensions/Activity.kt b/app/src/main/java/com/github/libretube/ui/extensions/Activity.kt index da8f0679f..71d5b2ac9 100644 --- a/app/src/main/java/com/github/libretube/ui/extensions/Activity.kt +++ b/app/src/main/java/com/github/libretube/ui/extensions/Activity.kt @@ -1,12 +1,12 @@ package com.github.libretube.ui.extensions -import android.app.Activity +import android.view.Window import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat.Type.InsetsType import androidx.core.view.WindowInsetsControllerCompat -fun Activity.toggleSystemBars(@InsetsType types: Int, showBars: Boolean) { - WindowCompat.getInsetsController(window, window.decorView).apply { +fun Window.toggleSystemBars(@InsetsType types: Int, showBars: Boolean) { + WindowCompat.getInsetsController(this, decorView).apply { systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE if (showBars) { show(types) diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt index c15e88441..78109d89f 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt @@ -1,6 +1,7 @@ package com.github.libretube.ui.fragments import android.annotation.SuppressLint +import android.app.Dialog import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -17,7 +18,9 @@ import android.text.format.DateUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams import android.widget.Toast +import android.window.OnBackInvokedDispatcher import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.TransitionAdapter import androidx.core.content.getSystemService @@ -85,12 +88,14 @@ import com.github.libretube.helpers.PlayerHelper.getVideoStats import com.github.libretube.helpers.PlayerHelper.isInSegment import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.ProxyHelper +import com.github.libretube.helpers.WindowHelper import com.github.libretube.obj.PlayerNotificationData import com.github.libretube.obj.ShareData import com.github.libretube.obj.VideoResolution import com.github.libretube.parcelable.PlayerData import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.adapters.VideosAdapter +import com.github.libretube.ui.base.BaseActivity import com.github.libretube.ui.dialogs.AddToPlaylistDialog import com.github.libretube.ui.dialogs.DownloadDialog import com.github.libretube.ui.dialogs.ShareDialog @@ -188,6 +193,21 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { private var scrubbingTimeBar = false private var chaptersBottomSheet: ChaptersBottomSheet? = null + /** + * The orientation of the `fragment_player.xml` that's currently used + * This is needed in order to figure out if the current layout is the landscape one or not. + */ + private var playerLayoutOrientation = Int.MIN_VALUE + + private val fullscreenDialog by lazy { + object: Dialog(requireContext(), android.R.style.Theme_Black_NoTitleBar_Fullscreen) { + override fun onBackPressed() { + super.onBackPressed() + unsetFullscreen() + } + } + } + /** * Receiver for all actions in the PiP mode */ @@ -236,6 +256,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { keepQueue = playerData.keepQueue timeStamp = playerData.timestamp + playerLayoutOrientation = resources.configuration.orientation + // broadcast receiver for PiP actions context?.registerReceiver( broadcastReceiver, @@ -365,17 +387,19 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { .isPictureInPictureAvailable(activity) } + private fun onManualPlayerClose() { + PlayingQueue.clear() + BackgroundHelper.stopBackgroundPlay(requireContext()) + killPlayerFragment() + } + // actions that don't depend on video information private fun initializeOnClickActions() { binding.closeImageView.setOnClickListener { - PlayingQueue.clear() - BackgroundHelper.stopBackgroundPlay(requireContext()) - killPlayerFragment() + onManualPlayerClose() } playerBinding.closeImageButton.setOnClickListener { - PlayingQueue.clear() - BackgroundHelper.stopBackgroundPlay(requireContext()) - killPlayerFragment() + onManualPlayerClose() } playerBinding.autoPlay.isVisible = true @@ -499,50 +523,30 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { NavigationHelper.startAudioPlayer(requireContext()) } - /** - * If enabled, determine the orientation o use based on the video's aspect ratio - * Expected behavior: Portrait for shorts, Landscape for normal videos - */ - private fun updateFullscreenOrientation() { - if (!PlayerHelper.autoFullscreenEnabled) { - val height = streams.videoStreams.firstOrNull()?.height ?: exoPlayer.videoSize.height - val width = streams.videoStreams.firstOrNull()?.width ?: exoPlayer.videoSize.width - - // different orientations of the video are only available when autorotation is disabled - val orientation = PlayerHelper.getOrientation(width, height) - mainActivity.requestedOrientation = orientation - } - } - private fun setFullscreen() { - with(binding.playerMotionLayout) { - getConstraintSet(R.id.start).constrainHeight(R.id.player, -1) - enableTransition(R.id.yt_transition, false) - } - // set status bar icon color to white windowInsetsControllerCompat.isAppearanceLightStatusBars = false - binding.mainContainer.isClickable = true - binding.linLayout.isGone = true + viewModel.isFullscreen.value = true + + if (mainActivity.screenOrientationPref == ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT) { + val height = streams.videoStreams.firstOrNull()?.height ?: exoPlayer.videoSize.height + val width = streams.videoStreams.firstOrNull()?.width ?: exoPlayer.videoSize.width + + mainActivity.requestedOrientation = PlayerHelper.getOrientation(width, height) + } + commentsViewModel.setCommentSheetExpand(null) playerBinding.fullscreen.setImageResource(R.drawable.ic_fullscreen_exit) playerBinding.exoTitle.isVisible = true - updateFullscreenOrientation() - viewModel.isFullscreen.value = true - updateResolutionOnFullscreenChange(true) + + openOrCloseFullscreenDialog(true) } @SuppressLint("SourceLockedOrientationActivity") fun unsetFullscreen() { - // leave fullscreen mode - with(binding.playerMotionLayout) { - getConstraintSet(R.id.start).constrainHeight(R.id.player, 0) - enableTransition(R.id.yt_transition, true) - } - // set status bar icon color back to theme color windowInsetsControllerCompat.isAppearanceLightStatusBars = when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { @@ -551,19 +555,40 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { else -> true } - binding.mainContainer.isClickable = false - binding.linLayout.isVisible = true - playerBinding.fullscreen.setImageResource(R.drawable.ic_fullscreen) - playerBinding.exoTitle.isInvisible = true + viewModel.isFullscreen.value = false - if (!PlayerHelper.autoFullscreenEnabled) { - // switch back to portrait mode if autorotation disabled + if (mainActivity.screenOrientationPref == ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT) { mainActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT } - viewModel.isFullscreen.value = false + playerBinding.fullscreen.setImageResource(R.drawable.ic_fullscreen) + playerBinding.exoTitle.isInvisible = true updateResolutionOnFullscreenChange(false) + + openOrCloseFullscreenDialog(false) + + checkForNecessaryOrientationRestart() + } + + private fun openOrCloseFullscreenDialog(open: Boolean) { + val playerView = binding.player + (playerView.parent as ViewGroup).removeView(playerView) + + if (open) { + fullscreenDialog.addContentView( + binding.player, + LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + ) + fullscreenDialog.show() + playerView.currentWindow = fullscreenDialog.window + } else { + binding.playerMotionLayout.addView(playerView) + playerView.currentWindow = null + fullscreenDialog.dismiss() + } + + WindowHelper.toggleFullscreen(fullscreenDialog.window!!, open) } override fun onPause() { @@ -735,8 +760,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { initializePlayerView() setupSeekbarPreview() - if (viewModel.isFullscreen.value == true) updateFullscreenOrientation() - exoPlayer.playWhenReady = PlayerHelper.playAutomatically exoPlayer.prepare() @@ -1341,7 +1364,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { onConfigurationChanged(resources.configuration) } else { // go to portrait mode - mainActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT + mainActivity.requestedOrientation = (requireActivity() as BaseActivity).screenOrientationPref } } @@ -1470,13 +1493,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { viewModel.isMiniPlayerVisible.value = false } - with(binding.playerMotionLayout) { - getConstraintSet(R.id.start).constrainHeight(R.id.player, -1) - enableTransition(R.id.yt_transition, false) - } - binding.linLayout.isGone = true - updateCurrentSubtitle(null) + + openOrCloseFullscreenDialog(true) } else { // close button got clicked in PiP mode // pause the video and keep the app alive @@ -1485,20 +1504,13 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { // enable exoPlayer controls again binding.player.useController = true - // set back to portrait mode - if (viewModel.isFullscreen.value != true) { - with(binding.playerMotionLayout) { - getConstraintSet(R.id.start).constrainHeight(R.id.player, 0) - enableTransition(R.id.yt_transition, true) - } - binding.linLayout.isVisible = true - } - updateCurrentSubtitle(currentSubtitle) binding.optionsLL.post { binding.optionsLL.requestLayout() } + + openOrCloseFullscreenDialog(false) } } @@ -1564,6 +1576,12 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { private fun killPlayerFragment() { viewModel.isFullscreen.value = false viewModel.isMiniPlayerVisible.value = false + + // dismiss the fullscreen dialog if it's currently visible + // otherwise it would stay alive while being detached from this fragment + fullscreenDialog.dismiss() + binding.player.currentWindow = null + binding.playerMotionLayout.transitionToEnd() mainActivity.supportFragmentManager.commit { remove(this@PlayerFragment) @@ -1572,21 +1590,43 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { onDestroy() } + /** + * Check if the activity needs to be recreated due to an orientation change + * If true, the activity will be automatically restarted + */ + private fun checkForNecessaryOrientationRestart() { + val lockedOrientations = listOf(ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) + if (mainActivity.screenOrientationPref in lockedOrientations) return + + val orientation = resources.configuration.orientation + if (viewModel.isFullscreen.value != true && orientation != playerLayoutOrientation) { + if (this::exoPlayer.isInitialized) { + arguments?.putLong(IntentData.timeStamp, exoPlayer.currentPosition / 1000) + } + playerLayoutOrientation = orientation + activity?.recreate() + } + } + override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - if (!PlayerHelper.autoFullscreenEnabled || _binding == null || + if (_binding == null || // If in PiP mode, orientation is given as landscape. PictureInPictureCompat.isInPictureInPictureMode(requireActivity()) ) { return } - when (newConfig.orientation) { - // go to fullscreen mode - Configuration.ORIENTATION_LANDSCAPE -> setFullscreen() - // exit fullscreen if not landscape - else -> unsetFullscreen() + if (PlayerHelper.autoFullscreenEnabled) { + when (newConfig.orientation) { + // go to fullscreen mode + Configuration.ORIENTATION_LANDSCAPE -> setFullscreen() + // exit fullscreen if not landscape + else -> unsetFullscreen() + } + } else { + checkForNecessaryOrientationRestart() } } } diff --git a/app/src/main/java/com/github/libretube/ui/sheets/CommentsSheet.kt b/app/src/main/java/com/github/libretube/ui/sheets/CommentsSheet.kt index 6379e1201..43f992e96 100644 --- a/app/src/main/java/com/github/libretube/ui/sheets/CommentsSheet.kt +++ b/app/src/main/java/com/github/libretube/ui/sheets/CommentsSheet.kt @@ -39,19 +39,6 @@ class CommentsSheet : UndimmedBottomSheet() { val binding = binding - binding.dragHandle.viewTreeObserver.addOnGlobalLayoutListener(object : - ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - binding.dragHandle.viewTreeObserver.removeOnGlobalLayoutListener(this) - - // limit the recyclerview height to not cover the video - binding.standardBottomSheet.layoutParams = - binding.commentFragContainer.layoutParams.apply { - height = playerViewModel.maxSheetHeightPx - } - } - }) - binding.btnBack.setOnClickListener { if (childFragmentManager.backStackEntryCount > 0) { childFragmentManager.popBackStack() diff --git a/app/src/main/java/com/github/libretube/ui/sheets/UndimmedBottomSheet.kt b/app/src/main/java/com/github/libretube/ui/sheets/UndimmedBottomSheet.kt index b3a0934b6..a7d67b71f 100644 --- a/app/src/main/java/com/github/libretube/ui/sheets/UndimmedBottomSheet.kt +++ b/app/src/main/java/com/github/libretube/ui/sheets/UndimmedBottomSheet.kt @@ -1,6 +1,7 @@ package com.github.libretube.ui.sheets import android.app.Dialog +import android.content.res.Configuration import android.os.Bundle import android.view.KeyEvent import android.view.View @@ -21,9 +22,11 @@ abstract class UndimmedBottomSheet : ExpandedBottomSheet() { override fun onGlobalLayout() { getDragHandle().viewTreeObserver.removeOnGlobalLayoutListener(this) - // limit the recyclerview height to not cover the video - getBottomSheet().updateLayoutParams { - height = getSheetMaxHeightPx() + if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + // limit the recyclerview height to not cover the video + getBottomSheet().updateLayoutParams { + height = getSheetMaxHeightPx() + } } } }) diff --git a/app/src/main/java/com/github/libretube/ui/views/CustomExoPlayerView.kt b/app/src/main/java/com/github/libretube/ui/views/CustomExoPlayerView.kt index f293228a3..27c5fec2d 100644 --- a/app/src/main/java/com/github/libretube/ui/views/CustomExoPlayerView.kt +++ b/app/src/main/java/com/github/libretube/ui/views/CustomExoPlayerView.kt @@ -11,6 +11,7 @@ import android.text.format.DateUtils import android.util.AttributeSet import android.view.MotionEvent import android.view.View +import android.view.Window import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView @@ -87,6 +88,12 @@ open class CustomExoPlayerView( updateCurrentPosition() } + /** + * The window that needs to be addressed for showing and hiding the system bars + * If null, the activity's default/main window will be used + */ + var currentWindow: Window? = null + /** * Preferences */ @@ -143,7 +150,7 @@ open class CustomExoPlayerView( // change locked status isPlayerLocked = !isPlayerLocked - activity.toggleSystemBars( + (currentWindow ?: activity.window).toggleSystemBars( types = WindowInsetsCompat.Type.statusBars(), showBars = !isPlayerLocked ) diff --git a/app/src/main/java/com/github/libretube/ui/views/OfflinePlayerView.kt b/app/src/main/java/com/github/libretube/ui/views/OfflinePlayerView.kt index 78134d4a1..d41544f39 100644 --- a/app/src/main/java/com/github/libretube/ui/views/OfflinePlayerView.kt +++ b/app/src/main/java/com/github/libretube/ui/views/OfflinePlayerView.kt @@ -13,13 +13,13 @@ class OfflinePlayerView( override fun hideController() { super.hideController() // hide the status bars when continuing to watch video - activity.toggleSystemBars(WindowInsetsCompat.Type.systemBars(), false) + activity.window.toggleSystemBars(WindowInsetsCompat.Type.systemBars(), false) } override fun showController() { super.showController() // show status bar when showing player options - activity.toggleSystemBars(WindowInsetsCompat.Type.statusBars(), true) + activity.window.toggleSystemBars(WindowInsetsCompat.Type.statusBars(), true) } override fun getTopBarMarginDp(): Int { diff --git a/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt b/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt index 25aa479b1..29544f4b8 100644 --- a/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt +++ b/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt @@ -145,7 +145,7 @@ class OnlinePlayerView( this.playerOptions = playerOptions playerViewModel.isFullscreen.observe(viewLifecycleOwner) { isFullscreen -> - WindowHelper.toggleFullscreen(activity, isFullscreen) + WindowHelper.toggleFullscreen(activity.window, isFullscreen) updateTopBarMargin() } @@ -154,7 +154,7 @@ class OnlinePlayerView( playerViewModel.isFullscreen.value?.let { isFullscreen -> if (!isFullscreen) return@let // Show status bar only not navigation bar if the player controls are visible and hide it otherwise - activity.toggleSystemBars( + activity.window.toggleSystemBars( types = WindowInsetsCompat.Type.statusBars(), showBars = visibility == View.VISIBLE && !isPlayerLocked ) @@ -189,7 +189,7 @@ class OnlinePlayerView( super.hideController() if (playerViewModel?.isFullscreen?.value == true) { - WindowHelper.toggleFullscreen(activity, true) + WindowHelper.toggleFullscreen(activity.window, true) } updateTopBarMargin() } diff --git a/app/src/main/res/layout-land/fragment_player.xml b/app/src/main/res/layout-land/fragment_player.xml new file mode 100644 index 000000000..ff8cb395f --- /dev/null +++ b/app/src/main/res/layout-land/fragment_player.xml @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/player_scene.xml b/app/src/main/res/xml/player_scene.xml index b1c1ad81e..a38522d44 100644 --- a/app/src/main/res/xml/player_scene.xml +++ b/app/src/main/res/xml/player_scene.xml @@ -33,9 +33,10 @@