/* * Copyright (C) 2020 Stefan Schüller * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package net.schueller.peertube.activity import androidx.appcompat.app.AppCompatActivity import android.annotation.SuppressLint import net.schueller.peertube.fragment.VideoPlayerFragment import net.schueller.peertube.R import android.app.RemoteAction import android.app.PendingIntent import com.google.android.exoplayer2.ui.PlayerNotificationManager import android.app.PictureInPictureParams import android.content.* import android.content.res.Configuration import android.graphics.drawable.Icon import android.os.Bundle import android.text.TextUtils import net.schueller.peertube.fragment.VideoMetaDataFragment import android.widget.RelativeLayout import android.widget.FrameLayout import android.util.TypedValue import android.view.WindowManager import net.schueller.peertube.service.VideoPlayerService import net.schueller.peertube.helper.VideoHelper import androidx.annotation.RequiresApi import android.os.Build import android.util.Log import android.util.Rational import androidx.fragment.app.Fragment import net.schueller.peertube.fragment.VideoDescriptionFragment import java.util.ArrayList class VideoPlayActivity : CommonActivity() { private var receiver: BroadcastReceiver? = null //This can only be called when in entering pip mode which can't happen if the device doesn't support pip mode. @SuppressLint("NewApi") fun makePipControls() { val fragmentManager = supportFragmentManager val videoPlayerFragment = fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment? val actions = ArrayList() val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 var actionIntent = Intent(getString(R.string.app_background_audio)) var pendingIntent = PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, flags) @SuppressLint("NewApi", "LocalSuppress") var icon = Icon.createWithResource( applicationContext, android.R.drawable.stat_sys_speakerphone ) @SuppressLint("NewApi", "LocalSuppress") var remoteAction = RemoteAction(icon!!, "close pip", "from pip window custom command", pendingIntent!!) actions.add(remoteAction) actionIntent = Intent(PlayerNotificationManager.ACTION_STOP) pendingIntent = PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, flags) icon = Icon.createWithResource( applicationContext, com.google.android.exoplayer2.ui.R.drawable.exo_notification_stop ) remoteAction = RemoteAction(icon, "play", "stop the media", pendingIntent) actions.add(remoteAction) assert(videoPlayerFragment != null) if (videoPlayerFragment!!.isPaused) { Log.e(TAG, "setting actions with play button") actionIntent = Intent(PlayerNotificationManager.ACTION_PLAY) pendingIntent = PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, flags) icon = Icon.createWithResource( applicationContext, com.google.android.exoplayer2.ui.R.drawable.exo_notification_play ) remoteAction = RemoteAction(icon, "play", "play the media", pendingIntent) } else { Log.e(TAG, "setting actions with pause button") actionIntent = Intent(PlayerNotificationManager.ACTION_PAUSE) pendingIntent = PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, flags) icon = Icon.createWithResource( applicationContext, com.google.android.exoplayer2.ui.R.drawable.exo_notification_pause ) remoteAction = RemoteAction(icon, "pause", "pause the media", pendingIntent) } actions.add(remoteAction) //add custom actions to pip window val params = PictureInPictureParams.Builder() .setActions(actions) .build() setPictureInPictureParams(params) } private fun changedToPipMode() { val fragmentManager = supportFragmentManager val videoPlayerFragment = (fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!! videoPlayerFragment.showControls(false) //create custom actions makePipControls() //setup receiver to handle customer actions val filter = IntentFilter() filter.addAction(PlayerNotificationManager.ACTION_STOP) filter.addAction(PlayerNotificationManager.ACTION_PAUSE) filter.addAction(PlayerNotificationManager.ACTION_PLAY) filter.addAction(getString(R.string.app_background_audio)) receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val action = intent.action!! if (action == PlayerNotificationManager.ACTION_PAUSE) { videoPlayerFragment.pauseVideo() makePipControls() } if (action == PlayerNotificationManager.ACTION_PLAY) { videoPlayerFragment.unPauseVideo() makePipControls() } if (action == getString(R.string.app_background_audio)) { unregisterReceiver(receiver) finish() } if (action == PlayerNotificationManager.ACTION_STOP) { unregisterReceiver(receiver) finishAndRemoveTask() } } } registerReceiver(receiver, filter) Log.v(TAG, "switched to pip ") floatMode = true videoPlayerFragment.showControls(false) } private fun changedToNormalMode() { val fragmentManager = supportFragmentManager val videoPlayerFragment = (fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!! videoPlayerFragment.showControls(true) if (receiver != null) { unregisterReceiver(receiver) } Log.v(TAG, "switched to normal") floatMode = false } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Set theme val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE) setTheme( resources.getIdentifier( sharedPref.getString( getString(R.string.pref_theme_key), getString(R.string.app_default_theme) ), "style", packageName ) ) setContentView(R.layout.activity_video_play) // get video ID val intent = intent val videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID) val videoPlayerFragment = (supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!! val playingVideo = videoPlayerFragment.videoUuid Log.v(TAG, "oncreate click: $videoUuid is trying to replace: $playingVideo") when { TextUtils.isEmpty(playingVideo) -> { Log.v(TAG, "oncreate no video currently playing") videoPlayerFragment.start(videoUuid) } playingVideo != videoUuid -> { Log.v(TAG, "oncreate different video playing currently") videoPlayerFragment.stopVideo() videoPlayerFragment.start(videoUuid) } else -> { Log.v(TAG, "oncreate same video playing currently") } } // if we are in landscape set the video to fullscreen val orientation = this.resources.configuration.orientation if (orientation == Configuration.ORIENTATION_LANDSCAPE) { setOrientation(true) } } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) val videoPlayerFragment = (supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!! val videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID) Log.v( TAG, "new intent click: " + videoUuid + " is trying to replace: " + videoPlayerFragment.videoUuid ) val playingVideo = videoPlayerFragment.videoUuid when { TextUtils.isEmpty(playingVideo) -> { Log.v(TAG, "new intent no video currently playing") videoPlayerFragment.start(videoUuid) } playingVideo != videoUuid -> { Log.v(TAG, "new intent different video playing currently") videoPlayerFragment.stopVideo() videoPlayerFragment.start(videoUuid) } else -> { Log.v(TAG, "new intent same video playing currently") } } // if we are in landscape set the video to fullscreen val orientation = this.resources.configuration.orientation if (orientation == Configuration.ORIENTATION_LANDSCAPE) { setOrientation(true) } } override fun onConfigurationChanged(newConfig: Configuration) { Log.v(TAG, "onConfigurationChanged()...") super.onConfigurationChanged(newConfig) // Checking the orientation changes of the screen if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { setOrientation(true) } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { setOrientation(false) } } private fun setOrientation(isLandscape: Boolean) { val fragmentManager = supportFragmentManager val videoPlayerFragment = fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment? val videoMetaFragment = fragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment? assert(videoPlayerFragment != null) val params = videoPlayerFragment!!.requireView().layoutParams as RelativeLayout.LayoutParams params.width = FrameLayout.LayoutParams.MATCH_PARENT params.height = if (isLandscape) FrameLayout.LayoutParams.MATCH_PARENT else TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 250f, resources.displayMetrics ) .toInt() videoPlayerFragment.requireView().layoutParams = params if (videoMetaFragment != null) { val transaction = fragmentManager.beginTransaction() .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out) if (isLandscape) { transaction.hide(videoMetaFragment) } else { transaction.show(videoMetaFragment) } transaction.commit() } videoPlayerFragment.setIsFullscreen(isLandscape) if (isLandscape) { window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) } else { window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) } } override fun onDestroy() { val videoPlayerFragment = (supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!! videoPlayerFragment.destroyVideo() super.onDestroy() Log.v(TAG, "onDestroy...") } override fun onPause() { super.onPause() Log.v(TAG, "onPause()...") } override fun onResume() { super.onResume() Log.v(TAG, "onResume()...") } override fun onStop() { super.onStop() val videoPlayerFragment = (supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!! videoPlayerFragment.stopVideo() // TODO: doesn't remove fragment?? val fragment: Fragment? = supportFragmentManager.findFragmentByTag(VideoDescriptionFragment.TAG) if (fragment != null) { Log.v(TAG, "remove VideoDescriptionFragment") supportFragmentManager.beginTransaction().remove(fragment).commit() } Log.v(TAG, "onStop()...") } override fun onStart() { super.onStart() Log.v(TAG, "onStart()...") } @SuppressLint("NewApi") public override fun onUserLeaveHint() { Log.v(TAG, "onUserLeaveHint()...") val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE) val fragmentManager = supportFragmentManager val videoPlayerFragment = fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment? val videoMetaDataFragment = fragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment? val backgroundBehavior = sharedPref.getString( getString(R.string.pref_background_behavior_key), getString(R.string.pref_background_stop_key) ) assert(videoPlayerFragment != null) assert(backgroundBehavior != null) if (videoMetaDataFragment!!.isLeaveAppExpected) { super.onUserLeaveHint() return } if (backgroundBehavior == getString(R.string.pref_background_stop_key)) { Log.v(TAG, "stop the video") videoPlayerFragment!!.pauseVideo() stopService(Intent(this, VideoPlayerService::class.java)) super.onBackPressed() } else if (backgroundBehavior == getString(R.string.pref_background_audio_key)) { Log.v(TAG, "play the Audio") super.onBackPressed() } else if (backgroundBehavior == getString(R.string.pref_background_float_key)) { Log.v(TAG, "play in floating video") //canEnterPIPMode makes sure API level is high enough if (VideoHelper.canEnterPipMode(this)) { Log.v(TAG, "enabling pip") enterPipMode() } else { Log.v(TAG, "unable to use pip") } } else { // Deal with bad entries from older version Log.v(TAG, "No setting, fallback") super.onBackPressed() } } // @RequiresApi(api = Build.VERSION_CODES.O) @SuppressLint("NewApi") override fun onBackPressed() { Log.v(TAG, "onBackPressed()...") val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE) val videoPlayerFragment = (supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!! // copying Youtube behavior to have back button exit full screen. if (videoPlayerFragment.getIsFullscreen()) { Log.v(TAG, "exiting full screen") videoPlayerFragment.fullScreenToggle() return } // pause video if pref is enabled if (sharedPref.getBoolean(getString(R.string.pref_back_pause_key), true)) { videoPlayerFragment.pauseVideo() } val backgroundBehavior = sharedPref.getString( getString(R.string.pref_background_behavior_key), getString(R.string.pref_background_stop_key) )!! if (backgroundBehavior == getString(R.string.pref_background_stop_key)) { Log.v(TAG, "stop the video") videoPlayerFragment.pauseVideo() stopService(Intent(this, VideoPlayerService::class.java)) super.onBackPressed() } else if (backgroundBehavior == getString(R.string.pref_background_audio_key)) { Log.v(TAG, "play the Audio") super.onBackPressed() } else if (backgroundBehavior == getString(R.string.pref_background_float_key)) { Log.v(TAG, "play in floating video") //canEnterPIPMode makes sure API level is high enough if (VideoHelper.canEnterPipMode(this)) { Log.v(TAG, "enabling pip") enterPipMode() //fixes problem where back press doesn't bring up video list after returning from PIP mode val intentSettings = Intent(this, VideoListActivity::class.java) this.startActivity(intentSettings) } else { Log.v(TAG, "Unable to enter PIP mode") super.onBackPressed() } } else { // Deal with bad entries from older version Log.v(TAG, "No setting, fallback") super.onBackPressed() } } @RequiresApi(api = Build.VERSION_CODES.O) fun enterPipMode() { val fragmentManager = supportFragmentManager val videoPlayerFragment = fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment? if (videoPlayerFragment!!.videoAspectRatio == 0.toFloat()) { Log.i(TAG, "impossible to switch to pip") } else { val rational = Rational((videoPlayerFragment.videoAspectRatio * 100).toInt(), 100) val mParams = PictureInPictureParams.Builder() .setAspectRatio(rational) // .setSourceRectHint(new Rect(0,500,400,600)) .build() enterPictureInPictureMode(mParams) } } override fun onPictureInPictureModeChanged( isInPictureInPictureMode: Boolean, newConfig: Configuration ) { val fragmentManager = supportFragmentManager val videoPlayerFragment = fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment? if (videoPlayerFragment != null) { if (isInPictureInPictureMode) { changedToPipMode() Log.v(TAG, "switched to pip ") videoPlayerFragment.useController(false) } else { changedToNormalMode() Log.v(TAG, "switched to normal") videoPlayerFragment.useController(true) } } else { Log.e(TAG, "videoPlayerFragment is NULL") } } companion object { private const val TAG = "VideoPlayActivity" var floatMode = false private const val REQUEST_CODE = 101 } }