From 78a67a452134329cea529771a1506f16a748d74c Mon Sep 17 00:00:00 2001 From: Anil Kumar Beesetti <66936649+anilbeesetti@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:46:01 +0530 Subject: [PATCH] Fix: Minor pip and background play issues (#1168) --- .../feature/player/PlayerActivity.kt | 116 +++++++++++++++--- .../feature/player/PlayerViewModel.kt | 3 +- .../feature/player/extensions/Activity.kt | 28 ----- .../feature/player/extensions/VideoSize.kt | 5 +- .../feature/player/service/PlayerService.kt | 5 +- .../player/src/main/res/values/drawables.xml | 2 + 6 files changed, 110 insertions(+), 49 deletions(-) diff --git a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerActivity.kt b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerActivity.kt index bec2fffb8..c4604709c 100644 --- a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerActivity.kt +++ b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerActivity.kt @@ -3,16 +3,21 @@ package dev.anilbeesetti.nextplayer.feature.player import android.annotation.SuppressLint import android.app.Activity import android.app.AppOpsManager +import android.app.PendingIntent import android.app.PictureInPictureParams +import android.app.RemoteAction +import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.res.Configuration import android.graphics.Color import android.graphics.Rect import android.graphics.Typeface +import android.graphics.drawable.Icon import android.media.AudioManager import android.media.audiofx.LoudnessEnhancer import android.net.Uri @@ -35,6 +40,7 @@ import android.widget.Toast import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts.OpenDocument import androidx.activity.viewModels +import androidx.annotation.DrawableRes import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate @@ -73,7 +79,6 @@ import dev.anilbeesetti.nextplayer.feature.player.dialogs.nameRes import dev.anilbeesetti.nextplayer.feature.player.extensions.audioSessionId import dev.anilbeesetti.nextplayer.feature.player.extensions.isPortrait import dev.anilbeesetti.nextplayer.feature.player.extensions.next -import dev.anilbeesetti.nextplayer.feature.player.extensions.prettyPrintIntent import dev.anilbeesetti.nextplayer.feature.player.extensions.seekBack import dev.anilbeesetti.nextplayer.feature.player.extensions.seekForward import dev.anilbeesetti.nextplayer.feature.player.extensions.setImageDrawable @@ -139,6 +144,7 @@ class PlayerActivity : AppCompatActivity() { private lateinit var volumeManager: VolumeManager private lateinit var brightnessManager: BrightnessManager var loudnessEnhancer: LoudnessEnhancer? = null + private var pipBroadcastReceiver: BroadcastReceiver? = null /** * Listeners @@ -198,7 +204,6 @@ class PlayerActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - prettyPrintIntent() AppCompatDelegate.setDefaultNightMode( when (applicationPreferences.themeConfig) { @@ -339,7 +344,7 @@ class PlayerActivity : AppCompatActivity() { volumeManager.loudnessEnhancer = loudnessEnhancer if (intent.data != null && intent.data.toString() != currentMediaItem?.mediaId) { - playVideo(uri = viewModel.currentMediaItem?.localConfiguration?.uri ?: intent.data!!) + playVideo(uri = viewModel.currentMediaItemUri ?: intent.data!!) } } subtitleFileLauncherLaunchedForMediaItem = null @@ -381,6 +386,7 @@ class PlayerActivity : AppCompatActivity() { } } + @SuppressLint("NewApi") override fun onUserLeaveHint() { super.onUserLeaveHint() if (Build.VERSION.SDK_INT in Build.VERSION_CODES.O.. mediaController?.play() + PIP_ACTION_PAUSE -> mediaController?.pause() + PIP_ACTION_NEXT -> mediaController?.seekToNext() + PIP_ACTION_PREVIOUS -> mediaController?.seekToPrevious() + } + updatePictureInPictureParams() + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(pipBroadcastReceiver, IntentFilter(PIP_INTENT_ACTION), RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(pipBroadcastReceiver, IntentFilter(PIP_INTENT_ACTION)) + } } else { binding.playerView.subtitleView?.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, playerPreferences.subtitleTextSize.toFloat()) if (!isControlsLocked) { playerUnlockControls.visibility = View.VISIBLE } + pipBroadcastReceiver?.let { + unregisterReceiver(it) + pipBroadcastReceiver = null + } } super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) } @@ -423,8 +450,41 @@ class PlayerActivity : AppCompatActivity() { setSourceRectHint(sourceRectHint) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + setSeamlessResizeEnabled(playerPreferences.autoPip && enableAutoEnter) setAutoEnterEnabled(playerPreferences.autoPip && enableAutoEnter) } + + setActions( + listOf( + createPipAction( + context = this@PlayerActivity, + "skip to previous", + coreUiR.drawable.ic_skip_prev, + PIP_ACTION_PREVIOUS, + ), + if (mediaController?.isPlaying == true) { + createPipAction( + context = this@PlayerActivity, + "pause", + coreUiR.drawable.ic_pause, + PIP_ACTION_PAUSE, + ) + } else { + createPipAction( + context = this@PlayerActivity, + "play", + coreUiR.drawable.ic_play, + PIP_ACTION_PLAY, + ) + }, + createPipAction( + context = this@PlayerActivity, + "skip to next", + coreUiR.drawable.ic_skip_next, + PIP_ACTION_NEXT, + ), + ), + ) }.build().also { setPictureInPictureParams(it) } } @@ -432,12 +492,10 @@ class PlayerActivity : AppCompatActivity() { return binding.playerView.player?.videoSize?.let { videoSize -> if (videoSize.width == 0 || videoSize.height == 0) return@let null - val minAspectRatio = 0.5f // 1:2 aspect ratio - val maxAspectRatio = 2.39f // 21:9 aspect ratio Rational( - videoSize.width.coerceIn((videoSize.height * minAspectRatio).toInt(), (videoSize.height * maxAspectRatio).toInt()), - videoSize.height.coerceIn((videoSize.width * minAspectRatio).toInt(), (videoSize.width * maxAspectRatio).toInt()), - ) + videoSize.width, + videoSize.height, + ).takeIf { it.toFloat() in 0.5f..2.39f } } } @@ -626,7 +684,7 @@ class PlayerActivity : AppCompatActivity() { private fun playbackStateListener() = object : Player.Listener { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { super.onMediaItemTransition(mediaItem, reason) - viewModel.currentMediaItem = mediaItem + viewModel.currentMediaItemUri = mediaItem?.localConfiguration?.uri isMediaItemReady = false } @@ -661,11 +719,11 @@ class PlayerActivity : AppCompatActivity() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPipSupported) { updatePictureInPictureParams() } + setOrientation() } lifecycleScope.launch { val videoScale = mediaController?.currentMediaItem?.mediaId?.let { viewModel.getVideoState(it)?.videoScale } ?: 1f applyVideoScale(videoScale = videoScale) - setOrientation() } } @@ -723,10 +781,11 @@ class PlayerActivity : AppCompatActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) if (intent.data != null) { - mediaController?.clearMediaItems() - setIntent(intent) - prettyPrintIntent() - playVideo(intent.data!!) + currentOrientation = null + viewModel.currentMediaItemUri = intent.data + if (mediaController != null) { + playVideo(intent.data!!) + } } } @@ -997,5 +1056,34 @@ class PlayerActivity : AppCompatActivity() { companion object { const val HIDE_DELAY_MILLIS = 1000L + const val PIP_INTENT_ACTION = "pip_action" + const val PIP_INTENT_ACTION_CODE = "pip_action_code" + const val PIP_ACTION_PLAY = 1 + const val PIP_ACTION_PAUSE = 2 + const val PIP_ACTION_NEXT = 3 + const val PIP_ACTION_PREVIOUS = 4 } } + +@RequiresApi(Build.VERSION_CODES.O) +fun createPipAction( + context: Context, + title: String, + @DrawableRes icon: Int, + actionCode: Int, +): RemoteAction { + return RemoteAction( + Icon.createWithResource(context, icon), + title, + title, + PendingIntent.getBroadcast( + context, + actionCode, + Intent(PlayerActivity.PIP_INTENT_ACTION).apply { + putExtra(PlayerActivity.PIP_INTENT_ACTION_CODE, actionCode) + setPackage(context.packageName) + }, + PendingIntent.FLAG_IMMUTABLE, + ), + ) +} diff --git a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerViewModel.kt b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerViewModel.kt index 66bb075cb..503e1b47a 100644 --- a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerViewModel.kt +++ b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerViewModel.kt @@ -3,7 +3,6 @@ package dev.anilbeesetti.nextplayer.feature.player import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.media3.common.MediaItem import dagger.hilt.android.lifecycle.HiltViewModel import dev.anilbeesetti.nextplayer.core.data.models.VideoState import dev.anilbeesetti.nextplayer.core.data.repository.MediaRepository @@ -25,7 +24,7 @@ class PlayerViewModel @Inject constructor( private val getSortedPlaylistUseCase: GetSortedPlaylistUseCase, ) : ViewModel() { - var currentMediaItem: MediaItem? = null + var currentMediaItemUri: Uri? = null var playWhenReady: Boolean = true var skipSilenceEnabled: Boolean = false diff --git a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/extensions/Activity.kt b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/extensions/Activity.kt index 5a50aae7f..404ae267f 100644 --- a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/extensions/Activity.kt +++ b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/extensions/Activity.kt @@ -6,8 +6,6 @@ import android.view.WindowManager import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat.Type import androidx.core.view.WindowInsetsControllerCompat -import java.util.Arrays -import timber.log.Timber /** * Must call this function after any configuration done to activity to keep system bars behaviour @@ -34,29 +32,3 @@ val Activity.currentBrightness: Float in WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF..WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL -> brightness else -> Settings.System.getFloat(contentResolver, Settings.System.SCREEN_BRIGHTNESS) / 255 } - -@Suppress("DEPRECATION") -fun Activity.prettyPrintIntent() { - try { - Timber.apply { - d("* action: ${intent.action}") - d("* data: ${intent.data}") - d("* type: ${intent.type}") - d("* package: ${intent.`package`}") - d("* component: ${intent.component}") - d("* flags: ${intent.flags}") - intent.extras?.let { bundle -> - d("=== Extras ===") - bundle.keySet().forEachIndexed { i, key -> - buildString { - append("${i + 1}) $key: ") - bundle.get(key).let { append(if (it is Array<*>) Arrays.toString(it) else it) } - }.also { d(it) } - } - } - } - } catch (e: Exception) { - Timber.e(e) - e.printStackTrace() - } -} diff --git a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/extensions/VideoSize.kt b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/extensions/VideoSize.kt index c2f135d17..f5e8ab222 100644 --- a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/extensions/VideoSize.kt +++ b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/extensions/VideoSize.kt @@ -3,7 +3,4 @@ package dev.anilbeesetti.nextplayer.feature.player.extensions import androidx.media3.common.VideoSize val VideoSize.isPortrait: Boolean - get() { - val isRotated = this.unappliedRotationDegrees == 90 || this.unappliedRotationDegrees == 270 - return if (isRotated) this.width > this.height else this.height > this.width - } + get() = this.height > this.width diff --git a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/service/PlayerService.kt b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/service/PlayerService.kt index 15b157f95..adb267996 100644 --- a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/service/PlayerService.kt +++ b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/service/PlayerService.kt @@ -295,7 +295,10 @@ class PlayerService : MediaSessionService() { } CustomCommands.STOP_PLAYER_SESSION -> { - stopSelf() + mediaSession?.run { + player.clearMediaItems() + player.stop() + } ?: stopSelf() return@future SessionResult(SessionResult.RESULT_SUCCESS) } } diff --git a/feature/player/src/main/res/values/drawables.xml b/feature/player/src/main/res/values/drawables.xml index f50bbce9b..a9fd86e5e 100644 --- a/feature/player/src/main/res/values/drawables.xml +++ b/feature/player/src/main/res/values/drawables.xml @@ -2,4 +2,6 @@ @drawable/ic_play @drawable/ic_pause + @drawable/ic_skip_next + @drawable/ic_skip_prev \ No newline at end of file