media: unwind tightly bound action handling

This commit is contained in:
Alexander Capehart 2024-08-23 13:55:49 -06:00
parent d91343070a
commit c1e5adbc44
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 127 additions and 118 deletions

View file

@ -21,6 +21,7 @@ package org.oxycblt.auxio.playback.service
import android.app.Notification
import android.content.Context
import android.os.Bundle
import androidx.core.content.ContextCompat
import androidx.media3.common.MediaItem
import androidx.media3.session.CommandButton
import androidx.media3.session.DefaultActionFactory
@ -49,10 +50,12 @@ import org.oxycblt.auxio.ForegroundListener
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.service.MediaItemBrowser
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.state.DeferredPlayback
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newMainPendingIntent
import org.oxycblt.auxio.widgets.WidgetComponent
class MediaSessionServiceFragment
@Inject
@ -60,6 +63,8 @@ constructor(
@ApplicationContext private val context: Context,
private val playbackManager: PlaybackStateManager,
private val actionHandler: PlaybackActionHandler,
private val playbackSettings: PlaybackSettings,
private val widgetComponent: WidgetComponent,
private val mediaItemBrowser: MediaItemBrowser,
exoHolderFactory: ExoPlaybackStateHolder.Factory
) :
@ -86,6 +91,7 @@ constructor(
.also { it.setSmallIcon(R.drawable.ic_auxio_24) }
private var foregroundListener: ForegroundListener? = null
lateinit var systemReceiver: SystemPlaybackReceiver
lateinit var mediaSession: MediaLibrarySession
private set
@ -99,6 +105,10 @@ constructor(
playbackManager.addListener(this)
exoHolder.attach()
actionHandler.attach(this)
systemReceiver = SystemPlaybackReceiver(playbackManager, playbackSettings, widgetComponent)
ContextCompat.registerReceiver(
context, systemReceiver, systemReceiver.intentFilter, ContextCompat.RECEIVER_EXPORTED)
widgetComponent.attach()
mediaItemBrowser.attach(this)
return mediaSession
}
@ -142,6 +152,8 @@ constructor(
fun release() {
waitJob.cancel()
mediaItemBrowser.release()
context.unregisterReceiver(systemReceiver)
widgetComponent.release()
actionHandler.release()
exoHolder.release()
playbackManager.removeListener(this)

View file

@ -18,13 +18,8 @@
package org.oxycblt.auxio.playback.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import android.os.Bundle
import androidx.core.content.ContextCompat
import androidx.media3.common.Player
import androidx.media3.session.CommandButton
import androidx.media3.session.SessionCommand
@ -39,41 +34,31 @@ import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Progression
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.widgets.WidgetComponent
import org.oxycblt.auxio.widgets.WidgetProvider
class PlaybackActionHandler
@Inject
constructor(
@ApplicationContext private val context: Context,
private val playbackManager: PlaybackStateManager,
private val playbackSettings: PlaybackSettings,
private val widgetComponent: WidgetComponent
private val playbackSettings: PlaybackSettings
) : PlaybackStateManager.Listener, PlaybackSettings.Listener {
interface Callback {
fun onCustomLayoutChanged(layout: List<CommandButton>)
}
private val systemReceiver =
SystemPlaybackReceiver(playbackManager, playbackSettings, widgetComponent)
private var callback: Callback? = null
fun attach(callback: Callback) {
this.callback = callback
playbackManager.addListener(this)
playbackSettings.registerListener(this)
ContextCompat.registerReceiver(
context, systemReceiver, systemReceiver.intentFilter, ContextCompat.RECEIVER_EXPORTED)
}
fun release() {
callback = null
playbackManager.removeListener(this)
playbackSettings.unregisterListener(this)
context.unregisterReceiver(systemReceiver)
widgetComponent.release()
}
fun withCommands(commands: SessionCommands) =
@ -178,103 +163,3 @@ object PlaybackActions {
const val ACTION_SKIP_NEXT = BuildConfig.APPLICATION_ID + ".action.NEXT"
const val ACTION_EXIT = BuildConfig.APPLICATION_ID + ".action.EXIT"
}
/**
* A [BroadcastReceiver] for receiving playback-specific [Intent]s from the system that require an
* active [IntentFilter] to be registered.
*/
class SystemPlaybackReceiver(
private val playbackManager: PlaybackStateManager,
private val playbackSettings: PlaybackSettings,
private val widgetComponent: WidgetComponent
) : BroadcastReceiver() {
private var initialHeadsetPlugEventHandled = false
val intentFilter =
IntentFilter().apply {
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
addAction(AudioManager.ACTION_HEADSET_PLUG)
addAction(PlaybackActions.ACTION_INC_REPEAT_MODE)
addAction(PlaybackActions.ACTION_INVERT_SHUFFLE)
addAction(PlaybackActions.ACTION_SKIP_PREV)
addAction(PlaybackActions.ACTION_PLAY_PAUSE)
addAction(PlaybackActions.ACTION_SKIP_NEXT)
addAction(WidgetProvider.ACTION_WIDGET_UPDATE)
}
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// --- SYSTEM EVENTS ---
// Android has three different ways of handling audio plug events for some reason:
// 1. ACTION_HEADSET_PLUG, which only works with wired headsets
// 2. ACTION_ACL_CONNECTED, which allows headset autoplay but also requires
// granting the BLUETOOTH/BLUETOOTH_CONNECT permissions, which is more or less
// a non-starter since both require me to display a permission prompt
// 3. Some internal framework thing that also handles bluetooth headsets
// Just use ACTION_HEADSET_PLUG.
AudioManager.ACTION_HEADSET_PLUG -> {
logD("Received headset plug event")
when (intent.getIntExtra("state", -1)) {
0 -> pauseFromHeadsetPlug()
1 -> playFromHeadsetPlug()
}
initialHeadsetPlugEventHandled = true
}
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> {
logD("Received Headset noise event")
pauseFromHeadsetPlug()
}
// --- AUXIO EVENTS ---
PlaybackActions.ACTION_PLAY_PAUSE -> {
logD("Received play event")
playbackManager.playing(!playbackManager.progression.isPlaying)
}
PlaybackActions.ACTION_INC_REPEAT_MODE -> {
logD("Received repeat mode event")
playbackManager.repeatMode(playbackManager.repeatMode.increment())
}
PlaybackActions.ACTION_INVERT_SHUFFLE -> {
logD("Received shuffle event")
playbackManager.shuffled(!playbackManager.isShuffled)
}
PlaybackActions.ACTION_SKIP_PREV -> {
logD("Received skip previous event")
playbackManager.prev()
}
PlaybackActions.ACTION_SKIP_NEXT -> {
logD("Received skip next event")
playbackManager.next()
}
PlaybackActions.ACTION_EXIT -> {
logD("Received exit event")
playbackManager.endSession()
}
WidgetProvider.ACTION_WIDGET_UPDATE -> {
logD("Received widget update event")
widgetComponent.update()
}
}
}
private fun playFromHeadsetPlug() {
// ACTION_HEADSET_PLUG will fire when this BroadcastReceiver is initially attached,
// which would result in unexpected playback. Work around it by dropping the first
// call to this function, which should come from that Intent.
if (playbackSettings.headsetAutoplay &&
playbackManager.currentSong != null &&
initialHeadsetPlugEventHandled) {
logD("Device connected, resuming")
playbackManager.playing(true)
}
}
private fun pauseFromHeadsetPlug() {
if (playbackManager.currentSong != null) {
logD("Device disconnected, pausing")
playbackManager.playing(false)
}
}
}

View file

@ -0,0 +1,112 @@
package org.oxycblt.auxio.playback.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.widgets.WidgetComponent
import org.oxycblt.auxio.widgets.WidgetProvider
/**
* A [BroadcastReceiver] for receiving playback-specific [Intent]s from the system that require an
* active [IntentFilter] to be registered.
*/
class SystemPlaybackReceiver(
private val playbackManager: PlaybackStateManager,
private val playbackSettings: PlaybackSettings,
private val widgetComponent: WidgetComponent
) : BroadcastReceiver() {
private var initialHeadsetPlugEventHandled = false
val intentFilter =
IntentFilter().apply {
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
addAction(AudioManager.ACTION_HEADSET_PLUG)
addAction(PlaybackActions.ACTION_INC_REPEAT_MODE)
addAction(PlaybackActions.ACTION_INVERT_SHUFFLE)
addAction(PlaybackActions.ACTION_SKIP_PREV)
addAction(PlaybackActions.ACTION_PLAY_PAUSE)
addAction(PlaybackActions.ACTION_SKIP_NEXT)
addAction(WidgetProvider.ACTION_WIDGET_UPDATE)
}
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// --- SYSTEM EVENTS ---
// Android has three different ways of handling audio plug events for some reason:
// 1. ACTION_HEADSET_PLUG, which only works with wired headsets
// 2. ACTION_ACL_CONNECTED, which allows headset autoplay but also requires
// granting the BLUETOOTH/BLUETOOTH_CONNECT permissions, which is more or less
// a non-starter since both require me to display a permission prompt
// 3. Some internal framework thing that also handles bluetooth headsets
// Just use ACTION_HEADSET_PLUG.
AudioManager.ACTION_HEADSET_PLUG -> {
logD("Received headset plug event")
when (intent.getIntExtra("state", -1)) {
0 -> pauseFromHeadsetPlug()
1 -> playFromHeadsetPlug()
}
initialHeadsetPlugEventHandled = true
}
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> {
logD("Received Headset noise event")
pauseFromHeadsetPlug()
}
// --- AUXIO EVENTS ---
PlaybackActions.ACTION_PLAY_PAUSE -> {
logD("Received play event")
playbackManager.playing(!playbackManager.progression.isPlaying)
}
PlaybackActions.ACTION_INC_REPEAT_MODE -> {
logD("Received repeat mode event")
playbackManager.repeatMode(playbackManager.repeatMode.increment())
}
PlaybackActions.ACTION_INVERT_SHUFFLE -> {
logD("Received shuffle event")
playbackManager.shuffled(!playbackManager.isShuffled)
}
PlaybackActions.ACTION_SKIP_PREV -> {
logD("Received skip previous event")
playbackManager.prev()
}
PlaybackActions.ACTION_SKIP_NEXT -> {
logD("Received skip next event")
playbackManager.next()
}
PlaybackActions.ACTION_EXIT -> {
logD("Received exit event")
playbackManager.endSession()
}
WidgetProvider.ACTION_WIDGET_UPDATE -> {
logD("Received widget update event")
widgetComponent.update()
}
}
}
private fun playFromHeadsetPlug() {
// ACTION_HEADSET_PLUG will fire when this BroadcastReceiver is initially attached,
// which would result in unexpected playback. Work around it by dropping the first
// call to this function, which should come from that Intent.
if (playbackSettings.headsetAutoplay &&
playbackManager.currentSong != null &&
initialHeadsetPlugEventHandled) {
logD("Device connected, resuming")
playbackManager.playing(true)
}
}
private fun pauseFromHeadsetPlug() {
if (playbackManager.currentSong != null) {
logD("Device disconnected, pausing")
playbackManager.playing(false)
}
}
}

View file

@ -57,7 +57,7 @@ constructor(
) : PlaybackStateManager.Listener, UISettings.Listener, ImageSettings.Listener {
private val widgetProvider = WidgetProvider()
init {
fun attach() {
playbackManager.addListener(this)
uiSettings.registerListener(this)
imageSettings.registerListener(this)
@ -90,7 +90,7 @@ constructor(
} else if (uiSettings.roundMode) {
// < Android 12, but the user still enabled round mode.
logD("Using default corner radius")
context.getDimenPixels(R.dimen.size_corners_medium)
context.getDimenPixels(R.dimen.spacing_medium)
} else {
// User did not enable round mode.
logD("Using no corner radius")