1240 lines
48 KiB
Kotlin
1240 lines
48 KiB
Kotlin
package com.lagradost.cloudstream3.ui.player
|
|
|
|
import android.animation.ObjectAnimator
|
|
import android.annotation.SuppressLint
|
|
import android.content.Context
|
|
import android.content.pm.ActivityInfo
|
|
import android.content.res.ColorStateList
|
|
import android.content.res.Resources
|
|
import android.graphics.Color
|
|
import android.media.AudioManager
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.provider.Settings
|
|
import android.text.Editable
|
|
import android.util.DisplayMetrics
|
|
import android.view.KeyEvent
|
|
import android.view.MotionEvent
|
|
import android.view.View
|
|
import android.view.WindowManager
|
|
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
|
import android.view.animation.AlphaAnimation
|
|
import android.view.animation.Animation
|
|
import android.view.animation.AnimationUtils
|
|
import android.widget.EditText
|
|
import android.widget.ImageView
|
|
import android.widget.TextView
|
|
import androidx.appcompat.app.AlertDialog
|
|
import androidx.core.graphics.blue
|
|
import androidx.core.graphics.green
|
|
import androidx.core.graphics.red
|
|
import androidx.core.view.isGone
|
|
import androidx.core.view.isVisible
|
|
import androidx.core.widget.doOnTextChanged
|
|
import androidx.preference.PreferenceManager
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
|
import com.lagradost.cloudstream3.CommonActivity.keyEventListener
|
|
import com.lagradost.cloudstream3.CommonActivity.playerEventListener
|
|
import com.lagradost.cloudstream3.R
|
|
import com.lagradost.cloudstream3.mvvm.logError
|
|
import com.lagradost.cloudstream3.utils.Qualities
|
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
|
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
|
import com.lagradost.cloudstream3.utils.UIHelper.getNavigationBarHeight
|
|
import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight
|
|
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
|
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
|
import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI
|
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
|
import com.lagradost.cloudstream3.utils.Vector2
|
|
import kotlinx.android.synthetic.main.player_custom_layout.*
|
|
import kotlin.math.*
|
|
|
|
const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking
|
|
const val MINIMUM_VERTICAL_SWIPE = 2.0f // in percentage
|
|
const val MINIMUM_HORIZONTAL_SWIPE = 2.0f // in percentage
|
|
const val VERTICAL_MULTIPLIER = 2.0f
|
|
const val HORIZONTAL_MULTIPLIER = 2.0f
|
|
const val DOUBLE_TAB_MAXIMUM_HOLD_TIME = 200L
|
|
const val DOUBLE_TAB_MINIMUM_TIME_BETWEEN = 200L // this also affects the UI show response time
|
|
const val DOUBLE_TAB_PAUSE_PERCENTAGE = 0.15 // in both directions
|
|
|
|
// All the UI Logic for the player
|
|
open class FullScreenPlayer : AbstractPlayerFragment() {
|
|
// state of player UI
|
|
protected var isShowing = false
|
|
protected var isLocked = false
|
|
|
|
// options for player
|
|
protected var currentPrefQuality =
|
|
Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell
|
|
protected var fastForwardTime = 10000L
|
|
protected var swipeHorizontalEnabled = false
|
|
protected var swipeVerticalEnabled = false
|
|
protected var playBackSpeedEnabled = false
|
|
protected var playerResizeEnabled = false
|
|
protected var doubleTapEnabled = false
|
|
protected var doubleTapPauseEnabled = true
|
|
|
|
protected var subtitleDelay
|
|
set(value) = try {
|
|
player.setSubtitleOffset(-value)
|
|
} catch (e: Exception) {
|
|
logError(e)
|
|
}
|
|
get() = try {
|
|
-player.getSubtitleOffset()
|
|
} catch (e: Exception) {
|
|
logError(e)
|
|
0L
|
|
}
|
|
|
|
//private var useSystemBrightness = false
|
|
protected var useTrueSystemBrightness = true
|
|
private val fullscreenNotch = true //TODO SETTING
|
|
|
|
protected val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics
|
|
|
|
// screenWidth and screenHeight does always
|
|
// refer to the screen while in landscape mode
|
|
private val screenWidth: Int
|
|
get() {
|
|
return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
|
|
}
|
|
private val screenHeight: Int
|
|
get() {
|
|
return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
|
|
}
|
|
|
|
private var statusBarHeight: Int? = null
|
|
private var navigationBarHeight: Int? = null
|
|
|
|
private val brightnessIcons = listOf(
|
|
R.drawable.sun_1,
|
|
R.drawable.sun_2,
|
|
R.drawable.sun_3,
|
|
R.drawable.sun_4,
|
|
R.drawable.sun_5,
|
|
R.drawable.sun_6,
|
|
//R.drawable.sun_7,
|
|
// R.drawable.ic_baseline_brightness_1_24,
|
|
// R.drawable.ic_baseline_brightness_2_24,
|
|
// R.drawable.ic_baseline_brightness_3_24,
|
|
// R.drawable.ic_baseline_brightness_4_24,
|
|
// R.drawable.ic_baseline_brightness_5_24,
|
|
// R.drawable.ic_baseline_brightness_6_24,
|
|
// R.drawable.ic_baseline_brightness_7_24,
|
|
)
|
|
|
|
private val volumeIcons = listOf(
|
|
R.drawable.ic_baseline_volume_mute_24,
|
|
R.drawable.ic_baseline_volume_down_24,
|
|
R.drawable.ic_baseline_volume_up_24,
|
|
)
|
|
|
|
open fun showMirrorsDialogue() {
|
|
throw NotImplementedError()
|
|
}
|
|
|
|
/** Returns false if the touch is on the status bar or navigation bar*/
|
|
private fun isValidTouch(rawX: Float, rawY: Float): Boolean {
|
|
val statusHeight = statusBarHeight ?: 0
|
|
// val navHeight = navigationBarHeight ?: 0
|
|
// nav height is removed because screenWidth already takes into account that
|
|
return rawY > statusHeight && rawX < screenWidth //- navHeight
|
|
}
|
|
|
|
override fun exitedPipMode() {
|
|
animateLayoutChanges()
|
|
}
|
|
|
|
private fun animateLayoutChanges() {
|
|
if (isShowing) {
|
|
updateUIVisibility()
|
|
} else {
|
|
player_holder?.postDelayed({ updateUIVisibility() }, 200)
|
|
}
|
|
|
|
val titleMove = if (isShowing) 0f else -50.toPx.toFloat()
|
|
player_video_title?.let {
|
|
ObjectAnimator.ofFloat(it, "translationY", titleMove).apply {
|
|
duration = 200
|
|
start()
|
|
}
|
|
}
|
|
player_video_title_rez?.let {
|
|
ObjectAnimator.ofFloat(it, "translationY", titleMove).apply {
|
|
duration = 200
|
|
start()
|
|
}
|
|
}
|
|
val playerBarMove = if (isShowing) 0f else 50.toPx.toFloat()
|
|
bottom_player_bar?.let {
|
|
ObjectAnimator.ofFloat(it, "translationY", playerBarMove).apply {
|
|
duration = 200
|
|
start()
|
|
}
|
|
}
|
|
|
|
val fadeTo = if (isShowing) 1f else 0f
|
|
val fadeAnimation = AlphaAnimation(1f - fadeTo, fadeTo)
|
|
|
|
fadeAnimation.duration = 100
|
|
fadeAnimation.fillAfter = true
|
|
|
|
val sView = subView
|
|
val sStyle = subStyle
|
|
if (sView != null && sStyle != null) {
|
|
val move = if (isShowing) -((bottom_player_bar?.height?.toFloat()
|
|
?: 0f) + 40.toPx) else -sStyle.elevation.toPx.toFloat()
|
|
ObjectAnimator.ofFloat(sView, "translationY", move).apply {
|
|
duration = 200
|
|
start()
|
|
}
|
|
}
|
|
|
|
if (!isLocked) {
|
|
player_ffwd_holder?.alpha = 1f
|
|
player_rew_holder?.alpha = 1f
|
|
// player_pause_play_holder?.alpha = 1f
|
|
|
|
shadow_overlay?.startAnimation(fadeAnimation)
|
|
player_ffwd_holder?.startAnimation(fadeAnimation)
|
|
player_rew_holder?.startAnimation(fadeAnimation)
|
|
player_pause_play?.startAnimation(fadeAnimation)
|
|
|
|
/*if (isBuffering) {
|
|
player_pause_play?.isVisible = false
|
|
player_pause_play_holder?.isVisible = false
|
|
} else {
|
|
player_pause_play?.isVisible = true
|
|
player_pause_play_holder?.startAnimation(fadeAnimation)
|
|
player_pause_play?.startAnimation(fadeAnimation)
|
|
}*/
|
|
//player_buffering?.startAnimation(fadeAnimation)
|
|
}
|
|
|
|
bottom_player_bar?.startAnimation(fadeAnimation)
|
|
player_top_holder?.startAnimation(fadeAnimation)
|
|
}
|
|
|
|
override fun subtitlesChanged() {
|
|
player_subtitle_offset_btt?.isGone = player.getCurrentPreferredSubtitle() == null
|
|
}
|
|
|
|
override fun onResume() {
|
|
activity?.hideSystemUI()
|
|
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && fullscreenNotch) {
|
|
val params = activity?.window?.attributes
|
|
params?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
|
activity?.window?.attributes = params
|
|
}
|
|
|
|
super.onResume()
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
activity?.showSystemUI()
|
|
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
|
|
|
|
// simply resets brightness and notch settings that might have been overridden
|
|
val lp = activity?.window?.attributes
|
|
lp?.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
lp?.layoutInDisplayCutoutMode =
|
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
|
|
}
|
|
activity?.window?.attributes = lp
|
|
super.onDestroy()
|
|
}
|
|
|
|
private fun setPlayBackSpeed(speed: Float) {
|
|
try {
|
|
setKey(PLAYBACK_SPEED_KEY, speed)
|
|
player_speed_btt?.text =
|
|
getString(R.string.player_speed_text_format).format(speed)
|
|
.replace(".0x", "x")
|
|
} catch (e: Exception) {
|
|
// the format string was wrong
|
|
logError(e)
|
|
}
|
|
|
|
player.setPlaybackSpeed(speed)
|
|
}
|
|
|
|
private fun skipOp() {
|
|
player.seekTime(85000) // skip 85s
|
|
}
|
|
|
|
private fun showSubtitleOffsetDialog() {
|
|
context?.let { ctx ->
|
|
val builder =
|
|
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
|
|
.setView(R.layout.subtitle_offset)
|
|
val dialog = builder.create()
|
|
dialog.show()
|
|
|
|
val beforeOffset = subtitleDelay
|
|
|
|
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
|
|
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
|
|
val input = dialog.findViewById<EditText>(R.id.subtitle_offset_input)!!
|
|
val sub = dialog.findViewById<ImageView>(R.id.subtitle_offset_subtract)!!
|
|
val add = dialog.findViewById<ImageView>(R.id.subtitle_offset_add)!!
|
|
val subTitle = dialog.findViewById<TextView>(R.id.subtitle_offset_sub_title)!!
|
|
|
|
input.doOnTextChanged { text, _, _, _ ->
|
|
text?.toString()?.toLongOrNull()?.let {
|
|
subtitleDelay = it
|
|
when {
|
|
it > 0L -> {
|
|
context?.getString(R.string.subtitle_offset_extra_hint_later_format)
|
|
?.format(it)
|
|
}
|
|
it < 0L -> {
|
|
context?.getString(R.string.subtitle_offset_extra_hint_before_format)
|
|
?.format(-it)
|
|
}
|
|
it == 0L -> {
|
|
context?.getString(R.string.subtitle_offset_extra_hint_none_format)
|
|
}
|
|
else -> {
|
|
null
|
|
}
|
|
}?.let { str ->
|
|
subTitle.text = str
|
|
}
|
|
}
|
|
}
|
|
input.text = Editable.Factory.getInstance()?.newEditable(beforeOffset.toString())
|
|
|
|
val buttonChange = 100L
|
|
|
|
fun changeBy(by: Long) {
|
|
val current = (input.text?.toString()?.toLongOrNull() ?: 0) + by
|
|
input.text = Editable.Factory.getInstance()?.newEditable(current.toString())
|
|
}
|
|
|
|
add.setOnClickListener {
|
|
changeBy(buttonChange)
|
|
}
|
|
|
|
sub.setOnClickListener {
|
|
changeBy(-buttonChange)
|
|
}
|
|
|
|
dialog.setOnDismissListener {
|
|
activity?.hideSystemUI()
|
|
}
|
|
applyButton.setOnClickListener {
|
|
dialog.dismissSafe(activity)
|
|
player.seekTime(1L)
|
|
}
|
|
cancelButton.setOnClickListener {
|
|
subtitleDelay = beforeOffset
|
|
dialog.dismissSafe(activity)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun showSpeedDialog() {
|
|
val speedsText =
|
|
listOf(
|
|
"0.5x",
|
|
"0.75x",
|
|
"0.85x",
|
|
"1x",
|
|
"1.15x",
|
|
"1.25x",
|
|
"1.4x",
|
|
"1.5x",
|
|
"1.75x",
|
|
"2x"
|
|
)
|
|
val speedsNumbers =
|
|
listOf(0.5f, 0.75f, 0.85f, 1f, 1.15f, 1.25f, 1.4f, 1.5f, 1.75f, 2f)
|
|
val speedIndex = speedsNumbers.indexOf(player.getPlaybackSpeed())
|
|
|
|
activity?.let { act ->
|
|
act.showDialog(
|
|
speedsText,
|
|
speedIndex,
|
|
act.getString(R.string.player_speed),
|
|
false,
|
|
{
|
|
activity?.hideSystemUI()
|
|
}) { index ->
|
|
activity?.hideSystemUI()
|
|
setPlayBackSpeed(speedsNumbers[index])
|
|
}
|
|
}
|
|
}
|
|
|
|
fun resetRewindText() {
|
|
exo_rew_text?.text =
|
|
getString(R.string.rew_text_regular_format).format(fastForwardTime / 1000)
|
|
}
|
|
|
|
fun resetFastForwardText() {
|
|
exo_ffwd_text?.text =
|
|
getString(R.string.ffw_text_regular_format).format(fastForwardTime / 1000)
|
|
}
|
|
|
|
private fun rewind() {
|
|
try {
|
|
player_center_menu?.isGone = false
|
|
player_rew_holder?.alpha = 1f
|
|
|
|
val rotateLeft = AnimationUtils.loadAnimation(context, R.anim.rotate_left)
|
|
exo_rew?.startAnimation(rotateLeft)
|
|
|
|
val goLeft = AnimationUtils.loadAnimation(context, R.anim.go_left)
|
|
goLeft.setAnimationListener(object : Animation.AnimationListener {
|
|
override fun onAnimationStart(animation: Animation?) {}
|
|
|
|
override fun onAnimationRepeat(animation: Animation?) {}
|
|
|
|
override fun onAnimationEnd(animation: Animation?) {
|
|
exo_rew_text?.post {
|
|
resetRewindText()
|
|
player_center_menu?.isGone = !isShowing
|
|
player_rew_holder?.alpha = if (isShowing) 1f else 0f
|
|
}
|
|
}
|
|
})
|
|
exo_rew_text?.startAnimation(goLeft)
|
|
exo_rew_text?.text = getString(R.string.rew_text_format).format(fastForwardTime / 1000)
|
|
player.seekTime(-fastForwardTime)
|
|
} catch (e: Exception) {
|
|
logError(e)
|
|
}
|
|
}
|
|
|
|
private fun fastForward() {
|
|
try {
|
|
player_center_menu?.isGone = false
|
|
player_ffwd_holder?.alpha = 1f
|
|
|
|
val rotateRight = AnimationUtils.loadAnimation(context, R.anim.rotate_right)
|
|
exo_ffwd?.startAnimation(rotateRight)
|
|
|
|
val goRight = AnimationUtils.loadAnimation(context, R.anim.go_right)
|
|
goRight.setAnimationListener(object : Animation.AnimationListener {
|
|
override fun onAnimationStart(animation: Animation?) {}
|
|
|
|
override fun onAnimationRepeat(animation: Animation?) {}
|
|
|
|
override fun onAnimationEnd(animation: Animation?) {
|
|
exo_ffwd_text?.post {
|
|
resetFastForwardText()
|
|
player_center_menu?.isGone = !isShowing
|
|
player_ffwd_holder?.alpha = if (isShowing) 1f else 0f
|
|
}
|
|
}
|
|
})
|
|
exo_ffwd_text?.startAnimation(goRight)
|
|
exo_ffwd_text?.text = getString(R.string.ffw_text_format).format(fastForwardTime / 1000)
|
|
player.seekTime(fastForwardTime)
|
|
} catch (e: Exception) {
|
|
logError(e)
|
|
}
|
|
}
|
|
|
|
private fun onClickChange() {
|
|
isShowing = !isShowing
|
|
if (isShowing) {
|
|
autoHide()
|
|
}
|
|
activity?.hideSystemUI()
|
|
animateLayoutChanges()
|
|
player_pause_play?.requestFocus()
|
|
}
|
|
|
|
private fun toggleLock() {
|
|
if (!isShowing) {
|
|
onClickChange()
|
|
}
|
|
|
|
isLocked = !isLocked
|
|
if (isLocked && isShowing) {
|
|
player_holder?.postDelayed({
|
|
if (isLocked && isShowing) {
|
|
onClickChange()
|
|
}
|
|
}, 200)
|
|
}
|
|
|
|
val fadeTo = if (isLocked) 0f else 1f
|
|
|
|
val fadeAnimation = AlphaAnimation(player_video_title.alpha, fadeTo).apply {
|
|
duration = 100
|
|
fillAfter = true
|
|
}
|
|
|
|
updateUIVisibility()
|
|
// MENUS
|
|
//centerMenu.startAnimation(fadeAnimation)
|
|
player_pause_play?.startAnimation(fadeAnimation)
|
|
player_ffwd_holder?.startAnimation(fadeAnimation)
|
|
player_rew_holder?.startAnimation(fadeAnimation)
|
|
//player_media_route_button?.startAnimation(fadeAnimation)
|
|
//video_bar.startAnimation(fadeAnimation)
|
|
|
|
//TITLE
|
|
player_video_title_rez?.startAnimation(fadeAnimation)
|
|
player_episode_filler?.startAnimation(fadeAnimation)
|
|
player_video_title?.startAnimation(fadeAnimation)
|
|
player_top_holder?.startAnimation(fadeAnimation)
|
|
// BOTTOM
|
|
player_lock_holder?.startAnimation(fadeAnimation)
|
|
//player_go_back_holder?.startAnimation(fadeAnimation)
|
|
|
|
shadow_overlay?.startAnimation(fadeAnimation)
|
|
|
|
updateLockUI()
|
|
}
|
|
|
|
private fun updateUIVisibility() {
|
|
val isGone = isLocked || !isShowing
|
|
var togglePlayerTitleGone = isGone
|
|
context?.let {
|
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it)
|
|
val limitTitle = settingsManager.getInt(getString(R.string.prefer_limit_title_key), 0)
|
|
if (limitTitle < 0) {
|
|
togglePlayerTitleGone = true
|
|
}
|
|
}
|
|
player_lock_holder?.isGone = isGone
|
|
player_video_bar?.isGone = isGone
|
|
player_pause_play_holder?.isGone = isGone
|
|
player_pause_play?.isGone = isGone
|
|
//player_buffering?.isGone = isGone
|
|
player_top_holder?.isGone = isGone
|
|
player_video_title?.isGone = togglePlayerTitleGone
|
|
player_video_title_rez?.isGone = isGone
|
|
player_episode_filler?.isGone = isGone
|
|
player_center_menu?.isGone = isGone
|
|
player_lock?.isGone = !isShowing
|
|
//player_media_route_button?.isClickable = !isGone
|
|
player_go_back_holder?.isGone = isGone
|
|
}
|
|
|
|
private fun updateLockUI() {
|
|
player_lock?.setIconResource(if (isLocked) R.drawable.video_locked else R.drawable.video_unlocked)
|
|
if (layout == R.layout.fragment_player) {
|
|
val color = if (isLocked) context?.colorFromAttribute(R.attr.colorPrimary)
|
|
else Color.WHITE
|
|
if (color != null) {
|
|
player_lock?.setTextColor(color)
|
|
player_lock?.iconTint = ColorStateList.valueOf(color)
|
|
player_lock?.rippleColor =
|
|
ColorStateList.valueOf(Color.argb(50, color.red, color.green, color.blue))
|
|
}
|
|
}
|
|
}
|
|
|
|
private var currentTapIndex = 0
|
|
protected fun autoHide() {
|
|
currentTapIndex++
|
|
val index = currentTapIndex
|
|
player_holder?.postDelayed({
|
|
if (!isCurrentTouchValid && isShowing && index == currentTapIndex && player.getIsPlaying()) {
|
|
onClickChange()
|
|
}
|
|
}, 2000)
|
|
}
|
|
|
|
// this is used because you don't want to hide UI when double tap seeking
|
|
private var currentDoubleTapIndex = 0
|
|
private fun toggleShowDelayed() {
|
|
if (doubleTapEnabled || doubleTapPauseEnabled) {
|
|
val index = currentDoubleTapIndex
|
|
player_holder?.postDelayed({
|
|
if (index == currentDoubleTapIndex) {
|
|
onClickChange()
|
|
}
|
|
}, DOUBLE_TAB_MINIMUM_TIME_BETWEEN)
|
|
} else {
|
|
onClickChange()
|
|
}
|
|
}
|
|
|
|
private var isCurrentTouchValid = false
|
|
private var currentTouchStart: Vector2? = null
|
|
private var currentTouchLast: Vector2? = null
|
|
private var currentTouchAction: TouchAction? = null
|
|
private var currentLastTouchAction: TouchAction? = null
|
|
private var currentTouchStartPlayerTime: Long? =
|
|
null // the time in the player when you first click
|
|
private var currentTouchStartTime: Long? = null // the system time when you first click
|
|
private var currentLastTouchEndTime: Long = 0 // the system time when you released your finger
|
|
private var currentClickCount: Int =
|
|
0 // amount of times you have double clicked, will reset when other action is taken
|
|
|
|
// requested volume and brightness is used to make swiping smoother
|
|
// to make it not jump between values,
|
|
// this value is within the range [0,1]
|
|
private var currentRequestedVolume: Float = 0.0f
|
|
private var currentRequestedBrightness: Float = 1.0f
|
|
|
|
enum class TouchAction {
|
|
Brightness,
|
|
Volume,
|
|
Time,
|
|
}
|
|
|
|
companion object {
|
|
private fun forceLetters(inp: Long, letters: Int = 2): String {
|
|
val added: Int = letters - inp.toString().length
|
|
return if (added > 0) {
|
|
"0".repeat(added) + inp.toString()
|
|
} else {
|
|
inp.toString()
|
|
}
|
|
}
|
|
|
|
private fun convertTimeToString(sec: Long): String {
|
|
val rsec = sec % 60L
|
|
val min = ceil((sec - rsec) / 60.0).toInt()
|
|
val rmin = min % 60L
|
|
val h = ceil((min - rmin) / 60.0).toLong()
|
|
//int rh = h;// h % 24;
|
|
return (if (h > 0) forceLetters(h) + ":" else "") + (if (rmin >= 0 || h >= 0) forceLetters(
|
|
rmin
|
|
) + ":" else "") + forceLetters(
|
|
rsec
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun calculateNewTime(
|
|
startTime: Long?,
|
|
touchStart: Vector2?,
|
|
touchEnd: Vector2?
|
|
): Long? {
|
|
if (touchStart == null || touchEnd == null || startTime == null) return null
|
|
val diffX = (touchEnd.x - touchStart.x) * HORIZONTAL_MULTIPLIER / screenWidth.toFloat()
|
|
val duration = player.getDuration() ?: return null
|
|
return max(
|
|
min(
|
|
startTime + ((duration * (diffX * diffX)) * (if (diffX < 0) -1 else 1)).toLong(),
|
|
duration
|
|
), 0
|
|
)
|
|
}
|
|
|
|
private fun getBrightness(): Float? {
|
|
return if (useTrueSystemBrightness) {
|
|
try {
|
|
Settings.System.getInt(
|
|
context?.contentResolver,
|
|
Settings.System.SCREEN_BRIGHTNESS
|
|
) / 255f
|
|
} catch (e: Exception) {
|
|
// because true system brightness requires
|
|
// permission, this is a lazy way to check
|
|
// as it will throw an error if we do not have it
|
|
useTrueSystemBrightness = false
|
|
return getBrightness()
|
|
}
|
|
} else {
|
|
try {
|
|
activity?.window?.attributes?.screenBrightness
|
|
} catch (e: Exception) {
|
|
logError(e)
|
|
null
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun setBrightness(brightness: Float) {
|
|
if (useTrueSystemBrightness) {
|
|
try {
|
|
Settings.System.putInt(
|
|
context?.contentResolver,
|
|
Settings.System.SCREEN_BRIGHTNESS_MODE,
|
|
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL
|
|
)
|
|
|
|
Settings.System.putInt(
|
|
context?.contentResolver,
|
|
Settings.System.SCREEN_BRIGHTNESS, (brightness * 255).toInt()
|
|
)
|
|
} catch (e: Exception) {
|
|
useTrueSystemBrightness = false
|
|
setBrightness(brightness)
|
|
}
|
|
} else {
|
|
try {
|
|
val lp = activity?.window?.attributes
|
|
lp?.screenBrightness = brightness
|
|
activity?.window?.attributes = lp
|
|
} catch (e: Exception) {
|
|
logError(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
@SuppressLint("SetTextI18n")
|
|
private fun handleMotionEvent(view: View?, event: MotionEvent?): Boolean {
|
|
if (event == null || view == null) return false
|
|
val currentTouch = Vector2(event.x, event.y)
|
|
val startTouch = currentTouchStart
|
|
|
|
when (event.action) {
|
|
MotionEvent.ACTION_DOWN -> {
|
|
// validates if the touch is inside of the player area
|
|
isCurrentTouchValid = isValidTouch(currentTouch.x, currentTouch.y)
|
|
if (isCurrentTouchValid) {
|
|
currentTouchStartTime = System.currentTimeMillis()
|
|
currentTouchStart = currentTouch
|
|
currentTouchLast = currentTouch
|
|
currentTouchStartPlayerTime = player.getPosition()
|
|
|
|
getBrightness()?.let {
|
|
currentRequestedBrightness = it
|
|
}
|
|
(activity?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager)?.let { audioManager ->
|
|
val currentVolume =
|
|
audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
|
val maxVolume =
|
|
audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
|
|
|
currentRequestedVolume = currentVolume.toFloat() / maxVolume.toFloat()
|
|
}
|
|
}
|
|
}
|
|
MotionEvent.ACTION_UP -> {
|
|
if (isCurrentTouchValid && !isLocked) {
|
|
// seek time
|
|
if (swipeHorizontalEnabled && currentTouchAction == TouchAction.Time) {
|
|
val startTime = currentTouchStartPlayerTime
|
|
if (startTime != null) {
|
|
calculateNewTime(startTime, startTouch, currentTouch)?.let { seekTo ->
|
|
if (abs(seekTo - startTime) > MINIMUM_SEEK_TIME) {
|
|
player.seekTo(seekTo)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// see if click is eligible for seek 10s
|
|
val holdTime = currentTouchStartTime?.minus(System.currentTimeMillis())
|
|
if (isCurrentTouchValid // is valid
|
|
&& currentTouchAction == null // no other action like swiping is taking place
|
|
&& currentLastTouchAction == null // last action was none, this prevents mis input random seek
|
|
&& holdTime != null
|
|
&& holdTime < DOUBLE_TAB_MAXIMUM_HOLD_TIME // it is a click not a long hold
|
|
) {
|
|
if (!isLocked
|
|
&& (System.currentTimeMillis() - currentLastTouchEndTime) < DOUBLE_TAB_MINIMUM_TIME_BETWEEN // the time since the last action is short
|
|
) {
|
|
currentClickCount++
|
|
|
|
if (currentClickCount >= 1) { // have double clicked
|
|
currentDoubleTapIndex++
|
|
if (doubleTapPauseEnabled) { // you can pause if your tap is in the middle of the screen
|
|
when {
|
|
currentTouch.x < screenWidth / 2 - (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> {
|
|
if (doubleTapEnabled)
|
|
rewind()
|
|
}
|
|
currentTouch.x > screenWidth / 2 + (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> {
|
|
if (doubleTapEnabled)
|
|
fastForward()
|
|
}
|
|
else -> {
|
|
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
|
|
}
|
|
}
|
|
} else if (doubleTapEnabled) {
|
|
if (currentTouch.x < screenWidth / 2) {
|
|
rewind()
|
|
} else {
|
|
fastForward()
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// is a valid click but not fast enough for seek
|
|
currentClickCount = 0
|
|
toggleShowDelayed()
|
|
//onClickChange()
|
|
}
|
|
} else {
|
|
currentClickCount = 0
|
|
}
|
|
|
|
// call auto hide as it wont hide when you have your finger down
|
|
autoHide()
|
|
|
|
// reset variables
|
|
isCurrentTouchValid = false
|
|
currentTouchStart = null
|
|
currentLastTouchAction = currentTouchAction
|
|
currentTouchAction = null
|
|
currentTouchStartPlayerTime = null
|
|
currentTouchLast = null
|
|
currentTouchStartTime = null
|
|
|
|
// resets UI
|
|
player_time_text?.isVisible = false
|
|
player_progressbar_left_holder?.isVisible = false
|
|
player_progressbar_right_holder?.isVisible = false
|
|
currentLastTouchEndTime = System.currentTimeMillis()
|
|
}
|
|
MotionEvent.ACTION_MOVE -> {
|
|
// if current touch is valid
|
|
if (startTouch != null && isCurrentTouchValid && !isLocked) {
|
|
// action is unassigned and can therefore be assigned
|
|
if (currentTouchAction == null) {
|
|
val diffFromStart = startTouch - currentTouch
|
|
|
|
if (swipeVerticalEnabled) {
|
|
if (abs(diffFromStart.y * 100 / screenHeight) > MINIMUM_VERTICAL_SWIPE) {
|
|
// left = Brightness, right = Volume, but the UI is reversed to show the UI better
|
|
currentTouchAction = if (startTouch.x < screenWidth / 2) {
|
|
// hide the UI if you hold brightness to show screen better, better UX
|
|
if (isShowing) {
|
|
isShowing = false
|
|
animateLayoutChanges()
|
|
}
|
|
|
|
TouchAction.Brightness
|
|
} else {
|
|
TouchAction.Volume
|
|
}
|
|
}
|
|
}
|
|
if (swipeHorizontalEnabled) {
|
|
if (abs(diffFromStart.x * 100 / screenHeight) > MINIMUM_HORIZONTAL_SWIPE) {
|
|
currentTouchAction = TouchAction.Time
|
|
}
|
|
}
|
|
}
|
|
|
|
// display action
|
|
val lastTouch = currentTouchLast
|
|
if (lastTouch != null) {
|
|
val diffFromLast = lastTouch - currentTouch
|
|
val verticalAddition =
|
|
diffFromLast.y * VERTICAL_MULTIPLIER / screenHeight.toFloat()
|
|
|
|
// update UI
|
|
player_time_text?.isVisible = false
|
|
player_progressbar_left_holder?.isVisible = false
|
|
player_progressbar_right_holder?.isVisible = false
|
|
|
|
when (currentTouchAction) {
|
|
TouchAction.Time -> {
|
|
// this simply updates UI as the seek logic happens on release
|
|
// startTime is rounded to make the UI sync in a nice way
|
|
val startTime =
|
|
currentTouchStartPlayerTime?.div(1000L)?.times(1000L)
|
|
if (startTime != null) {
|
|
calculateNewTime(
|
|
startTime,
|
|
startTouch,
|
|
currentTouch
|
|
)?.let { newMs ->
|
|
val skipMs = newMs - startTime
|
|
player_time_text?.text =
|
|
"${convertTimeToString(newMs / 1000)} [${
|
|
(if (abs(skipMs) < 1000) "" else (if (skipMs > 0) "+" else "-"))
|
|
}${convertTimeToString(abs(skipMs / 1000))}]"
|
|
player_time_text?.isVisible = true
|
|
}
|
|
}
|
|
}
|
|
TouchAction.Brightness -> {
|
|
player_progressbar_right_holder?.isVisible = true
|
|
val lastRequested = currentRequestedBrightness
|
|
currentRequestedBrightness =
|
|
min(
|
|
1.0f,
|
|
max(currentRequestedBrightness + verticalAddition, 0.0f)
|
|
)
|
|
|
|
// this is to not spam request it, just in case it fucks over someone
|
|
if (lastRequested != currentRequestedBrightness)
|
|
setBrightness(currentRequestedBrightness)
|
|
|
|
// max is set high to make it smooth
|
|
player_progressbar_right?.max = 100_000
|
|
player_progressbar_right?.progress =
|
|
max(2_000, (currentRequestedBrightness * 100_000f).toInt())
|
|
|
|
player_progressbar_right_icon?.setImageResource(
|
|
brightnessIcons[min( // clamp the value just in case
|
|
brightnessIcons.size - 1,
|
|
max(
|
|
0,
|
|
round(currentRequestedBrightness * (brightnessIcons.size - 1)).toInt()
|
|
)
|
|
)]
|
|
)
|
|
}
|
|
TouchAction.Volume -> {
|
|
(activity?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager)?.let { audioManager ->
|
|
player_progressbar_left_holder?.isVisible = true
|
|
val maxVolume =
|
|
audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
|
val currentVolume =
|
|
audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
|
|
|
// clamps volume and adds swipe
|
|
currentRequestedVolume =
|
|
min(
|
|
1.0f,
|
|
max(currentRequestedVolume + verticalAddition, 0.0f)
|
|
)
|
|
|
|
// max is set high to make it smooth
|
|
player_progressbar_left?.max = 100_000
|
|
player_progressbar_left?.progress =
|
|
max(2_000, (currentRequestedVolume * 100_000f).toInt())
|
|
|
|
player_progressbar_left_icon?.setImageResource(
|
|
volumeIcons[min( // clamp the value just in case
|
|
volumeIcons.size - 1,
|
|
max(
|
|
0,
|
|
round(currentRequestedVolume * (volumeIcons.size - 1)).toInt()
|
|
)
|
|
)]
|
|
)
|
|
|
|
// this is used instead of set volume because old devices does not support it
|
|
val desiredVolume =
|
|
round(currentRequestedVolume * maxVolume).toInt()
|
|
if (desiredVolume != currentVolume) {
|
|
val newVolumeAdjusted =
|
|
if (desiredVolume < currentVolume) AudioManager.ADJUST_LOWER else AudioManager.ADJUST_RAISE
|
|
|
|
audioManager.adjustStreamVolume(
|
|
AudioManager.STREAM_MUSIC,
|
|
newVolumeAdjusted,
|
|
0
|
|
)
|
|
}
|
|
}
|
|
}
|
|
else -> Unit
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
currentTouchLast = currentTouch
|
|
return true
|
|
}
|
|
|
|
private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean {
|
|
if (hasNavigated) {
|
|
autoHide()
|
|
} else {
|
|
event.keyCode.let { keyCode ->
|
|
when (event.action) {
|
|
KeyEvent.ACTION_DOWN -> {
|
|
when (keyCode) {
|
|
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
|
if (!isShowing) {
|
|
if (!isLocked) player.handleEvent(CSPlayerEvent.PlayPauseToggle)
|
|
onClickChange()
|
|
return true
|
|
}
|
|
}
|
|
KeyEvent.KEYCODE_DPAD_UP -> {
|
|
if (!isShowing) {
|
|
onClickChange()
|
|
return true
|
|
}
|
|
}
|
|
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
|
if (!isShowing && !isLocked) {
|
|
player.seekTime(-10000L)
|
|
return true
|
|
} else if (player_pause_play?.isFocused == true) {
|
|
player.seekTime(-30000L)
|
|
return true
|
|
}
|
|
}
|
|
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
|
if (!isShowing && !isLocked) {
|
|
player.seekTime(10000L)
|
|
return true
|
|
} else if (player_pause_play?.isFocused == true) {
|
|
player.seekTime(30000L)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
when (keyCode) {
|
|
// don't allow dpad move when hidden
|
|
|
|
KeyEvent.KEYCODE_DPAD_DOWN,
|
|
KeyEvent.KEYCODE_DPAD_UP,
|
|
KeyEvent.KEYCODE_DPAD_DOWN_LEFT,
|
|
KeyEvent.KEYCODE_DPAD_DOWN_RIGHT,
|
|
KeyEvent.KEYCODE_DPAD_UP_LEFT,
|
|
KeyEvent.KEYCODE_DPAD_UP_RIGHT -> {
|
|
if (!isShowing) {
|
|
return true
|
|
} else {
|
|
autoHide()
|
|
}
|
|
}
|
|
|
|
// netflix capture back and hide ~monke
|
|
KeyEvent.KEYCODE_BACK -> {
|
|
if (isShowing) {
|
|
onClickChange()
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
protected fun uiReset() {
|
|
isLocked = false
|
|
isShowing = false
|
|
|
|
// if nothing has loaded these buttons should not be visible
|
|
player_skip_episode?.isVisible = false
|
|
player_skip_op?.isVisible = false
|
|
|
|
updateLockUI()
|
|
updateUIVisibility()
|
|
animateLayoutChanges()
|
|
resetFastForwardText()
|
|
resetRewindText()
|
|
}
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
super.onViewCreated(view, savedInstanceState)
|
|
|
|
// init variables
|
|
setPlayBackSpeed(getKey(PLAYBACK_SPEED_KEY) ?: 1.0f)
|
|
|
|
// handle tv controls
|
|
playerEventListener = { eventType ->
|
|
when (eventType) {
|
|
PlayerEventType.Lock -> {
|
|
toggleLock()
|
|
}
|
|
PlayerEventType.NextEpisode -> {
|
|
player.handleEvent(CSPlayerEvent.NextEpisode)
|
|
}
|
|
PlayerEventType.Pause -> {
|
|
player.handleEvent(CSPlayerEvent.Pause)
|
|
}
|
|
PlayerEventType.PlayPauseToggle -> {
|
|
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
|
|
}
|
|
PlayerEventType.Play -> {
|
|
player.handleEvent(CSPlayerEvent.Play)
|
|
}
|
|
PlayerEventType.Resize -> {
|
|
nextResize()
|
|
}
|
|
PlayerEventType.PrevEpisode -> {
|
|
player.handleEvent(CSPlayerEvent.PrevEpisode)
|
|
}
|
|
PlayerEventType.SeekForward -> {
|
|
player.handleEvent(CSPlayerEvent.SeekForward)
|
|
}
|
|
PlayerEventType.ShowSpeed -> {
|
|
showSpeedDialog()
|
|
}
|
|
PlayerEventType.SeekBack -> {
|
|
player.handleEvent(CSPlayerEvent.SeekBack)
|
|
}
|
|
PlayerEventType.ToggleMute -> {
|
|
player.handleEvent(CSPlayerEvent.ToggleMute)
|
|
}
|
|
PlayerEventType.ToggleHide -> {
|
|
onClickChange()
|
|
}
|
|
PlayerEventType.ShowMirrors -> {
|
|
showMirrorsDialogue()
|
|
}
|
|
}
|
|
}
|
|
|
|
// handle tv controls directly based on player state
|
|
keyEventListener = { eventNav ->
|
|
val (event, hasNavigated) = eventNav
|
|
if (event != null)
|
|
handleKeyEvent(event, hasNavigated)
|
|
else false
|
|
}
|
|
|
|
try {
|
|
context?.let { ctx ->
|
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
|
|
|
fastForwardTime =
|
|
settingsManager.getInt(ctx.getString(R.string.double_tap_seek_time_key), 10)
|
|
.toLong() * 1000L
|
|
|
|
navigationBarHeight = ctx.getNavigationBarHeight()
|
|
statusBarHeight = ctx.getStatusBarHeight()
|
|
|
|
swipeHorizontalEnabled =
|
|
settingsManager.getBoolean(ctx.getString(R.string.swipe_enabled_key), true)
|
|
swipeVerticalEnabled =
|
|
settingsManager.getBoolean(
|
|
ctx.getString(R.string.swipe_vertical_enabled_key),
|
|
true
|
|
)
|
|
playBackSpeedEnabled = settingsManager.getBoolean(
|
|
ctx.getString(R.string.playback_speed_enabled_key),
|
|
false
|
|
)
|
|
playerResizeEnabled =
|
|
settingsManager.getBoolean(
|
|
ctx.getString(R.string.player_resize_enabled_key),
|
|
true
|
|
)
|
|
doubleTapEnabled =
|
|
settingsManager.getBoolean(
|
|
ctx.getString(R.string.double_tap_enabled_key),
|
|
false
|
|
)
|
|
|
|
doubleTapPauseEnabled =
|
|
settingsManager.getBoolean(
|
|
ctx.getString(R.string.double_tap_pause_enabled_key),
|
|
false
|
|
)
|
|
|
|
currentPrefQuality = settingsManager.getInt(
|
|
ctx.getString(R.string.quality_pref_key),
|
|
currentPrefQuality
|
|
)
|
|
// useSystemBrightness =
|
|
// settingsManager.getBoolean(ctx.getString(R.string.use_system_brightness_key), false)
|
|
}
|
|
|
|
player_speed_btt?.isVisible = playBackSpeedEnabled
|
|
player_resize_btt?.isVisible = playerResizeEnabled
|
|
} catch (e: Exception) {
|
|
logError(e)
|
|
}
|
|
|
|
player_pause_play?.setOnClickListener {
|
|
autoHide()
|
|
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
|
|
}
|
|
|
|
// init clicks
|
|
player_resize_btt?.setOnClickListener {
|
|
autoHide()
|
|
nextResize()
|
|
}
|
|
|
|
player_speed_btt?.setOnClickListener {
|
|
autoHide()
|
|
showSpeedDialog()
|
|
}
|
|
|
|
player_skip_op?.setOnClickListener {
|
|
autoHide()
|
|
skipOp()
|
|
}
|
|
|
|
player_skip_episode?.setOnClickListener {
|
|
autoHide()
|
|
player.handleEvent(CSPlayerEvent.NextEpisode)
|
|
}
|
|
|
|
player_lock?.setOnClickListener {
|
|
autoHide()
|
|
toggleLock()
|
|
}
|
|
|
|
player_subtitle_offset_btt?.setOnClickListener {
|
|
showSubtitleOffsetDialog()
|
|
}
|
|
|
|
exo_rew?.setOnClickListener {
|
|
autoHide()
|
|
rewind()
|
|
}
|
|
|
|
exo_ffwd?.setOnClickListener {
|
|
autoHide()
|
|
fastForward()
|
|
}
|
|
|
|
player_go_back?.setOnClickListener {
|
|
activity?.popCurrentPage()
|
|
}
|
|
|
|
player_sources_btt?.setOnClickListener {
|
|
showMirrorsDialogue()
|
|
}
|
|
|
|
// it is !not! a bug that you cant touch the right side, it does not register inputs on navbar or status bar
|
|
player_holder?.setOnTouchListener { callView, event ->
|
|
return@setOnTouchListener handleMotionEvent(callView, event)
|
|
}
|
|
|
|
exo_progress?.setOnTouchListener { _, event ->
|
|
// this makes the bar not disappear when sliding
|
|
when (event.action) {
|
|
MotionEvent.ACTION_DOWN -> {
|
|
currentTapIndex++
|
|
}
|
|
MotionEvent.ACTION_MOVE -> {
|
|
currentTapIndex++
|
|
}
|
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_BUTTON_RELEASE -> {
|
|
autoHide()
|
|
}
|
|
}
|
|
return@setOnTouchListener false
|
|
}
|
|
// init UI
|
|
try {
|
|
uiReset()
|
|
|
|
// init chromecast UI
|
|
// removed due to having no use and bugging
|
|
//activity?.let {
|
|
// if (it.isCastApiAvailable()) {
|
|
// try {
|
|
// CastButtonFactory.setUpMediaRouteButton(it, player_media_route_button)
|
|
// val castContext = CastContext.getSharedInstance(it.applicationContext)
|
|
//
|
|
// player_media_route_button?.isGone =
|
|
// castContext.castState == CastState.NO_DEVICES_AVAILABLE
|
|
// castContext.addCastStateListener { state ->
|
|
// player_media_route_button?.isGone =
|
|
// state == CastState.NO_DEVICES_AVAILABLE
|
|
// }
|
|
// } catch (e: Exception) {
|
|
// logError(e)
|
|
// }
|
|
// } else {
|
|
// // if cast is not possible hide UI
|
|
// player_media_route_button?.isGone = true
|
|
// }
|
|
//}
|
|
} catch (e: Exception) {
|
|
logError(e)
|
|
}
|
|
}
|
|
} |