diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt index 5392675..43e199f 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt @@ -268,12 +268,7 @@ class OmniPlayerService : MediaSessionService() { lateinit var player: Player lateinit var mediaSession: MediaSession - init { - Log.e("omni", "service inited") - } - override fun onCreate() { - Log.e("omni", "service created") super.onCreate() player = OmniPlayer.notificationPlayer ?: throw Error("No player available") val launchIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply { diff --git a/android/src/main/java/dev/zoriya/omni/OmniView.kt b/android/src/main/java/dev/zoriya/omni/OmniView.kt index 533c38d..3c0d9cf 100755 --- a/android/src/main/java/dev/zoriya/omni/OmniView.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniView.kt @@ -1,15 +1,42 @@ package dev.zoriya.omni +import android.app.PictureInPictureParams +import android.os.Build import android.util.Log +import android.util.Rational import android.view.SurfaceHolder import android.view.SurfaceView +import android.view.View +import android.view.ViewGroup import android.widget.FrameLayout +import androidx.core.view.isVisible +import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.uimanager.ThemedReactContext import com.margelo.nitro.omni.HybridOmniPlayerSpec import com.margelo.nitro.omni.HybridOmniViewSpec -class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceHolder.Callback { - override val view = FrameLayout(context) +class OmniView(val context: ThemedReactContext) : + HybridOmniViewSpec(), + SurfaceHolder.Callback, + LifecycleEventListener { + override val view = object : FrameLayout(context) { + // React Native manages layout via Yoga and swallows requestLayout() calls from native + // views. When children are added/removed (e.g. during PIP transitions), addView triggers + // requestLayout() but it never results in a measure+layout pass. Override to force one. + // see: https://github.com/facebook/react-native/blob/d19afc73f5048f81656d0b4424232ce6d69a6368/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbar.java#L166 + private val layoutRunnable = Runnable { + measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) + ) + layout(left, top, right, bottom) + } + + override fun requestLayout() { + super.requestLayout() + post(layoutRunnable) + } + } private val surfaceView = SurfaceView(context).apply { layoutParams = FrameLayout.LayoutParams( @@ -21,11 +48,18 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH } private var surfaceReady = false private var boundPlayer: OmniPlayer? = null + private var rootContent: ViewGroup? = null + private var rootContentViews: List = emptyList() + private var movedSurfaceToRootForPip = false override lateinit var player: HybridOmniPlayerSpec override var autoplay: Boolean? = true override var autoPip: Boolean? = true + init { + context.addLifecycleEventListener(this) + } + override fun afterUpdate() { Log.e("omniView", "After update called") if (!::player.isInitialized) { @@ -51,6 +85,9 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH } override fun onDropView() { + restoreUiAfterPip() + context.removeLifecycleEventListener(this) + if (!::player.isInitialized) return val omniPlayer = player as? OmniPlayer ?: return @@ -58,6 +95,95 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH boundPlayer = null } + override fun onHostResume() { + Log.e("omni", "resume") + restoreUiAfterPip() + } + + override fun onHostPause() { + Log.e("omni", "pause") + if (autoPip != true) { + return + } + + val activity = context.currentActivity ?: return + if (activity.isFinishing || activity.isDestroyed || activity.isInPictureInPictureMode) { + return + } + + val omniPlayer = boundPlayer ?: return + if (!omniPlayer.isPlaying) { + return + } + + val aspectRatio = if (surfaceView.width > 0 && surfaceView.height > 0) { + Rational(surfaceView.width, surfaceView.height) + } else { + Rational(16, 9) + } + + val params = PictureInPictureParams.Builder() + .setAspectRatio(aspectRatio) + .build() + isolateUiForPip(activity) + val enteredPip = activity.enterPictureInPictureMode(params) + if (!enteredPip) { + restoreUiAfterPip() + } + } + + override fun onHostDestroy() { + restoreUiAfterPip() + } + + private fun isolateUiForPip(activity: android.app.Activity) { + if (movedSurfaceToRootForPip) { + return + } + + val root = activity.window.decorView.findViewById(android.R.id.content) ?: return + rootContent = root + + rootContentViews = (0 until root.childCount) + .map { root.getChildAt(it) } + .filter { child -> child.isVisible } + rootContentViews.forEach { child -> + child.visibility = View.GONE + } + + view.removeView(surfaceView) + root.addView( + surfaceView, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + ) + movedSurfaceToRootForPip = true + } + + private fun restoreUiAfterPip() { + if (!movedSurfaceToRootForPip) { + return + } + + val root = rootContent ?: return + root.removeView(surfaceView) + rootContent = null + + rootContentViews.forEach { child -> + child.visibility = View.VISIBLE + } + rootContentViews = emptyList() + + view.addView(surfaceView, FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + )) + + movedSurfaceToRootForPip = false + } + override fun surfaceCreated(holder: SurfaceHolder) { surfaceReady = true boundPlayer?.setSurface(holder) diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index fb78f39..ac177fd 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -16,6 +16,8 @@ android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:launchMode="singleTask" + android:resizeableActivity="true" + android:supportsPictureInPicture="true" android:windowSoftInputMode="adjustResize" android:exported="true">