Merge pull request #3411 from Isira-Seneviratne/Pip_compat

Add picture-in-picture helper classes.
This commit is contained in:
Bnyro 2023-03-28 07:12:14 +02:00 committed by GitHub
commit 29a855d415
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 206 additions and 103 deletions

View File

@ -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())
}
}
}

View File

@ -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<RemoteActionCompat>,
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<RemoteActionCompat> = 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<RemoteActionCompat>) = 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
)
}
}
}

View File

@ -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<RemoteAction> {
fun getPiPModeActions(activity: Activity, isPlaying: Boolean): List<RemoteActionCompat> {
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)
}
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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