From ab2b6da9b2dfe33feb1878e40b48637720e3ab4f Mon Sep 17 00:00:00 2001 From: Samfun75 <38332931+Samfun75@users.noreply.github.com> Date: Fri, 4 Aug 2023 20:53:57 +0300 Subject: [PATCH] feat(player): handle media buttons from earphones, bluetooth devices and possibly android tv remotes --- app/build.gradle.kts | 1 + .../tachiyomi/ui/player/PlayerActivity.kt | 107 +++++++++++++++++- gradle/androidx.versions.toml | 1 + 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c3db4865e..2bb0600da 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -187,6 +187,7 @@ dependencies { implementation(androidx.recyclerview) implementation(androidx.viewpager) implementation(androidx.profileinstaller) + implementation(androidx.mediasession) implementation(androidx.bundles.lifecycle) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt index 426ff853c..bf003dc98 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt @@ -16,6 +16,9 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.os.ParcelFileDescriptor +import android.support.v4.media.session.MediaControllerCompat +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat import android.util.DisplayMetrics import android.view.KeyEvent import android.view.MotionEvent @@ -146,6 +149,10 @@ class PlayerActivity : BaseActivity() { lateinit var binding: PlayerActivityBinding + private lateinit var mediaSession: MediaSessionCompat + + private val playbackStateBuilder = PlaybackStateCompat.Builder() + internal val player get() = binding.player internal val playerControls get() = binding.playerControls @@ -276,6 +283,7 @@ class PlayerActivity : BaseActivity() { super.onCreate(savedInstanceState) setupPlayerControls() + setupMediaSession() setupPlayerMPV() setupPlayerAudio() setupPlayerBrightness() @@ -433,6 +441,89 @@ class PlayerActivity : BaseActivity() { verticalScrollLeft(0F) } + @Suppress("DEPRECATION") + private fun setupMediaSession() { + mediaSession = MediaSessionCompat(this, "Aniyomi_Player_Session").apply { + // Enable callbacks from MediaButtons and TransportControls + setFlags( + MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or + MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS, + ) + + // Do not let MediaButtons restart the player when the app is not visible + setMediaButtonReceiver(null) + + setPlaybackState( + with(playbackStateBuilder) { + setState( + PlaybackStateCompat.STATE_NONE, + 0L, + 0.0F, + ) + build() + }, + ) + + // Implement methods that handle callbacks from a media controller + setCallback( + object : MediaSessionCompat.Callback() { + override fun onPlay() { + player.paused = false + playerControls.toggleControls(isTapped = true) + updatePlaybackState() + } + + override fun onPause() { + player.paused = true + playerControls.toggleControls() + updatePlaybackState(pause = true) + } + + override fun onSkipToPrevious() { + changeEpisode(viewModel.getAdjacentEpisodeId(previous = true)) + } + + override fun onSkipToNext() { + changeEpisode(viewModel.getAdjacentEpisodeId(previous = false)) + } + }, + ) + } + + MediaControllerCompat(this, mediaSession).also { mediaController -> + MediaControllerCompat.setMediaController(this, mediaController) + } + } + + private fun updatePlaybackState(cachePause: Boolean = false, pause: Boolean = false) { + val state = when { + player.timePos?.let { it < 0 } ?: true || + player.duration?.let { it <= 0 } ?: true -> PlaybackStateCompat.STATE_CONNECTING + cachePause -> PlaybackStateCompat.STATE_BUFFERING + pause or (player.paused == true) -> PlaybackStateCompat.STATE_PAUSED + else -> PlaybackStateCompat.STATE_PLAYING + } + var actions = PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PLAY_PAUSE or + PlaybackStateCompat.ACTION_PAUSE + if (viewModel.currentPlaylist.size > 1) { + actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT + } + + mediaSession.setPlaybackState( + with(playbackStateBuilder) { + setState( + state, + player.timePos?.toLong() ?: 0L, + player.playbackSpeed?.toFloat() ?: 1.0f, + ) + setActions(actions) + build() + }, + ) + } + @Suppress("DEPRECATION") private fun loadDeviceDimensions() { this@PlayerActivity.requestedOrientation = playerPreferences.defaultPlayerOrientationType().get() @@ -497,6 +588,9 @@ class PlayerActivity : BaseActivity() { } override fun onDestroy() { + mediaSession.isActive = false + mediaSession.release() + playerPreferences.playerVolumeValue().set(fineVolume) playerPreferences.playerBrightnessValue().set(brightness) MPVLib.removeLogObserver(playerObserver) @@ -1560,19 +1654,28 @@ class PlayerActivity : BaseActivity() { "time-pos" -> { playerControls.updatePlaybackPos(value.toInt()) viewModel.viewModelScope.launchUI { aniSkipStuff(value) } + updatePlaybackState() + } + "duration" -> { + playerControls.updatePlaybackDuration(value.toInt()) + mediaSession.isActive = true + updatePlaybackState() } - "duration" -> playerControls.updatePlaybackDuration(value.toInt()) } } internal fun eventPropertyUi(property: String, value: Boolean) { when (property) { "seeking" -> isSeeking(value) - "paused-for-cache" -> showLoadingIndicator(value) + "paused-for-cache" -> { + showLoadingIndicator(value) + updatePlaybackState(cachePause = true) + } "pause" -> { if (!isFinishing) { setAudioFocus(value) updatePlaybackStatus(value) + updatePlaybackState(pause = true) } } "eof-reached" -> endFile(value) diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 56b152253..f5cb34e77 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -16,6 +16,7 @@ recyclerview = "androidx.recyclerview:recyclerview:1.3.0" viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01" glance = "androidx.glance:glance-appwidget:1.0.0-alpha03" profileinstaller = "androidx.profileinstaller:profileinstaller:1.3.0" +mediasession = "androidx.media:media:1.6.0" lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "lifecycle_version" } lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle_version" }