feat: adding pip support for android and handling other deprecation warnings

This commit is contained in:
tapframe 2026-04-05 16:44:14 +05:30
parent 77a337d505
commit 786b4395fb
11 changed files with 164 additions and 11 deletions

View file

@ -14,6 +14,8 @@
<activity
android:exported="true"
android:name=".MainActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:supportsPictureInPicture="true"
android:theme="@style/Theme.Nuvio.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -7,7 +7,6 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.SystemBarStyle
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.nuvio.app.core.auth.AuthStorage
import com.nuvio.app.core.deeplink.handleAppUrl
import com.nuvio.app.core.storage.PlatformLocalAccountDataCleaner
@ -19,6 +18,7 @@ import com.nuvio.app.features.mdblist.MdbListSettingsStorage
import com.nuvio.app.features.notifications.EpisodeReleaseNotificationPlatform
import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsStorage
import com.nuvio.app.features.player.PlayerSettingsStorage
import com.nuvio.app.features.player.PlayerPictureInPictureManager
import com.nuvio.app.features.plugins.PluginStorage
import com.nuvio.app.features.profiles.ProfileStorage
import com.nuvio.app.features.details.SeasonViewModeStorage
@ -42,9 +42,6 @@ class MainActivity : ComponentActivity() {
)
super.onCreate(savedInstanceState)
window.setBackgroundDrawableResource(R.color.nuvio_background)
window.navigationBarColor = getColor(R.color.nuvio_background)
window.isNavigationBarContrastEnforced = false
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = false
AddonStorage.initialize(applicationContext)
AuthStorage.initialize(applicationContext)
LibraryStorage.initialize(applicationContext)
@ -81,6 +78,11 @@ class MainActivity : ComponentActivity() {
handleIncomingAppIntent(intent)
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
PlayerPictureInPictureManager.onUserLeaveHint(this)
}
override fun onDestroy() {
EpisodeReleaseNotificationPlatform.unbindActivity(this)
super.onDestroy()

View file

@ -1,9 +1,13 @@
package com.nuvio.app.features.player
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.net.Uri
import android.util.Log
import android.util.TypedValue
import android.graphics.Typeface
import android.os.Build
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -195,10 +199,17 @@ actual fun PlatformPlayerSurface(
}
DisposableEffect(exoPlayer, lifecycleOwner) {
val activity = context.findActivity()
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> exoPlayer.playWhenReady = playWhenReady
Lifecycle.Event.ON_STOP -> exoPlayer.pause()
Lifecycle.Event.ON_STOP -> {
val isInPictureInPicture =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity?.isInPictureInPictureMode == true
if (!isInPictureInPicture) {
exoPlayer.pause()
}
}
else -> Unit
}
}
@ -388,6 +399,13 @@ actual fun PlatformPlayerSurface(
)
}
private tailrec fun Context.findActivity(): Activity? =
when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
private fun ExoPlayer.snapshot(): PlayerPlaybackSnapshot =
PlayerPlaybackSnapshot(
isLoading = playbackState == Player.STATE_IDLE || playbackState == Player.STATE_BUFFERING,

View file

@ -0,0 +1,83 @@
package com.nuvio.app.features.player
import android.app.Activity
import android.app.PictureInPictureParams
import android.os.Build
import android.util.Rational
import androidx.compose.ui.unit.IntSize
internal object PlayerPictureInPictureManager {
private data class SessionState(
val isActive: Boolean = false,
val isPlaying: Boolean = false,
val playerSize: IntSize = IntSize.Zero,
)
private var sessionState = SessionState()
fun updateSession(
activity: Activity,
isActive: Boolean,
isPlaying: Boolean,
playerSize: IntSize,
) {
sessionState = SessionState(
isActive = isActive,
isPlaying = isPlaying,
playerSize = playerSize,
)
applyPictureInPictureParams(activity)
}
fun clearSession(activity: Activity) {
sessionState = SessionState()
applyPictureInPictureParams(activity)
}
fun onUserLeaveHint(activity: Activity): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return false
}
return enterIfEligible(activity)
}
private fun applyPictureInPictureParams(activity: Activity) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
activity.setPictureInPictureParams(buildParams())
}
private fun enterIfEligible(activity: Activity): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false
if (!sessionState.isActive || !sessionState.isPlaying) return false
if (activity.isFinishing) return false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInPictureInPictureMode) return false
return activity.enterPictureInPictureMode(buildParams())
}
private fun buildParams(): PictureInPictureParams {
val builder = PictureInPictureParams.Builder()
buildAspectRatio(sessionState.playerSize)?.let(builder::setAspectRatio)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setAutoEnterEnabled(sessionState.isActive && sessionState.isPlaying)
builder.setSeamlessResizeEnabled(true)
}
return builder.build()
}
private fun buildAspectRatio(playerSize: IntSize): Rational? {
if (playerSize.width <= 0 || playerSize.height <= 0) return null
val width = playerSize.width.coerceAtLeast(1)
val height = playerSize.height.coerceAtLeast(1)
val ratio = width.toDouble() / height.toDouble()
return when {
ratio > MaxPictureInPictureAspectRatio -> Rational(239, 100)
ratio < MinPictureInPictureAspectRatio -> Rational(100, 239)
else -> Rational(width, height)
}
}
}
private const val MaxPictureInPictureAspectRatio = 2.39
private const val MinPictureInPictureAspectRatio = 1.0 / MaxPictureInPictureAspectRatio

View file

@ -5,11 +5,14 @@ import android.content.Context
import android.content.ContextWrapper
import android.content.pm.ActivityInfo
import android.media.AudioManager
import android.os.Build
import android.provider.Settings
import android.view.WindowManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.platform.LocalContext
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
@ -19,6 +22,7 @@ import kotlin.math.roundToInt
@Composable
actual fun LockPlayerToLandscape() {
val activity = LocalContext.current.findActivity() ?: return
if (!activity.shouldForceLandscapePlayer()) return
DisposableEffect(activity) {
val previousOrientation = activity.requestedOrientation
@ -50,6 +54,29 @@ actual fun EnterImmersivePlayerMode() {
}
}
@Composable
actual fun ManagePlayerPictureInPicture(
isPlaying: Boolean,
playerSize: IntSize,
) {
val activity = LocalContext.current.findActivity() ?: return
DisposableEffect(activity) {
onDispose {
PlayerPictureInPictureManager.clearSession(activity)
}
}
SideEffect {
PlayerPictureInPictureManager.updateSession(
activity = activity,
isActive = true,
isPlaying = isPlaying,
playerSize = playerSize,
)
}
}
@Composable
actual fun rememberPlayerGestureController(): PlayerGestureController? {
val context = LocalContext.current
@ -79,6 +106,12 @@ private tailrec fun Context.findActivity(): Activity? =
else -> null
}
private fun Activity.shouldForceLandscapePlayer(): Boolean {
if (resources.configuration.smallestScreenWidthDp >= 600) return false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode) return false
return true
}
private class AndroidPlayerGestureController(
private val activity: Activity,
private val audioManager: AudioManager,

View file

@ -2,8 +2,6 @@
<resources>
<style name="Theme.Nuvio" parent="@android:style/Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/nuvio_background</item>
<item name="android:navigationBarColor">@color/nuvio_background</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
<style name="Theme.Nuvio.Splash" parent="Theme.SplashScreen">

View file

@ -2,8 +2,6 @@
<resources>
<style name="Theme.Nuvio" parent="@android:style/Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/nuvio_background</item>
<item name="android:navigationBarColor">@color/nuvio_background</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
<style name="Theme.Nuvio.Splash" parent="@style/Theme.Nuvio" />

View file

@ -1,6 +1,7 @@
package com.nuvio.app.features.player
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.IntSize
interface PlayerGestureController {
fun currentBrightness(): Float?
@ -20,5 +21,11 @@ expect fun LockPlayerToLandscape()
@Composable
expect fun EnterImmersivePlayerMode()
@Composable
expect fun ManagePlayerPictureInPicture(
isPlaying: Boolean,
playerSize: IntSize,
)
@Composable
expect fun rememberPlayerGestureController(): PlayerGestureController?

View file

@ -203,6 +203,11 @@ fun PlayerScreen(
var nextEpisodeAutoPlayCountdown by remember { mutableStateOf<Int?>(null) }
var nextEpisodeAutoPlayJob by remember { mutableStateOf<Job?>(null) }
ManagePlayerPictureInPicture(
isPlaying = playbackSnapshot.isPlaying,
playerSize = layoutSize,
)
val playbackSession = remember(
contentType,
parentMetaId,

View file

@ -3,6 +3,7 @@ package com.nuvio.app.features.player
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.IntSize
import platform.Foundation.NSNotificationCenter
import platform.MediaPlayer.MPVolumeView
import platform.UIKit.UIApplication
@ -41,6 +42,12 @@ actual fun EnterImmersivePlayerMode() {
}
}
@Composable
actual fun ManagePlayerPictureInPicture(
isPlaying: Boolean,
playerSize: IntSize,
) = Unit
@Composable
actual fun rememberPlayerGestureController(): PlayerGestureController? {
val controller = remember { IOSPlayerGestureController() }

View file

@ -1,2 +1,2 @@
CURRENT_PROJECT_VERSION=4
MARKETING_VERSION=0.1.0-alpha04
CURRENT_PROJECT_VERSION=5
MARKETING_VERSION=0.1.0-alpha05