diff --git a/app/src/main/java/com/github/libretube/compat/PictureInPictureCompat.kt b/app/src/main/java/com/github/libretube/compat/PictureInPictureCompat.kt new file mode 100644 index 000000000..6c0b4e78d --- /dev/null +++ b/app/src/main/java/com/github/libretube/compat/PictureInPictureCompat.kt @@ -0,0 +1,22 @@ +package com.github.libretube.compat + +import android.app.Activity +import android.os.Build + +object PictureInPictureCompat { + fun isInPictureInPictureMode(activity: Activity): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity.isInPictureInPictureMode + } + + fun setPictureInPictureParams(activity: Activity, params: PictureInPictureParamsCompat) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + activity.setPictureInPictureParams(params.toPictureInPictureParams()) + } + } + + fun enterPictureInPictureMode(activity: Activity, params: PictureInPictureParamsCompat) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + activity.enterPictureInPictureMode(params.toPictureInPictureParams()) + } + } +} diff --git a/app/src/main/java/com/github/libretube/compat/PictureInPictureParamsCompat.kt b/app/src/main/java/com/github/libretube/compat/PictureInPictureParamsCompat.kt new file mode 100644 index 000000000..317dbaf22 --- /dev/null +++ b/app/src/main/java/com/github/libretube/compat/PictureInPictureParamsCompat.kt @@ -0,0 +1,116 @@ +package com.github.libretube.compat + +import android.app.PictureInPictureParams +import android.graphics.Rect +import android.os.Build +import android.util.Rational +import androidx.annotation.RequiresApi +import androidx.core.app.RemoteActionCompat +import com.google.android.exoplayer2.video.VideoSize + +class PictureInPictureParamsCompat private constructor( + private val autoEnterEnabled: Boolean, + private val seamlessResizeEnabled: Boolean, + private val closeAction: RemoteActionCompat?, + private val actions: List, + private val sourceRectHint: Rect?, + private val title: CharSequence?, + private val subtitle: CharSequence?, + private val aspectRatio: Rational?, + private val expandedAspectRatio: Rational? +) { + @RequiresApi(Build.VERSION_CODES.O) + fun toPictureInPictureParams(): PictureInPictureParams { + val pipParams = PictureInPictureParams.Builder() + .setSourceRectHint(sourceRectHint) + .setActions(actions.map { it.toRemoteAction() }) + .setAspectRatio(aspectRatio) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + pipParams.setAutoEnterEnabled(autoEnterEnabled) + .setSeamlessResizeEnabled(seamlessResizeEnabled) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pipParams.setTitle(title) + .setSubtitle(subtitle) + .setCloseAction(closeAction?.toRemoteAction()) + .setExpandedAspectRatio(expandedAspectRatio) + } + + return pipParams.build() + } + + class Builder { + private var autoEnterEnabled = false + private var seamlessResizeEnabled = true + private var closeAction: RemoteActionCompat? = null + private var actions: List = emptyList() + private var sourceRectHint: Rect? = null + private var title: CharSequence? = null + private var subtitle: CharSequence? = null + private var aspectRatio: Rational? = null + private var expandedAspectRatio: Rational? = null + + fun setAutoEnterEnabled(autoEnterEnabled: Boolean) = apply { + this.autoEnterEnabled = autoEnterEnabled + } + + fun setSeamlessResizeEnabled(seamlessResizeEnabled: Boolean) = apply { + this.seamlessResizeEnabled = seamlessResizeEnabled + } + + fun setCloseAction(action: RemoteActionCompat?) = apply { + this.closeAction = action + } + + fun setActions(actions: List) = apply { + this.actions = actions + } + + fun setSourceRectHint(sourceRectHint: Rect?) = apply { + this.sourceRectHint = sourceRectHint + } + + fun setTitle(title: CharSequence?) = apply { + this.title = title + } + + fun setSubtitle(subtitle: CharSequence?) = apply { + this.subtitle = subtitle + } + + fun setAspectRatio(aspectRatio: Rational?) = apply { + this.aspectRatio = aspectRatio + } + + // Additional function replacing the project's extension function for the platform builder. + fun setAspectRatio(videoSize: VideoSize): Builder { + val ratio = (videoSize.width.toFloat() / videoSize.height) + val rational = when { + ratio.isNaN() -> Rational(4, 3) + ratio <= 0.418410 -> Rational(41841, 100000) + ratio >= 2.390000 -> Rational(239, 100) + else -> Rational(videoSize.width, videoSize.height) + } + return setAspectRatio(rational) + } + + fun setExpandedAspectRatio(expandedAspectRatio: Rational?) = apply { + this.expandedAspectRatio = expandedAspectRatio + } + + fun build(): PictureInPictureParamsCompat { + return PictureInPictureParamsCompat( + autoEnterEnabled, + seamlessResizeEnabled, + closeAction, + actions, + sourceRectHint, + title, + subtitle, + aspectRatio, + expandedAspectRatio + ) + } + } +} diff --git a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt index e7545c7c4..f370f37ca 100644 --- a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt @@ -2,19 +2,18 @@ package com.github.libretube.helpers import android.app.Activity import android.app.PendingIntent -import android.app.RemoteAction import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo -import android.graphics.drawable.Icon -import android.os.Build import android.view.accessibility.CaptioningManager import android.widget.Toast -import androidx.annotation.RequiresApi import androidx.annotation.StringRes +import androidx.core.app.RemoteActionCompat +import androidx.core.graphics.drawable.IconCompat import com.github.libretube.R import com.github.libretube.api.obj.PipedStream import com.github.libretube.api.obj.Segment +import com.github.libretube.compat.PendingIntentCompat import com.github.libretube.constants.PreferenceKeys import com.github.libretube.enums.AudioQuality import com.github.libretube.enums.PlayerEvent @@ -378,26 +377,24 @@ object PlayerHelper { return context.packageName + "." + ACTION_MEDIA_CONTROL } - @RequiresApi(Build.VERSION_CODES.O) private fun getPendingIntent(activity: Activity, code: Int): PendingIntent { - return PendingIntent.getBroadcast( + return PendingIntentCompat.getBroadcast( activity, code, Intent(getIntentActon(activity)).putExtra(CONTROL_TYPE, code), - PendingIntent.FLAG_IMMUTABLE + 0 ) } - @RequiresApi(Build.VERSION_CODES.O) private fun getRemoteAction( activity: Activity, id: Int, @StringRes title: Int, event: PlayerEvent - ): RemoteAction { + ): RemoteActionCompat { val text = activity.getString(title) - return RemoteAction( - Icon.createWithResource(activity, id), + return RemoteActionCompat( + IconCompat.createWithResource(activity, id), text, text, getPendingIntent(activity, event.value) @@ -407,8 +404,7 @@ object PlayerHelper { /** * Create controls to use in the PiP window */ - @RequiresApi(Build.VERSION_CODES.O) - fun getPiPModeActions(activity: Activity, isPlaying: Boolean, isOfflinePlayer: Boolean = false): ArrayList { + fun getPiPModeActions(activity: Activity, isPlaying: Boolean): List { val audioModeAction = getRemoteAction( activity, R.drawable.ic_headphones, @@ -443,12 +439,10 @@ object PlayerHelper { R.string.forward, PlayerEvent.Forward ) - return if ( - !isOfflinePlayer && alternativePiPControls - ) { - arrayListOf(audioModeAction, playPauseAction, skipNextAction) + return if (alternativePiPControls) { + listOf(audioModeAction, playPauseAction, skipNextAction) } else { - arrayListOf(rewindAction, playPauseAction, forwardAction) + listOf(rewindAction, playPauseAction, forwardAction) } } 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 ac85c0e8d..53e4d972b 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 @@ -1,15 +1,15 @@ package com.github.libretube.ui.activities -import android.app.PictureInPictureParams import android.content.pm.ActivityInfo import android.media.session.PlaybackState import android.net.Uri -import android.os.Build import android.os.Bundle import android.text.format.DateUtils import android.view.View import androidx.activity.viewModels import androidx.lifecycle.lifecycleScope +import com.github.libretube.compat.PictureInPictureCompat +import com.github.libretube.compat.PictureInPictureParamsCompat import com.github.libretube.constants.IntentData import com.github.libretube.databinding.ActivityOfflinePlayerBinding import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding @@ -21,7 +21,6 @@ import com.github.libretube.helpers.PlayerHelper import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams import com.github.libretube.helpers.WindowHelper import com.github.libretube.ui.base.BaseActivity -import com.github.libretube.ui.extensions.setAspectRatio import com.github.libretube.ui.models.PlayerViewModel import com.google.android.exoplayer2.C import com.google.android.exoplayer2.ExoPlayer @@ -197,18 +196,14 @@ class OfflinePlayerActivity : BaseActivity() { } override fun onUserLeaveHint() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return - - if (!PlayerHelper.pipEnabled) return - - if (player.playbackState == PlaybackState.STATE_PAUSED) return - - enterPictureInPictureMode( - PictureInPictureParams.Builder() - .setActions(emptyList()) - .setAspectRatio(player.videoSize.width, player.videoSize.height) - .build() - ) + if (PlayerHelper.pipEnabled && player.playbackState != PlaybackState.STATE_PAUSED) { + PictureInPictureCompat.enterPictureInPictureMode( + this, + PictureInPictureParamsCompat.Builder() + .setAspectRatio(player.videoSize) + .build() + ) + } super.onUserLeaveHint() } diff --git a/app/src/main/java/com/github/libretube/ui/extensions/SetAspectRatio.kt b/app/src/main/java/com/github/libretube/ui/extensions/SetAspectRatio.kt deleted file mode 100644 index df1ebbf94..000000000 --- a/app/src/main/java/com/github/libretube/ui/extensions/SetAspectRatio.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.libretube.ui.extensions - -import android.app.PictureInPictureParams -import android.os.Build -import android.util.Rational -import androidx.annotation.RequiresApi - -@RequiresApi(Build.VERSION_CODES.O) -fun PictureInPictureParams.Builder.setAspectRatio( - width: Int, - height: Int -): PictureInPictureParams.Builder { - val ratio = (width.toFloat() / height).let { - when { - it.isNaN() -> Rational(4, 3) - it <= 0.418410 -> Rational(41841, 100000) - it >= 2.390000 -> Rational(239, 100) - else -> Rational(width, height) - } - } - return setAspectRatio(ratio) -} 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 88e66b204..067d38387 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,7 +1,6 @@ package com.github.libretube.ui.fragments import android.annotation.SuppressLint -import android.app.PictureInPictureParams import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -11,7 +10,6 @@ import android.content.res.Configuration import android.media.session.PlaybackState import android.net.Uri import android.os.Build -import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.os.Handler import android.os.Looper @@ -25,7 +23,6 @@ import android.view.View import android.view.ViewGroup import android.widget.TextView import android.widget.Toast -import androidx.annotation.RequiresApi import androidx.constraintlayout.motion.widget.MotionLayout import androidx.core.net.toUri import androidx.core.os.bundleOf @@ -49,6 +46,8 @@ import com.github.libretube.api.obj.PipedStream import com.github.libretube.api.obj.Segment import com.github.libretube.api.obj.StreamItem import com.github.libretube.api.obj.Streams +import com.github.libretube.compat.PictureInPictureCompat +import com.github.libretube.compat.PictureInPictureParamsCompat import com.github.libretube.constants.IntentData import com.github.libretube.constants.PreferenceKeys import com.github.libretube.databinding.FragmentPlayerBinding @@ -82,7 +81,6 @@ import com.github.libretube.ui.dialogs.AddToPlaylistDialog import com.github.libretube.ui.dialogs.DownloadDialog import com.github.libretube.ui.dialogs.ShareDialog import com.github.libretube.ui.dialogs.StatsDialog -import com.github.libretube.ui.extensions.setAspectRatio import com.github.libretube.ui.extensions.setupSubscriptionButton import com.github.libretube.ui.interfaces.OnlinePlayerOptions import com.github.libretube.ui.listeners.SeekbarPreviewListener @@ -346,9 +344,11 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { binding.playerMotionLayout.progress = 1.toFloat() binding.playerMotionLayout.transitionToStart() - if (usePiP()) activity?.setPictureInPictureParams(getPipParams()) + if (PlayerHelper.pipEnabled) { + PictureInPictureCompat.setPictureInPictureParams(requireActivity(), pipParams) + } - if (SDK_INT < Build.VERSION_CODES.O) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { binding.relPlayerPip.visibility = View.GONE } } @@ -620,11 +620,10 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } private fun disableAutoPiP() { - if (SDK_INT < Build.VERSION_CODES.S) { - return - } - activity?.setPictureInPictureParams( - PictureInPictureParams.Builder().setAutoEnterEnabled(false).build() + // autoEnterEnabled is false by default + PictureInPictureCompat.setPictureInPictureParams( + requireActivity(), + PictureInPictureParamsCompat.Builder().build() ) } @@ -729,7 +728,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { if (binding.playerMotionLayout.progress != 1.0f) { // show controllers when not in picture in picture mode - if (!(usePiP() && activity?.isInPictureInPictureMode!!)) { + val inPipMode = PlayerHelper.pipEnabled && + PictureInPictureCompat.isInPictureInPictureMode(requireActivity()) + if (!inPipMode) { binding.player.useController = true } } @@ -745,13 +746,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } } - /** - * Detect whether PiP is supported and enabled - */ - private fun usePiP(): Boolean { - return SDK_INT >= Build.VERSION_CODES.O && PlayerHelper.pipEnabled - } - /** * fetch the segments for SponsorBlock */ @@ -913,7 +907,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { // Listener for play and pause icon change exoPlayer.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { - if (usePiP()) activity?.setPictureInPictureParams(getPipParams()) + if (PlayerHelper.pipEnabled) { + PictureInPictureCompat.setPictureInPictureParams(requireActivity(), pipParams) + } if (isPlaying) { // Stop [BackgroundMode] service if it is running. @@ -964,17 +960,22 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } } + val activity = requireActivity() if (playbackState == Player.STATE_READY) { // media actually playing transitioning = false // update the PiP params to use the correct aspect ratio - if (usePiP()) activity?.setPictureInPictureParams(getPipParams()) + if (PlayerHelper.pipEnabled) { + PictureInPictureCompat.setPictureInPictureParams(activity, pipParams) + } } // listen for the stop button in the notification - if (playbackState == PlaybackState.STATE_STOPPED && usePiP()) { + if (playbackState == PlaybackState.STATE_STOPPED && PlayerHelper.pipEnabled && + PictureInPictureCompat.isInPictureInPictureMode(activity) + ) { // finish PiP by finishing the activity - if (activity?.isInPictureInPictureMode!!) activity?.finish() + activity.finish() } super.onPlaybackStateChanged(playbackState) } @@ -1005,12 +1006,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } binding.relPlayerPip.setOnClickListener { - if (SDK_INT < Build.VERSION_CODES.O) return@setOnClickListener - try { - activity?.enterPictureInPictureMode(getPipParams()) - } catch (e: Exception) { - e.printStackTrace() - } + PictureInPictureCompat.enterPictureInPictureMode(requireActivity(), pipParams) } initializeRelatedVideos(streams.relatedStreams.filter { !it.title.isNullOrBlank() }) // set video description @@ -1502,25 +1498,25 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } fun onUserLeaveHint() { - if (usePiP() && shouldStartPiP()) { - activity?.enterPictureInPictureMode(getPipParams()) + if (PlayerHelper.pipEnabled && shouldStartPiP()) { + PictureInPictureCompat.enterPictureInPictureMode(requireActivity(), pipParams) return } - if (PlayerHelper.pauseOnQuit) exoPlayer.pause() + if (PlayerHelper.pauseOnQuit) { + exoPlayer.pause() + } } - @RequiresApi(Build.VERSION_CODES.O) - fun getPipParams(): PictureInPictureParams = PictureInPictureParams.Builder() - .setActions(PlayerHelper.getPiPModeActions(requireActivity(), exoPlayer.isPlaying)) - .apply { - if (SDK_INT >= Build.VERSION_CODES.S && PlayerHelper.pipEnabled) { - setAutoEnterEnabled(true) + private val pipParams + get() = PictureInPictureParamsCompat.Builder() + .setActions(PlayerHelper.getPiPModeActions(requireActivity(), exoPlayer.isPlaying)) + .setAutoEnterEnabled(PlayerHelper.pipEnabled) + .apply { + if (exoPlayer.isPlaying) { + setAspectRatio(exoPlayer.videoSize) + } } - if (exoPlayer.isPlaying) { - setAspectRatio(exoPlayer.videoSize.width, exoPlayer.videoSize.height) - } - } - .build() + .build() private fun setupSeekbarPreview() { playerBinding.seekbarPreview.visibility = View.GONE @@ -1534,7 +1530,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } private fun shouldStartPiP(): Boolean { - if (!PlayerHelper.pipEnabled || SDK_INT >= Build.VERSION_CODES.S) { + if (!PlayerHelper.pipEnabled || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return false } @@ -1559,10 +1555,12 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - if (!PlayerHelper.autoRotationEnabled) return - - // If in PiP mode, orientation is given as landscape. - if (SDK_INT >= Build.VERSION_CODES.N && activity?.isInPictureInPictureMode == true) return + if (!PlayerHelper.autoRotationEnabled || + // If in PiP mode, orientation is given as landscape. + PictureInPictureCompat.isInPictureInPictureMode(requireActivity()) + ) { + return + } when (newConfig.orientation) { // go to fullscreen mode