2021-02-06 19:02:12 +01:00
package org.mosad.teapod.ui.activity.player
2020-10-11 13:18:20 +02:00
2020-11-15 13:39:33 +01:00
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
2020-11-08 18:05:46 +01:00
import android.annotation.SuppressLint
2021-01-06 15:07:31 +01:00
import android.app.PictureInPictureParams
2021-01-08 09:31:50 +01:00
import android.content.Intent
2021-01-06 15:07:31 +01:00
import android.content.pm.PackageManager
import android.content.res.Configuration
2020-10-11 14:14:38 +02:00
import android.os.Build
2020-10-11 13:18:20 +02:00
import android.os.Bundle
2020-10-11 14:14:38 +02:00
import android.util.Log
2021-01-06 15:07:31 +01:00
import android.util.Rational
2021-01-08 10:58:24 +01:00
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
2020-11-25 16:04:04 +01:00
import androidx.activity.viewModels
2021-01-06 15:07:31 +01:00
import androidx.annotation.RequiresApi
2020-10-11 13:18:20 +02:00
import androidx.appcompat.app.AppCompatActivity
2020-11-08 18:05:46 +01:00
import androidx.core.view.GestureDetectorCompat
2020-11-13 15:36:12 +01:00
import androidx.core.view.isVisible
2021-06-06 17:54:19 +02:00
import androidx.lifecycle.lifecycleScope
2020-11-21 19:40:55 +01:00
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player
2020-11-06 10:21:57 +01:00
import com.google.android.exoplayer2.ui.StyledPlayerControlView
2020-10-11 13:18:20 +02:00
import com.google.android.exoplayer2.util.Util
import kotlinx.android.synthetic.main.activity_player.*
2020-11-05 20:07:41 +01:00
import kotlinx.android.synthetic.main.player_controls.*
2020-11-20 11:20:11 +01:00
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
2020-11-25 22:35:55 +01:00
import org.mosad.teapod.R
2020-11-13 11:23:09 +01:00
import org.mosad.teapod.preferences.Preferences
2020-12-15 23:15:14 +01:00
import org.mosad.teapod.ui.components.EpisodesListPlayer
2020-12-20 20:21:27 +01:00
import org.mosad.teapod.ui.components.LanguageSettingsPlayer
2020-12-02 11:14:09 +01:00
import org.mosad.teapod.util.DataTypes
2021-01-08 10:58:24 +01:00
import org.mosad.teapod.util.hideBars
import org.mosad.teapod.util.isInPiPMode
import org.mosad.teapod.util.navToLauncherTask
2020-11-15 17:17:56 +01:00
import java.util.*
2020-11-07 13:33:59 +01:00
import java.util.concurrent.TimeUnit
2020-11-15 17:17:56 +01:00
import kotlin.concurrent.scheduleAtFixedRate
2020-10-11 13:18:20 +02:00
class PlayerActivity : AppCompatActivity ( ) {
2020-11-25 22:35:55 +01:00
private val model : PlayerViewModel by viewModels ( )
2020-11-06 10:21:57 +01:00
private lateinit var controller : StyledPlayerControlView
2020-11-08 18:05:46 +01:00
private lateinit var gestureDetector : GestureDetectorCompat
2020-11-15 17:17:56 +01:00
private lateinit var timerUpdates : TimerTask
2020-10-11 13:18:20 +02:00
2021-01-08 10:58:24 +01:00
private var wasInPiP = false
2020-11-07 13:33:59 +01:00
private var remainingTime : Long = 0
2020-10-11 13:18:20 +02:00
2020-12-26 20:09:35 +01:00
private val rwdTime : Long = 10000.u naryMinus ( )
private val fwdTime : Long = 10000
2020-12-30 20:08:19 +01:00
private val defaultShowTimeoutMs = 5000
2020-10-11 14:14:38 +02:00
2020-10-11 13:18:20 +02:00
override fun onCreate ( savedInstanceState : Bundle ? ) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity _player )
2020-10-11 14:14:38 +02:00
hideBars ( ) // Initial hide the bars
2020-10-11 13:18:20 +02:00
2020-11-25 16:04:04 +01:00
model . loadMedia (
intent . getIntExtra ( getString ( R . string . intent _media _id ) , 0 ) ,
intent . getIntExtra ( getString ( R . string . intent _episode _id ) , 0 )
)
2020-12-27 20:11:01 +01:00
model . currentEpisodeChangedListener . add { onMediaChanged ( ) }
2020-11-08 18:05:46 +01:00
gestureDetector = GestureDetectorCompat ( this , PlayerGestureListener ( ) )
2020-12-28 20:45:52 +01:00
controller = video _view . findViewById ( R . id . exo _controller )
controller . isAnimationEnabled = false // disable controls (time-bar) animation
2020-12-30 20:08:19 +01:00
initExoPlayer ( ) // call in onCreate, exoplayer lives in view model
2020-12-27 20:11:01 +01:00
initGUI ( )
2020-11-05 20:07:41 +01:00
initActions ( )
2020-10-11 13:18:20 +02:00
}
2021-01-06 15:07:31 +01:00
/ * *
* once minimum is android 7.0 this can be simplified
* only onStart and onStop should be needed then
* see : https : //developer.android.com/guide/topics/ui/picture-in-picture#continuing_playback
* /
2020-10-11 13:18:20 +02:00
override fun onStart ( ) {
super . onStart ( )
if ( Util . SDK _INT > 23 ) {
initPlayer ( )
2020-11-25 22:35:55 +01:00
video _view ?. onResume ( )
2020-10-11 13:18:20 +02:00
}
}
override fun onResume ( ) {
super . onResume ( )
2021-01-06 15:07:31 +01:00
if ( isInPiPMode ( ) ) { return }
2020-10-11 13:18:20 +02:00
if ( Util . SDK _INT <= 23 ) {
initPlayer ( )
2020-11-25 22:35:55 +01:00
video _view ?. onResume ( )
2020-10-11 13:18:20 +02:00
}
}
override fun onPause ( ) {
super . onPause ( )
2021-01-06 15:07:31 +01:00
2021-03-11 19:11:12 +01:00
if ( isInPiPMode ( ) ) { return }
if ( Util . SDK _INT <= 23 ) { onPauseOnStop ( ) }
2020-10-11 13:18:20 +02:00
}
override fun onStop ( ) {
super . onStop ( )
2021-01-08 09:31:50 +01:00
2021-03-11 19:11:12 +01:00
if ( Util . SDK _INT > 23 ) { onPauseOnStop ( ) }
2021-01-08 10:58:24 +01:00
// if the player was in pip, it's on a different task
if ( wasInPiP ) { navToLauncherTask ( ) }
2021-03-04 20:19:54 +01:00
// if the player is in pip, remove the task, else we'll get a zombie
if ( isInPiPMode ( ) ) { finishAndRemoveTask ( ) }
2020-10-11 13:18:20 +02:00
}
2021-01-08 10:58:24 +01:00
/ * *
* used , when the player is in pip and the user selects a new media
* /
override fun onNewIntent ( intent : Intent ? ) {
super . onNewIntent ( intent )
// when the intent changed, lead the new media and play it
intent ?. let {
model . loadMedia (
it . getIntExtra ( getString ( R . string . intent _media _id ) , 0 ) ,
it . getIntExtra ( getString ( R . string . intent _episode _id ) , 0 )
)
model . playEpisode ( model . currentEpisode , replace = true )
}
}
2021-01-06 15:07:31 +01:00
/ * *
* previous to android n , don ' t override
* /
@RequiresApi ( Build . VERSION_CODES . N )
override fun onUserLeaveHint ( ) {
super . onUserLeaveHint ( )
// start pip mode, if supported
if ( packageManager . hasSystemFeature ( PackageManager . FEATURE _PICTURE _IN _PICTURE ) ) {
if ( Build . VERSION . SDK _INT < Build . VERSION_CODES . O ) {
@Suppress ( " deprecation " )
enterPictureInPictureMode ( )
} else {
val width = model . player . videoFormat ?. width ?: 0
val height = model . player . videoFormat ?. height ?: 0
val params = PictureInPictureParams . Builder ( )
. setAspectRatio ( Rational ( width , height ) )
. build ( )
enterPictureInPictureMode ( params )
}
2021-01-08 09:31:50 +01:00
2021-01-08 10:58:24 +01:00
wasInPiP = isInPiPMode ( )
2021-01-06 15:07:31 +01:00
}
}
2021-01-08 09:31:50 +01:00
override fun onPictureInPictureModeChanged (
isInPictureInPictureMode : Boolean ,
newConfig : Configuration ?
) {
2021-01-06 15:07:31 +01:00
super . onPictureInPictureModeChanged ( isInPictureInPictureMode , newConfig )
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
2021-03-04 20:19:54 +01:00
video _view . useController = !is InPictureInPictureMode
2021-01-06 15:07:31 +01:00
}
2020-10-11 13:18:20 +02:00
private fun initPlayer ( ) {
2020-12-26 20:09:35 +01:00
if ( model . media . id < 0 ) {
Log . e ( javaClass . name , " No media was set. " )
2020-11-13 11:23:09 +01:00
this . finish ( )
2020-10-11 13:18:20 +02:00
}
2020-11-08 18:05:46 +01:00
initVideoView ( )
2020-11-13 15:36:12 +01:00
initTimeUpdates ( )
2020-12-28 20:45:52 +01:00
// if the player is ready or buffering we can simply play the file again, else do nothing
2021-03-04 20:19:54 +01:00
val playbackState = model . player . playbackState
if ( ( playbackState == ExoPlayer . STATE _READY || playbackState == ExoPlayer . STATE _BUFFERING ) ) {
2020-12-28 20:45:52 +01:00
model . player . play ( )
}
2020-11-08 18:05:46 +01:00
}
2020-12-28 20:45:52 +01:00
/ * *
* set play when ready and listeners
* /
2020-11-08 18:05:46 +01:00
private fun initExoPlayer ( ) {
2020-12-26 20:09:35 +01:00
model . player . addListener ( object : Player . EventListener {
2020-10-11 13:18:20 +02:00
override fun onPlaybackStateChanged ( state : Int ) {
super . onPlaybackStateChanged ( state )
loading . visibility = when ( state ) {
2020-10-11 14:14:38 +02:00
ExoPlayer . STATE _READY -> View . GONE
2020-10-11 13:18:20 +02:00
ExoPlayer . STATE _BUFFERING -> View . VISIBLE
else -> View . GONE
}
2020-11-05 20:07:41 +01:00
exo _play _pause . visibility = when ( loading . visibility ) {
View . GONE -> View . VISIBLE
View . VISIBLE -> View . INVISIBLE
else -> View . VISIBLE
}
2020-11-13 15:36:12 +01:00
2020-11-25 16:04:04 +01:00
if ( state == ExoPlayer . STATE _ENDED && model . nextEpisode != null && Preferences . autoplay ) {
2020-12-27 20:11:01 +01:00
playNextEpisode ( )
2020-11-13 15:36:12 +01:00
}
2020-10-11 13:18:20 +02:00
}
} )
2020-11-21 18:05:34 +01:00
2020-12-27 20:11:01 +01:00
// start playing the current episode, after all needed player components have been initialized
2021-03-11 19:11:12 +01:00
model . playEpisode ( model . currentEpisode , true )
2020-11-08 18:05:46 +01:00
}
@SuppressLint ( " ClickableViewAccessibility " )
private fun initVideoView ( ) {
2020-12-26 20:09:35 +01:00
video _view . player = model . player
2020-10-11 13:18:20 +02:00
2020-10-11 14:14:38 +02:00
// when the player controls get hidden, hide the bars too
video _view . setControllerVisibilityListener {
2020-11-21 18:05:34 +01:00
when ( it ) {
View . GONE -> hideBars ( )
View . VISIBLE -> updateControls ( )
2020-11-07 13:33:59 +01:00
}
}
2020-11-08 18:05:46 +01:00
video _view . setOnTouchListener { _ , event ->
gestureDetector . onTouchEvent ( event )
true
}
}
2020-11-05 20:07:41 +01:00
private fun initActions ( ) {
2021-01-06 15:07:31 +01:00
exo _close _player . setOnClickListener {
this . finish ( )
}
2020-11-16 19:23:06 +01:00
rwd _10 . setOnButtonClickListener { rewind ( ) }
2020-11-25 23:33:06 +01:00
ffwd _10 . setOnButtonClickListener { fastForward ( ) }
2020-11-13 15:36:12 +01:00
button _next _ep . setOnClickListener { playNextEpisode ( ) }
2020-12-20 20:21:27 +01:00
button _language . setOnClickListener { showLanguageSettings ( ) }
2020-12-14 23:46:55 +01:00
button _episodes . setOnClickListener { showEpisodesList ( ) }
2020-12-20 20:21:27 +01:00
button _next _ep _c . setOnClickListener { playNextEpisode ( ) }
2020-11-13 15:36:12 +01:00
}
2020-12-27 20:11:01 +01:00
private fun initGUI ( ) {
if ( model . media . type == DataTypes . MediaType . MOVIE ) {
button _episodes . visibility = View . GONE
}
}
2020-11-15 17:17:56 +01:00
private fun initTimeUpdates ( ) {
if ( this :: timerUpdates . isInitialized ) {
timerUpdates . cancel ( )
}
2020-11-21 18:05:34 +01:00
timerUpdates = Timer ( ) . scheduleAtFixedRate ( 0 , 500 ) {
2021-06-06 17:54:19 +02:00
lifecycleScope . launch {
2020-11-15 17:17:56 +01:00
var btnNextEpIsVisible : Boolean
2020-11-21 18:05:34 +01:00
var controlsVisible : Boolean
2020-11-13 15:36:12 +01:00
2020-12-26 20:09:35 +01:00
withContext ( Dispatchers . Main ) {
2020-12-27 20:41:18 +01:00
if ( model . player . duration > 0 ) {
remainingTime = model . player . duration - model . player . currentPosition
remainingTime = if ( remainingTime < 0 ) 0 else remainingTime
}
2020-11-21 18:05:34 +01:00
btnNextEpIsVisible = button _next _ep . isVisible
controlsVisible = controller . isVisible
}
2020-11-22 14:20:17 +01:00
if ( remainingTime in 1. . 20000 ) {
2021-01-06 15:07:31 +01:00
// if the next ep button is not visible, make it visible. Don't show in pip mode
if ( ! btnNextEpIsVisible && model . nextEpisode != null && Preferences . autoplay && !is InPiPMode ( ) ) {
2020-11-22 14:20:17 +01:00
withContext ( Dispatchers . Main ) { showButtonNextEp ( ) }
}
2020-11-21 18:05:34 +01:00
} else if ( btnNextEpIsVisible ) {
withContext ( Dispatchers . Main ) { hideButtonNextEp ( ) }
2020-11-15 13:39:33 +01:00
}
2020-11-15 17:17:56 +01:00
2020-11-21 18:05:34 +01:00
// if controls are visible, update them
if ( controlsVisible ) {
withContext ( Dispatchers . Main ) { updateControls ( ) }
2020-11-13 15:36:12 +01:00
}
}
}
2020-11-05 20:07:41 +01:00
}
2021-03-11 19:11:12 +01:00
private fun onPauseOnStop ( ) {
video _view ?. onPause ( )
2020-12-27 20:31:18 +01:00
model . player . pause ( )
2020-11-15 17:17:56 +01:00
timerUpdates . cancel ( )
2020-10-11 13:18:20 +02:00
}
2020-11-21 18:05:34 +01:00
/ * *
* update the custom controls
* /
private fun updateControls ( ) {
// update remaining time label
val hours = TimeUnit . MILLISECONDS . toHours ( remainingTime ) % 24
val minutes = TimeUnit . MILLISECONDS . toMinutes ( remainingTime ) % 60
val seconds = TimeUnit . MILLISECONDS . toSeconds ( remainingTime ) % 60
// if remaining time is below 60 minutes, don't show hours
exo _remaining . text = if ( TimeUnit . MILLISECONDS . toMinutes ( remainingTime ) < 60 ) {
getString ( R . string . time _min _sec , minutes , seconds )
} else {
getString ( R . string . time _hour _min _sec , hours , minutes , seconds )
}
}
2020-12-27 20:11:01 +01:00
/ * *
* update title text and next ep button visibility , set ignoreNextStateEnded
* /
private fun onMediaChanged ( ) {
exo _text _title . text = model . getMediaTitle ( )
2021-01-08 10:58:24 +01:00
// hide the next ep button, if there is none
2021-01-01 12:15:17 +01:00
button _next _ep _c . visibility = if ( model . nextEpisode == null ) {
View . GONE
} else {
View . VISIBLE
2020-12-27 20:11:01 +01:00
}
2021-01-08 10:58:24 +01:00
// hide the episodes button, if the media type changed
button _episodes . visibility = if ( model . media . type == DataTypes . MediaType . MOVIE ) {
View . GONE
} else {
View . VISIBLE
}
2020-12-27 20:11:01 +01:00
}
2020-11-20 11:20:11 +01:00
/ * *
* TODO set position of rewind / fast forward indicators programmatically
* /
2020-11-08 18:05:46 +01:00
private fun rewind ( ) {
2020-12-26 20:09:35 +01:00
model . seekToOffset ( rwdTime )
2020-11-20 11:20:11 +01:00
// hide/show needed components
exo _double _tap _indicator . visibility = View . VISIBLE
ffwd _10 _indicator . visibility = View . INVISIBLE
2020-11-21 18:05:34 +01:00
rwd _10 . visibility = View . INVISIBLE
2020-11-20 11:20:11 +01:00
rwd _10 _indicator . onAnimationEndCallback = {
exo _double _tap _indicator . visibility = View . GONE
ffwd _10 _indicator . visibility = View . VISIBLE
2020-11-21 18:05:34 +01:00
rwd _10 . visibility = View . VISIBLE
2020-11-20 11:20:11 +01:00
}
// run animation
rwd _10 _indicator . runOnClickAnimation ( )
2020-11-08 18:05:46 +01:00
}
2020-11-25 23:33:06 +01:00
private fun fastForward ( ) {
2020-12-26 20:09:35 +01:00
model . seekToOffset ( fwdTime )
2020-11-20 11:20:11 +01:00
// hide/show needed components
exo _double _tap _indicator . visibility = View . VISIBLE
rwd _10 _indicator . visibility = View . INVISIBLE
ffwd _10 . visibility = View . INVISIBLE
ffwd _10 _indicator . onAnimationEndCallback = {
exo _double _tap _indicator . visibility = View . GONE
rwd _10 _indicator . visibility = View . VISIBLE
ffwd _10 . visibility = View . VISIBLE
}
// run animation
ffwd _10 _indicator . runOnClickAnimation ( )
}
2020-12-27 20:11:01 +01:00
private fun playNextEpisode ( ) {
model . playNextEpisode ( )
2020-11-21 18:05:34 +01:00
hideButtonNextEp ( )
2020-12-02 11:14:09 +01:00
}
2020-11-15 13:39:33 +01:00
/ * *
* show the next episode button
* TODO improve the show animation
* /
private fun showButtonNextEp ( ) {
button _next _ep . visibility = View . VISIBLE
button _next _ep . alpha = 0.0f
button _next _ep . animate ( )
. alpha ( 1.0f )
. setListener ( null )
}
/ * *
* hide the next episode button
* TODO improve the hide animation
* /
private fun hideButtonNextEp ( ) {
button _next _ep . animate ( )
. alpha ( 0.0f )
. setListener ( object : AnimatorListenerAdapter ( ) {
2020-11-20 11:20:11 +01:00
override fun onAnimationEnd ( animation : Animator ? ) {
super . onAnimationEnd ( animation )
button _next _ep . visibility = View . GONE
}
} )
2020-11-15 13:39:33 +01:00
}
2020-12-14 23:46:55 +01:00
private fun showEpisodesList ( ) {
2020-12-15 23:15:14 +01:00
val episodesList = EpisodesListPlayer ( this , model = model ) . apply {
2020-12-26 20:09:35 +01:00
onViewRemovedAction = { model . player . play ( ) }
2020-12-15 23:15:14 +01:00
}
player _layout . addView ( episodesList )
2020-12-30 20:08:19 +01:00
pauseAndHideControls ( )
2020-12-20 20:21:27 +01:00
}
private fun showLanguageSettings ( ) {
val languageSettings = LanguageSettingsPlayer ( this , model = model ) . apply {
2020-12-26 20:09:35 +01:00
onViewRemovedAction = { model . player . play ( ) }
2020-12-20 20:21:27 +01:00
}
player _layout . addView ( languageSettings )
2020-12-30 20:08:19 +01:00
pauseAndHideControls ( )
}
2020-12-20 20:21:27 +01:00
2020-12-30 20:08:19 +01:00
/ * *
* pause playback and hide controls
* /
private fun pauseAndHideControls ( ) {
model . player . pause ( ) // showTimeoutMs is set to 0 when calling pause, but why
controller . showTimeoutMs = defaultShowTimeoutMs // fix showTimeoutMs set to 0
controller . hide ( )
2020-12-14 23:46:55 +01:00
}
2020-11-08 18:05:46 +01:00
inner class PlayerGestureListener : GestureDetector . SimpleOnGestureListener ( ) {
/ * *
* on single tap hide or show the controls
* /
override fun onSingleTapConfirmed ( e : MotionEvent ? ) : Boolean {
2021-01-06 15:07:31 +01:00
if ( !is InPiPMode ( ) ) {
if ( controller . isVisible ) controller . hide ( ) else controller . show ( )
}
2020-11-08 18:05:46 +01:00
return true
}
/ * *
* on double tap rewind or forward
* /
override fun onDoubleTap ( e : MotionEvent ? ) : Boolean {
2020-11-15 17:17:56 +01:00
val eventPosX = e ?. x ?. toInt ( ) ?: 0
val viewCenterX = video _view . measuredWidth / 2
2020-11-08 18:05:46 +01:00
// if the event position is on the left side rewind, if it's on the right forward
2020-12-15 23:15:14 +01:00
if ( eventPosX < viewCenterX ) rewind ( ) else fastForward ( )
2020-11-08 18:05:46 +01:00
return true
}
/ * *
* not used
* /
override fun onDoubleTapEvent ( e : MotionEvent ? ) : Boolean {
return true
}
2020-11-21 18:05:34 +01:00
/ * *
* on long press toggle pause / play
* /
2020-11-20 11:20:11 +01:00
override fun onLongPress ( e : MotionEvent ? ) {
2020-12-26 20:09:35 +01:00
model . togglePausePlay ( )
2020-11-20 11:20:11 +01:00
}
2020-11-08 18:05:46 +01:00
}
2020-10-11 13:18:20 +02:00
}