package expo.modules.video import android.app.Activity import android.content.pm.ActivityInfo import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.content.Context import android.util.Log import android.view.View import android.view.accessibility.CaptioningManager import android.view.WindowInsets import android.view.WindowInsetsController import android.widget.ImageButton import androidx.media3.ui.PlayerView import expo.modules.kotlin.exception.CodedException import expo.modules.video.player.VideoPlayer import expo.modules.video.records.FullscreenOptions import expo.modules.video.utils.FullscreenActivityOrientationHelper import expo.modules.video.utils.SubtitleUtils import expo.modules.video.utils.applyPiPParams import expo.modules.video.utils.applyRectHint import expo.modules.video.utils.calculatePiPAspectRatio import expo.modules.video.utils.calculateRectHint @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) class FullscreenPlayerActivity : Activity() { private lateinit var mContentView: View private lateinit var videoViewId: String private var videoPlayer: VideoPlayer? = null private lateinit var playerView: PlayerView private lateinit var videoView: VideoView private var didFinish = false private var wasAutoPaused = false private lateinit var options: FullscreenOptions private lateinit var orientationHelper: FullscreenActivityOrientationHelper private var captioningChangeListener: CaptioningManager.CaptioningChangeListener? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) try { videoViewId = intent.getStringExtra(VideoManager.INTENT_PLAYER_KEY) ?: throw FullScreenVideoViewNotFoundException() options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getSerializableExtra(INTENT_FULLSCREEN_OPTIONS_KEY, FullscreenOptions::class.java) ?: throw FullScreenOptionsNotFoundException() } else { @Suppress("DEPRECATION") intent.getSerializableExtra(INTENT_FULLSCREEN_OPTIONS_KEY) as? FullscreenOptions ?: throw FullScreenOptionsNotFoundException() } videoView = VideoManager.getVideoView(videoViewId) orientationHelper = FullscreenActivityOrientationHelper( this, options, onShouldAutoExit = { finish() }, onShouldReleaseOrientation = { requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } ) orientationHelper.startOrientationEventListener() } catch (e: CodedException) { Log.e("ExpoVideo", "${e.message}", e) finish() return } setContentView(R.layout.fullscreen_player_activity) mContentView = findViewById(R.id.enclosing_layout) playerView = findViewById(R.id.player_view) requestedOrientation = options.orientation.toActivityOrientation() videoPlayer = videoView.videoPlayer videoPlayer?.changePlayerView(playerView) VideoManager.registerFullscreenPlayerActivity(hashCode().toString(), this) playerView.player?.let { val aspectRatio = calculatePiPAspectRatio(it.videoSize, playerView.width, playerView.height, videoView.contentFit) applyPiPParams(this, videoView.autoEnterPiP, aspectRatio) } } override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) hideStatusBar() setupFullscreenButton() playerView.applyRequiresLinearPlayback(videoPlayer?.requiresLinearPlayback ?: false) playerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> // On every re-layout ExoPlayer makes the timeBar interactive. // We need to disable it to keep scrubbing off. playerView.setTimeBarInteractive(videoPlayer?.requiresLinearPlayback ?: true) } playerView.setShowSubtitleButton(videoView.showsSubtitlesButton) // Configure subtitle view to fix sizing issues with embedded styles (same as VideoView) SubtitleUtils.configureSubtitleView(playerView, this) // Set up listener for accessibility caption changes setupCaptioningChangeListener() playerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> applyRectHint(this, calculateRectHint(playerView)) } } override fun finish() { super.finish() didFinish = true VideoManager.getVideoView(videoViewId).attachPlayer() // Disable the exit transition if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, 0, 0) } else { @Suppress("DEPRECATION") overridePendingTransition(0, 0) } } override fun onResume() { orientationHelper.startOrientationEventListener() playerView.useController = true // Reconfigure subtitles when resuming (handles returning from settings) SubtitleUtils.configureSubtitleView(playerView, this) super.onResume() } override fun onPause() { if (videoPlayer?.staysActiveInBackground != true && !didFinish) { wasAutoPaused = videoPlayer?.player?.isPlaying == true if (wasAutoPaused) { playerView.useController = false videoPlayer?.player?.pause() } } orientationHelper.stopOrientationEventListener() super.onPause() } override fun onDestroy() { super.onDestroy() // Clean up captioning change listener captioningChangeListener?.let { val captioningManager = getSystemService(Context.CAPTIONING_SERVICE) as? CaptioningManager captioningManager?.removeCaptioningChangeListener(it) captioningChangeListener = null } videoView.exitFullscreen() VideoManager.unregisterFullscreenPlayerActivity(hashCode().toString()) orientationHelper.stopOrientationEventListener() } private fun setupFullscreenButton() { playerView.setFullscreenButtonClickListener { finish() } val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen) fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_exit) } override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) { if (!isInPictureInPictureMode) { playerView.useController = videoView.useNativeControls } else { playerView.useController = false } if (wasAutoPaused && isInPictureInPictureMode) { videoPlayer?.player?.play() } super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) } private fun hideStatusBar() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val controller = mContentView.windowInsetsController controller?.apply { systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars()) } } else { @Suppress("DEPRECATION") mContentView.systemUiVisibility = ( View.SYSTEM_UI_FLAG_LOW_PROFILE or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION ) } } private fun setupCaptioningChangeListener() { val captioningManager = getSystemService(Context.CAPTIONING_SERVICE) as? CaptioningManager captioningChangeListener = SubtitleUtils.createCaptioningChangeListener(playerView, this) captioningChangeListener?.let { listener -> captioningManager?.addCaptioningChangeListener(listener) } } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) orientationHelper.onConfigurationChanged(newConfig) } companion object { const val INTENT_FULLSCREEN_OPTIONS_KEY = "fullscreen_options" } }