Add ability to swipe up and down to adjust volume (#48)

* Add ability to swipe up and down to adjust volume

Created AudioController to adjust global volume for media channel. Alarm, system and other volume levels are unaffected. This way it doesn't need specific implementation for separate players. During swiping system volume slider is shown and it is possible to change direction mid-swipe.

AudioController should probably be singleton and provided by DI but currently PlayerActivity is handling all the playback so it seemed unnecessarily complicated.

Sensitivity can be adjusted by threshold value in VerticalSwipeListener.

* Add audio controller class

Co-authored-by: Jarne Demeulemeester <32322857+jarnedemeulemeester@users.noreply.github.com>
This commit is contained in:
lsrom 2021-10-24 18:07:08 +02:00 committed by GitHub
parent 62d09b3566
commit b0b7d7f5b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 105 additions and 6 deletions

View file

@ -20,14 +20,19 @@ import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment
import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment
import dev.jdtech.jellyfin.mpv.MPVPlayer import dev.jdtech.jellyfin.mpv.MPVPlayer
import dev.jdtech.jellyfin.mpv.TrackType import dev.jdtech.jellyfin.mpv.TrackType
import dev.jdtech.jellyfin.utils.AudioController
import dev.jdtech.jellyfin.utils.VerticalSwipeListener
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
import timber.log.Timber import timber.log.Timber
import kotlin.math.max
@AndroidEntryPoint @AndroidEntryPoint
class PlayerActivity : AppCompatActivity() { class PlayerActivity : AppCompatActivity() {
private lateinit var binding: ActivityPlayerBinding private lateinit var binding: ActivityPlayerBinding
private val viewModel: PlayerActivityViewModel by viewModels() private val viewModel: PlayerActivityViewModel by viewModels()
private val args: PlayerActivityArgs by navArgs() private val args: PlayerActivityArgs by navArgs()
private val audioController by lazy { AudioController(this) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -39,6 +44,7 @@ class PlayerActivity : AppCompatActivity() {
binding.playerView.player = viewModel.player binding.playerView.player = viewModel.player
val playerControls = binding.playerView.findViewById<View>(R.id.player_controls) val playerControls = binding.playerView.findViewById<View>(R.id.player_controls)
setupVolumeControl()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
binding.playerView.findViewById<View>(R.id.player_controls) binding.playerView.findViewById<View>(R.id.player_controls)
@ -179,12 +185,26 @@ class PlayerActivity : AppCompatActivity() {
hideSystemUI() hideSystemUI()
} }
private fun setupVolumeControl() {
val height = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
windowManager.currentWindowMetrics.bounds.height()
} else {
windowManager.defaultDisplay.height
}
binding.playerView.setOnTouchListener(VerticalSwipeListener(
onUp = { audioController.volumeUp() },
onDown = { audioController.volumeDown() },
onTouch = { audioController.showVolumeSlider() },
threshold = max(height / 8, 100)
))
}
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun hideSystemUI() { private fun hideSystemUI() {
// These methods are deprecated but we still use them because the new WindowInsetsControllerCompat has a bug which makes the action bar reappear // These methods are deprecated but we still use them because the new WindowInsetsControllerCompat has a bug which makes the action bar reappear
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {

View file

@ -34,6 +34,7 @@ import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.lang.IllegalArgumentException
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
@ -1199,22 +1200,22 @@ class MPVPlayer(
* @param volume The volume to set. * @param volume The volume to set.
*/ */
override fun setDeviceVolume(volume: Int) { override fun setDeviceVolume(volume: Int) {
TODO("Not yet implemented") throw IllegalArgumentException("You should use global volume controls. Check out AUDIO_SERVICE.")
} }
/** Increases the volume of the device. */ /** Increases the volume of the device. */
override fun increaseDeviceVolume() { override fun increaseDeviceVolume() {
TODO("Not yet implemented") throw IllegalArgumentException("You should use global volume controls. Check out AUDIO_SERVICE.")
} }
/** Decreases the volume of the device. */ /** Decreases the volume of the device. */
override fun decreaseDeviceVolume() { override fun decreaseDeviceVolume() {
TODO("Not yet implemented") throw IllegalArgumentException("You should use global volume controls. Check out AUDIO_SERVICE.")
} }
/** Sets the mute state of the device. */ /** Sets the mute state of the device. */
override fun setDeviceMuted(muted: Boolean) { override fun setDeviceMuted(muted: Boolean) {
TODO("Not yet implemented") throw IllegalArgumentException("You should use global volume controls. Check out AUDIO_SERVICE.")
} }
private class CurrentTrackSelection( private class CurrentTrackSelection(

View file

@ -0,0 +1,18 @@
package dev.jdtech.jellyfin.utils
import android.content.Context
import android.media.AudioManager
import android.media.AudioManager.ADJUST_LOWER
import android.media.AudioManager.ADJUST_RAISE
import android.media.AudioManager.ADJUST_SAME
import android.media.AudioManager.FLAG_SHOW_UI
import android.media.AudioManager.STREAM_MUSIC
internal class AudioController internal constructor(context: Context) {
private val manager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
fun volumeUp() = manager.adjustStreamVolume(STREAM_MUSIC, ADJUST_RAISE, FLAG_SHOW_UI)
fun volumeDown() = manager.adjustStreamVolume(STREAM_MUSIC, ADJUST_LOWER, FLAG_SHOW_UI)
fun showVolumeSlider() = manager.adjustStreamVolume(STREAM_MUSIC, ADJUST_SAME, FLAG_SHOW_UI)
}

View file

@ -0,0 +1,60 @@
package dev.jdtech.jellyfin.utils
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_MOVE
import android.view.MotionEvent.ACTION_UP
import android.view.View
import kotlin.math.absoluteValue
/**
* Never consumes touch events. Can report when swipe starts with onTouch.
*/
internal class VerticalSwipeListener(
private val onUp: () -> Unit,
private val onDown: () -> Unit,
private val onTouch: () -> Unit = {},
private val threshold: Int = 150,
): View.OnTouchListener {
private var verticalStart = 0f
private var distance = 0f
private var dispatchTouchEvent = true
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
when(event.action) {
ACTION_DOWN -> verticalStart = event.y
ACTION_UP -> dispatchTouchEvent = true
ACTION_MOVE -> event.reportSwipe()
else -> Unit
}
return false
}
private fun MotionEvent.reportSwipe() {
reportFirstTouch()
distance = verticalStart - y
if (distance.tooShort()) return
verticalStart = y
if (distance > 0) {
onUp()
} else {
onDown()
}
}
private fun reportFirstTouch() {
if (dispatchTouchEvent) {
onTouch()
dispatchTouchEvent = false
}
}
private fun Float.tooShort() = absoluteValue < threshold
}