Add picture-in-picture helper classes.

This commit is contained in:
Isira Seneviratne 2023-02-03 17:09:08 +05:30
parent 9ad095f9d3
commit c513d7bd25
5 changed files with 206 additions and 81 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.Activity
import android.app.PendingIntent import android.app.PendingIntent
import android.app.RemoteAction
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.graphics.drawable.Icon
import android.os.Build
import android.view.accessibility.CaptioningManager import android.view.accessibility.CaptioningManager
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.app.RemoteActionCompat
import androidx.core.graphics.drawable.IconCompat
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.obj.PipedStream import com.github.libretube.api.obj.PipedStream
import com.github.libretube.api.obj.Segment import com.github.libretube.api.obj.Segment
import com.github.libretube.compat.PendingIntentCompat
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.enums.AudioQuality import com.github.libretube.enums.AudioQuality
import com.github.libretube.enums.PlayerEvent import com.github.libretube.enums.PlayerEvent
@ -378,26 +377,24 @@ object PlayerHelper {
return context.packageName + "." + ACTION_MEDIA_CONTROL return context.packageName + "." + ACTION_MEDIA_CONTROL
} }
@RequiresApi(Build.VERSION_CODES.O)
private fun getPendingIntent(activity: Activity, code: Int): PendingIntent { private fun getPendingIntent(activity: Activity, code: Int): PendingIntent {
return PendingIntent.getBroadcast( return PendingIntentCompat.getBroadcast(
activity, activity,
code, code,
Intent(getIntentActon(activity)).putExtra(CONTROL_TYPE, code), Intent(getIntentActon(activity)).putExtra(CONTROL_TYPE, code),
PendingIntent.FLAG_IMMUTABLE 0
) )
} }
@RequiresApi(Build.VERSION_CODES.O)
private fun getRemoteAction( private fun getRemoteAction(
activity: Activity, activity: Activity,
id: Int, id: Int,
@StringRes title: Int, @StringRes title: Int,
event: PlayerEvent event: PlayerEvent
): RemoteAction { ): RemoteActionCompat {
val text = activity.getString(title) val text = activity.getString(title)
return RemoteAction( return RemoteActionCompat(
Icon.createWithResource(activity, id), IconCompat.createWithResource(activity, id),
text, text,
text, text,
getPendingIntent(activity, event.value) getPendingIntent(activity, event.value)
@ -407,8 +404,7 @@ object PlayerHelper {
/** /**
* Create controls to use in the PiP window * Create controls to use in the PiP window
*/ */
@RequiresApi(Build.VERSION_CODES.O) fun getPiPModeActions(activity: Activity, isPlaying: Boolean): List<RemoteActionCompat> {
fun getPiPModeActions(activity: Activity, isPlaying: Boolean, isOfflinePlayer: Boolean = false): ArrayList<RemoteAction> {
val audioModeAction = getRemoteAction( val audioModeAction = getRemoteAction(
activity, activity,
R.drawable.ic_headphones, R.drawable.ic_headphones,
@ -443,12 +439,10 @@ object PlayerHelper {
R.string.forward, R.string.forward,
PlayerEvent.Forward PlayerEvent.Forward
) )
return if ( return if (alternativePiPControls) {
!isOfflinePlayer && alternativePiPControls listOf(audioModeAction, playPauseAction, skipNextAction)
) {
arrayListOf(audioModeAction, playPauseAction, skipNextAction)
} else { } else {
arrayListOf(rewindAction, playPauseAction, forwardAction) listOf(rewindAction, playPauseAction, forwardAction)
} }
} }

View File

@ -1,15 +1,15 @@
package com.github.libretube.ui.activities package com.github.libretube.ui.activities
import android.app.PictureInPictureParams
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.media.session.PlaybackState import android.media.session.PlaybackState
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope 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.constants.IntentData
import com.github.libretube.databinding.ActivityOfflinePlayerBinding import com.github.libretube.databinding.ActivityOfflinePlayerBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding 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.PlayerHelper.loadPlaybackParams
import com.github.libretube.helpers.WindowHelper import com.github.libretube.helpers.WindowHelper
import com.github.libretube.ui.base.BaseActivity import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setAspectRatio
import com.github.libretube.ui.models.PlayerViewModel import com.github.libretube.ui.models.PlayerViewModel
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
@ -197,18 +196,14 @@ class OfflinePlayerActivity : BaseActivity() {
} }
override fun onUserLeaveHint() { override fun onUserLeaveHint() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return if (PlayerHelper.pipEnabled && player.playbackState != PlaybackState.STATE_PAUSED) {
PictureInPictureCompat.enterPictureInPictureMode(
if (!PlayerHelper.pipEnabled) return this,
PictureInPictureParamsCompat.Builder()
if (player.playbackState == PlaybackState.STATE_PAUSED) return .setAspectRatio(player.videoSize)
.build()
enterPictureInPictureMode( )
PictureInPictureParams.Builder() }
.setActions(emptyList())
.setAspectRatio(player.videoSize.width, player.videoSize.height)
.build()
)
super.onUserLeaveHint() super.onUserLeaveHint()
} }

View File

@ -1,7 +1,6 @@
package com.github.libretube.ui.fragments package com.github.libretube.ui.fragments
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PictureInPictureParams
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -11,7 +10,6 @@ import android.content.res.Configuration
import android.media.session.PlaybackState import android.media.session.PlaybackState
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@ -25,7 +23,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.bundleOf 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.Segment
import com.github.libretube.api.obj.StreamItem import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Streams 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.IntentData
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentPlayerBinding 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.DownloadDialog
import com.github.libretube.ui.dialogs.ShareDialog import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.dialogs.StatsDialog 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.extensions.setupSubscriptionButton
import com.github.libretube.ui.interfaces.OnlinePlayerOptions import com.github.libretube.ui.interfaces.OnlinePlayerOptions
import com.github.libretube.ui.listeners.SeekbarPreviewListener import com.github.libretube.ui.listeners.SeekbarPreviewListener
@ -344,9 +342,11 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
binding.playerMotionLayout.progress = 1.toFloat() binding.playerMotionLayout.progress = 1.toFloat()
binding.playerMotionLayout.transitionToStart() 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 binding.relPlayerPip.visibility = View.GONE
} }
} }
@ -618,11 +618,10 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
} }
private fun disableAutoPiP() { private fun disableAutoPiP() {
if (SDK_INT < Build.VERSION_CODES.S) { // autoEnterEnabled is false by default
return PictureInPictureCompat.setPictureInPictureParams(
} requireActivity(),
activity?.setPictureInPictureParams( PictureInPictureParamsCompat.Builder().build()
PictureInPictureParams.Builder().setAutoEnterEnabled(false).build()
) )
} }
@ -727,7 +726,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
if (binding.playerMotionLayout.progress != 1.0f) { if (binding.playerMotionLayout.progress != 1.0f) {
// show controllers when not in picture in picture mode // 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 binding.player.useController = true
} }
} }
@ -743,13 +744,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 * fetch the segments for SponsorBlock
*/ */
@ -911,7 +905,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
// Listener for play and pause icon change // Listener for play and pause icon change
exoPlayer.addListener(object : Player.Listener { exoPlayer.addListener(object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
if (usePiP()) activity?.setPictureInPictureParams(getPipParams()) if (PlayerHelper.pipEnabled) {
PictureInPictureCompat.setPictureInPictureParams(requireActivity(), pipParams)
}
if (isPlaying) { if (isPlaying) {
// Stop [BackgroundMode] service if it is running. // Stop [BackgroundMode] service if it is running.
@ -962,17 +958,22 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
} }
} }
val activity = requireActivity()
if (playbackState == Player.STATE_READY) { if (playbackState == Player.STATE_READY) {
// media actually playing // media actually playing
transitioning = false transitioning = false
// update the PiP params to use the correct aspect ratio // 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 // 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 // finish PiP by finishing the activity
if (activity?.isInPictureInPictureMode!!) activity?.finish() activity.finish()
} }
super.onPlaybackStateChanged(playbackState) super.onPlaybackStateChanged(playbackState)
} }
@ -1003,12 +1004,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
} }
binding.relPlayerPip.setOnClickListener { binding.relPlayerPip.setOnClickListener {
if (SDK_INT < Build.VERSION_CODES.O) return@setOnClickListener PictureInPictureCompat.enterPictureInPictureMode(requireActivity(), pipParams)
try {
activity?.enterPictureInPictureMode(getPipParams())
} catch (e: Exception) {
e.printStackTrace()
}
} }
initializeRelatedVideos(streams.relatedStreams.filter { !it.title.isNullOrBlank() }) initializeRelatedVideos(streams.relatedStreams.filter { !it.title.isNullOrBlank() })
// set video description // set video description
@ -1500,25 +1496,25 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
} }
fun onUserLeaveHint() { fun onUserLeaveHint() {
if (usePiP() && shouldStartPiP()) { if (PlayerHelper.pipEnabled && shouldStartPiP()) {
activity?.enterPictureInPictureMode(getPipParams()) PictureInPictureCompat.enterPictureInPictureMode(requireActivity(), pipParams)
return return
} }
if (PlayerHelper.pauseOnQuit) exoPlayer.pause() if (PlayerHelper.pauseOnQuit) {
exoPlayer.pause()
}
} }
@RequiresApi(Build.VERSION_CODES.O) private val pipParams
fun getPipParams(): PictureInPictureParams = PictureInPictureParams.Builder() get() = PictureInPictureParamsCompat.Builder()
.setActions(PlayerHelper.getPiPModeActions(requireActivity(), exoPlayer.isPlaying)) .setActions(PlayerHelper.getPiPModeActions(requireActivity(), exoPlayer.isPlaying))
.apply { .setAutoEnterEnabled(PlayerHelper.pipEnabled)
if (SDK_INT >= Build.VERSION_CODES.S && PlayerHelper.pipEnabled) { .apply {
setAutoEnterEnabled(true) if (exoPlayer.isPlaying) {
setAspectRatio(exoPlayer.videoSize)
}
} }
if (exoPlayer.isPlaying) { .build()
setAspectRatio(exoPlayer.videoSize.width, exoPlayer.videoSize.height)
}
}
.build()
private fun setupSeekbarPreview() { private fun setupSeekbarPreview() {
playerBinding.seekbarPreview.visibility = View.GONE playerBinding.seekbarPreview.visibility = View.GONE
@ -1532,7 +1528,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
} }
private fun shouldStartPiP(): Boolean { 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 return false
} }
@ -1557,10 +1553,12 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
if (!PlayerHelper.autoRotationEnabled) return if (!PlayerHelper.autoRotationEnabled ||
// If in PiP mode, orientation is given as landscape.
// If in PiP mode, orientation is given as landscape. PictureInPictureCompat.isInPictureInPictureMode(requireActivity())
if (SDK_INT >= Build.VERSION_CODES.N && activity?.isInPictureInPictureMode == true) return ) {
return
}
when (newConfig.orientation) { when (newConfig.orientation) {
// go to fullscreen mode // go to fullscreen mode