diff --git a/app/src/main/java/com/github/libretube/enums/PlayerEvent.kt b/app/src/main/java/com/github/libretube/enums/PlayerEvent.kt new file mode 100644 index 000000000..9479e309c --- /dev/null +++ b/app/src/main/java/com/github/libretube/enums/PlayerEvent.kt @@ -0,0 +1,14 @@ +package com.github.libretube.enums + +enum class PlayerEvent(val value: Int) { + Pause(0), + Play(1), + Forward(2), + Rewind(3), + Next(5), + Prev(6); + + companion object { + fun fromInt(value: Int) = PlayerEvent.values().first { it.value == value } + } +} 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 67b730f6e..c11f5ba59 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 @@ -2,8 +2,10 @@ 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 +import android.content.IntentFilter import android.content.pm.ActivityInfo import android.content.res.Configuration import android.media.session.PlaybackState @@ -50,6 +52,7 @@ import com.github.libretube.databinding.PlayerGestureControlsViewBinding import com.github.libretube.db.DatabaseHelper import com.github.libretube.db.DatabaseHolder.Companion.Database import com.github.libretube.db.obj.WatchPosition +import com.github.libretube.enums.PlayerEvent import com.github.libretube.enums.ShareObjectType import com.github.libretube.extensions.TAG import com.github.libretube.extensions.awaitQuery @@ -107,15 +110,15 @@ import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.upstream.DefaultDataSource import com.google.android.exoplayer2.util.MimeTypes import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.io.IOException +import java.util.* +import java.util.concurrent.Executors +import kotlin.math.abs import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.chromium.net.CronetEngine import retrofit2.HttpException -import java.io.IOException -import java.util.* -import java.util.concurrent.Executors -import kotlin.math.abs class PlayerFragment : BaseFragment(), OnlinePlayerOptions { @@ -178,6 +181,28 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { val handler = Handler(Looper.getMainLooper()) + private val broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val action = intent?.getIntExtra(PlayerHelper.CONTROL_TYPE, 0) ?: return + when (PlayerEvent.fromInt(action)) { + PlayerEvent.Play -> { + exoPlayer.play() + } + PlayerEvent.Pause -> { + exoPlayer.pause() + } + PlayerEvent.Forward -> { + exoPlayer.seekTo(exoPlayer.currentPosition + PlayerHelper.seekIncrement) + } + PlayerEvent.Rewind -> { + exoPlayer.seekTo(exoPlayer.currentPosition - PlayerHelper.seekIncrement) + } + else -> { + } + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { @@ -186,6 +211,12 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { channelId = it.getString(IntentData.channelId) keepQueue = it.getBoolean(IntentData.keepQueue, false) } + + // broadcast receiver for PiP actions + context?.registerReceiver( + broadcastReceiver, + IntentFilter(PlayerHelper.getIntentActon(requireContext())) + ) } override fun onCreateView( @@ -529,6 +560,9 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { // disable the auto PiP mode for SDK >= 32 disableAutoPiP() + // unregister the receiver for player actions + context?.unregisterReceiver(broadcastReceiver) + saveWatchPosition() // clear the playing queue and release the player @@ -794,7 +828,9 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { private fun localizedDate(date: String?): String? { return if (SDK_INT >= Build.VERSION_CODES.N) { TextUtils.localizeDate(date, resources.configuration.locales[0]) - } else TextUtils.localizeDate(date) + } else { + TextUtils.localizeDate(date) + } } private fun handleLiveVideo() { @@ -855,10 +891,11 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { // Listener for play and pause icon change exoPlayer.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { + if (usePiP()) activity?.setPictureInPictureParams(getPipParams()) + if (isPlaying) { // Stop [BackgroundMode] service if it is running. BackgroundHelper.stopBackgroundPlay(requireContext()) - if (usePiP()) activity?.setPictureInPictureParams(getPipParams()) } else { disableAutoPiP() } @@ -1169,8 +1206,8 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { for (vid in videoStreams) { if (resolutions.any { - it.resolution == vid.quality.qualityToInt() - } || vid.url == null + it.resolution == vid.quality.qualityToInt() + } || vid.url == null ) { continue } @@ -1460,7 +1497,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions { @RequiresApi(Build.VERSION_CODES.O) fun getPipParams(): PictureInPictureParams = PictureInPictureParams.Builder() - .setActions(emptyList()) + .setActions(PlayerHelper.getPIPModeActions(requireActivity(), exoPlayer.isPlaying)) .apply { if (SDK_INT >= Build.VERSION_CODES.S) { setAutoEnterEnabled(true) diff --git a/app/src/main/java/com/github/libretube/util/PlayerHelper.kt b/app/src/main/java/com/github/libretube/util/PlayerHelper.kt index cf54fd0c4..202720a9f 100644 --- a/app/src/main/java/com/github/libretube/util/PlayerHelper.kt +++ b/app/src/main/java/com/github/libretube/util/PlayerHelper.kt @@ -1,16 +1,29 @@ package com.github.libretube.util +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 androidx.annotation.RequiresApi +import androidx.annotation.StringRes +import com.github.libretube.R import com.github.libretube.api.obj.PipedStream import com.github.libretube.constants.PreferenceKeys import com.github.libretube.enums.AudioQuality +import com.github.libretube.enums.PlayerEvent import com.google.android.exoplayer2.ui.CaptionStyleCompat import com.google.android.exoplayer2.video.VideoSize import kotlin.math.roundToInt object PlayerHelper { + private const val ACTION_MEDIA_CONTROL = "media_control" + const val CONTROL_TYPE = "control_type" + /** * Get the audio source following the users preferences */ @@ -43,7 +56,7 @@ object PlayerHelper { /** * Get the best or worst bitrate from a list of audio streams * @param audios list of the audio streams - * @param getLeast whether the least bitrate should be returned + * @param quality Whether to use the best or worst quality available * @return Url of the audio source */ private fun getBitRate(audios: List, quality: AudioQuality): String { @@ -332,4 +345,74 @@ object PlayerHelper { ) } } + + fun getIntentActon(context: Context): String { + return context.packageName + "." + ACTION_MEDIA_CONTROL + } + + private fun getPendingIntent(activity: Activity, code: Int): PendingIntent { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.getBroadcast( + activity, + code, + Intent(getIntentActon(activity)).putExtra(CONTROL_TYPE, code), + PendingIntent.FLAG_IMMUTABLE + ) + } else { + PendingIntent.getBroadcast( + activity, + code, + Intent(getIntentActon(activity)).putExtra(CONTROL_TYPE, code), + 0 + ) + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun getRemoteAction( + activity: Activity, + id: Int, + @StringRes title: Int, + event: PlayerEvent + ): RemoteAction { + val text = activity.getString(title) + return RemoteAction( + Icon.createWithResource(activity, id), + text, + text, + getPendingIntent(activity, event.value) + ) + } + + @RequiresApi(Build.VERSION_CODES.O) + fun getPIPModeActions(activity: Activity, isPlaying: Boolean): ArrayList { + val actions: ArrayList = ArrayList() + actions.add( + getRemoteAction( + activity, + R.drawable.ic_rewind, + R.string.rewind, + PlayerEvent.Rewind + ) + ) + + actions.add( + getRemoteAction( + activity, + if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play, + R.string.pause, + if (isPlaying) PlayerEvent.Pause else PlayerEvent.Play + ) + ) + + actions.add( + getRemoteAction( + activity, + R.drawable.ic_forward, + R.string.forward, + PlayerEvent.Forward + ) + ) + return actions + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c3ca4b956..b290994e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -425,6 +425,9 @@ Shuffle Add to bookmarks Remove bookmark + Rewind + Forward + Pause Download Service