diff --git a/app/src/main/java/com/nextcloud/client/player/ui/VideoPictureInPicture.kt b/app/src/main/java/com/nextcloud/client/player/ui/VideoPictureInPicture.kt index 05b4dbb6ad99..c941ef602516 100644 --- a/app/src/main/java/com/nextcloud/client/player/ui/VideoPictureInPicture.kt +++ b/app/src/main/java/com/nextcloud/client/player/ui/VideoPictureInPicture.kt @@ -9,59 +9,65 @@ package com.nextcloud.client.player.ui import android.app.Activity import android.app.PictureInPictureParams -import android.graphics.Rect import android.os.Build import android.util.Rational -import android.view.View -import com.nextcloud.client.player.media3.PlaybackModel import com.nextcloud.client.player.model.state.VideoSize import com.nextcloud.client.player.util.PlayerUtil.isPictureInPictureAllowed +import com.owncloud.android.lib.common.utils.Log_OC -private const val ASPECT_RATIO_WIDTH = 16 -private const val ASPECT_RATIO_HEIGHT = 9 -private const val MIN_ASPECT_RATIO = 0.42f -private const val MAX_ASPECT_RATIO = 2.39f +private const val TAG = "VideoPictureInPicture" +private val MIN_ASPECT_RATIO = Rational(41841, 100000) +private val MAX_ASPECT_RATIO = Rational(239, 100) -class VideoPictureInPicture(private val activity: Activity, private val playbackModel: PlaybackModel) { +class VideoPictureInPicture(private val activity: Activity) { - private val defaultAspectRatio = Rational(ASPECT_RATIO_WIDTH, ASPECT_RATIO_HEIGHT) + private var aspectRatio: Rational? = null + private var isAutoEnterEnabled = false val isAllowed: Boolean get() = activity.isPictureInPictureAllowed() - fun enter(sourceView: View): Boolean = isAllowed && activity.enterPictureInPictureMode(createParams(sourceView)) - - private fun createParams(sourceView: View): PictureInPictureParams { - val aspectRatio = getAspectRatio(playbackModel.state?.currentItemState?.videoSize) - return PictureInPictureParams.Builder().let { - it.setAspectRatio(aspectRatio) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - it.setAutoEnterEnabled(false) - } - it.setSourceRectHint(getSourceRectHint(sourceView, aspectRatio)) - it.build() - } + fun enter(): Boolean = isAllowed && activity.enterPictureInPictureMode(createParams()) + + fun enterOnUserLeave() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || !isAutoEnterEnabled) return + enter() + } + + fun update(videoSize: VideoSize?, isPlaying: Boolean) { + val newAspectRatio = videoSize?.toAspectRatio() ?: aspectRatio + if (newAspectRatio == aspectRatio && isPlaying == isAutoEnterEnabled) return + + aspectRatio = newAspectRatio + isAutoEnterEnabled = isPlaying + applyParams() } - private fun getAspectRatio(videoSize: VideoSize?): Rational { - if (videoSize == null) return defaultAspectRatio + fun disableAutoEnter() { + if (!isAutoEnterEnabled) return - val ratio = videoSize.width.toFloat() / videoSize.height.toFloat() - return if (ratio < MIN_ASPECT_RATIO || ratio > MAX_ASPECT_RATIO) { - defaultAspectRatio - } else { - Rational(videoSize.width, videoSize.height) + isAutoEnterEnabled = false + applyParams() + } + + private fun applyParams() { + if (!isAllowed) return + + try { + activity.setPictureInPictureParams(createParams()) + } catch (e: IllegalStateException) { + Log_OC.e(TAG, "setPictureInPictureParams failed", e) } } - private fun getSourceRectHint(sourceView: View, aspectRatio: Rational): Rect { - val sourceRect = Rect() - sourceView.getGlobalVisibleRect(sourceRect) - val heightHint = (sourceRect.width() / aspectRatio.toFloat()).toInt() - return Rect( - sourceRect.left, - sourceRect.top + (sourceRect.height() - heightHint) / 2, - sourceRect.right, - sourceRect.top + (sourceRect.height() + heightHint) / 2 - ) + private fun createParams(): PictureInPictureParams = PictureInPictureParams.Builder().apply { + aspectRatio?.let(::setAspectRatio) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + setAutoEnterEnabled(isAutoEnterEnabled) + } + }.build() + + private fun VideoSize.toAspectRatio(): Rational? { + if (width <= 0 || height <= 0) return null + return Rational(width, height).coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO) } } diff --git a/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewMediaActivity.kt b/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewMediaActivity.kt index 154248d0014a..e5cd14aaeb92 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewMediaActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewMediaActivity.kt @@ -111,7 +111,7 @@ class PreviewMediaActivity : private var isDownloadWorkStarted = false private var screenState = PreviewImageActivityState.Idle - private val pictureInPicture by lazy { VideoPictureInPicture(this, playbackModel) } + val pictureInPicture by lazy { VideoPictureInPicture(this) } private var wasActionBarVisibleBeforePictureInPicture = true private var keepPlaybackOnFinish = false @@ -563,7 +563,12 @@ class PreviewMediaActivity : viewPager?.setCurrentItem(position, true) } - fun enterPictureInPicture(): Boolean = viewPager?.let { pictureInPicture.enter(it) } == true + fun enterPictureInPicture(): Boolean = pictureInPicture.enter() + + override fun onUserLeaveHint() { + super.onUserLeaveHint() + pictureInPicture.enterOnUserLeave() + } override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) { super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) diff --git a/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewPlaybackFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewPlaybackFragment.kt index 2c30fb8b305b..3373d85a7c92 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewPlaybackFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewPlaybackFragment.kt @@ -16,6 +16,7 @@ import android.view.View import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.activity.addCallback +import androidx.activity.trackPipAnimationHintView import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.core.view.MenuProvider @@ -23,6 +24,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.player.media3.PlaybackModel import com.nextcloud.client.player.media3.PlaybackQueueLoader @@ -127,6 +129,7 @@ class PreviewPlaybackFragment : binding = PreviewPlaybackFragmentBinding.inflate(inflater, container, false) loadThumbnail() registerPictureInPictureOnBack() + trackPictureInPictureSourceRect() binding.playerControlView.navigator = activity as? MediaNavigator binding.playerControlView.addPaddingForNavBar() binding.playerControlView.isVisible = previewActivity?.isActionBarVisible != false @@ -257,6 +260,16 @@ class PreviewPlaybackFragment : } } + private fun trackPictureInPictureSourceRect() { + if (!MimeTypeUtil.isVideo(file)) return + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + requireActivity().trackPipAnimationHintView(binding.surfaceView) + } + } + } + override fun onStart() { super.onStart() playbackModel.addListener(this) @@ -274,6 +287,9 @@ class PreviewPlaybackFragment : override fun onPause() { pictureInPictureCallback?.isEnabled = false binding.playerControlView.onStop() + if (!isInPictureInPictureMode()) { + previewActivity?.pictureInPicture?.disableAutoEnter() + } if (!isInPictureInPictureMode() && isCurrentItem(playbackModel.state)) { playbackModel.pause() playbackModel.setVideoSurfaceView(null) @@ -339,6 +355,7 @@ class PreviewPlaybackFragment : if (isCurrentItem(state)) { wasCurrentItem = true showVideo(state?.currentItemState?.videoSize) + updatePictureInPicture(state) // Keep the screen on while playing. This works because it's called at every playback update and when // the surfaceview is not visible (scrolled away, app in background...) @@ -368,6 +385,14 @@ class PreviewPlaybackFragment : private fun isInPictureInPictureMode(): Boolean = activity?.isInPictureInPictureMode == true + private fun updatePictureInPicture(state: PlaybackState?) { + if (!MimeTypeUtil.isVideo(file)) return + if (!isResumed && !isInPictureInPictureMode()) return + + val itemState = state?.currentItemState + previewActivity?.pictureInPicture?.update(itemState?.videoSize, itemState?.playerState == PlayerState.PLAYING) + } + private fun toggleFullScreen() { val previewActivity = previewActivity ?: return binding.playerControlView.setVisibilityWithAnimation(!previewActivity.isActionBarVisible)