media: unwind tightly bound action handling
This commit is contained in:
parent
d91343070a
commit
c1e5adbc44
4 changed files with 127 additions and 118 deletions
|
@ -21,6 +21,7 @@ package org.oxycblt.auxio.playback.service
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.session.CommandButton
|
import androidx.media3.session.CommandButton
|
||||||
import androidx.media3.session.DefaultActionFactory
|
import androidx.media3.session.DefaultActionFactory
|
||||||
|
@ -49,10 +50,12 @@ import org.oxycblt.auxio.ForegroundListener
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.service.MediaItemBrowser
|
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.DeferredPlayback
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.newMainPendingIntent
|
import org.oxycblt.auxio.util.newMainPendingIntent
|
||||||
|
import org.oxycblt.auxio.widgets.WidgetComponent
|
||||||
|
|
||||||
class MediaSessionServiceFragment
|
class MediaSessionServiceFragment
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -60,6 +63,8 @@ constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val playbackManager: PlaybackStateManager,
|
private val playbackManager: PlaybackStateManager,
|
||||||
private val actionHandler: PlaybackActionHandler,
|
private val actionHandler: PlaybackActionHandler,
|
||||||
|
private val playbackSettings: PlaybackSettings,
|
||||||
|
private val widgetComponent: WidgetComponent,
|
||||||
private val mediaItemBrowser: MediaItemBrowser,
|
private val mediaItemBrowser: MediaItemBrowser,
|
||||||
exoHolderFactory: ExoPlaybackStateHolder.Factory
|
exoHolderFactory: ExoPlaybackStateHolder.Factory
|
||||||
) :
|
) :
|
||||||
|
@ -86,6 +91,7 @@ constructor(
|
||||||
.also { it.setSmallIcon(R.drawable.ic_auxio_24) }
|
.also { it.setSmallIcon(R.drawable.ic_auxio_24) }
|
||||||
private var foregroundListener: ForegroundListener? = null
|
private var foregroundListener: ForegroundListener? = null
|
||||||
|
|
||||||
|
lateinit var systemReceiver: SystemPlaybackReceiver
|
||||||
lateinit var mediaSession: MediaLibrarySession
|
lateinit var mediaSession: MediaLibrarySession
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
@ -99,6 +105,10 @@ constructor(
|
||||||
playbackManager.addListener(this)
|
playbackManager.addListener(this)
|
||||||
exoHolder.attach()
|
exoHolder.attach()
|
||||||
actionHandler.attach(this)
|
actionHandler.attach(this)
|
||||||
|
systemReceiver = SystemPlaybackReceiver(playbackManager, playbackSettings, widgetComponent)
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
context, systemReceiver, systemReceiver.intentFilter, ContextCompat.RECEIVER_EXPORTED)
|
||||||
|
widgetComponent.attach()
|
||||||
mediaItemBrowser.attach(this)
|
mediaItemBrowser.attach(this)
|
||||||
return mediaSession
|
return mediaSession
|
||||||
}
|
}
|
||||||
|
@ -142,6 +152,8 @@ constructor(
|
||||||
fun release() {
|
fun release() {
|
||||||
waitJob.cancel()
|
waitJob.cancel()
|
||||||
mediaItemBrowser.release()
|
mediaItemBrowser.release()
|
||||||
|
context.unregisterReceiver(systemReceiver)
|
||||||
|
widgetComponent.release()
|
||||||
actionHandler.release()
|
actionHandler.release()
|
||||||
exoHolder.release()
|
exoHolder.release()
|
||||||
playbackManager.removeListener(this)
|
playbackManager.removeListener(this)
|
||||||
|
|
|
@ -18,13 +18,8 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.service
|
package org.oxycblt.auxio.playback.service
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.media.AudioManager
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.session.CommandButton
|
import androidx.media3.session.CommandButton
|
||||||
import androidx.media3.session.SessionCommand
|
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.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.Progression
|
import org.oxycblt.auxio.playback.state.Progression
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
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
|
class PlaybackActionHandler
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val playbackManager: PlaybackStateManager,
|
private val playbackManager: PlaybackStateManager,
|
||||||
private val playbackSettings: PlaybackSettings,
|
private val playbackSettings: PlaybackSettings
|
||||||
private val widgetComponent: WidgetComponent
|
|
||||||
) : PlaybackStateManager.Listener, PlaybackSettings.Listener {
|
) : PlaybackStateManager.Listener, PlaybackSettings.Listener {
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onCustomLayoutChanged(layout: List<CommandButton>)
|
fun onCustomLayoutChanged(layout: List<CommandButton>)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val systemReceiver =
|
|
||||||
SystemPlaybackReceiver(playbackManager, playbackSettings, widgetComponent)
|
|
||||||
private var callback: Callback? = null
|
private var callback: Callback? = null
|
||||||
|
|
||||||
fun attach(callback: Callback) {
|
fun attach(callback: Callback) {
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
playbackManager.addListener(this)
|
playbackManager.addListener(this)
|
||||||
playbackSettings.registerListener(this)
|
playbackSettings.registerListener(this)
|
||||||
ContextCompat.registerReceiver(
|
|
||||||
context, systemReceiver, systemReceiver.intentFilter, ContextCompat.RECEIVER_EXPORTED)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
callback = null
|
callback = null
|
||||||
playbackManager.removeListener(this)
|
playbackManager.removeListener(this)
|
||||||
playbackSettings.unregisterListener(this)
|
playbackSettings.unregisterListener(this)
|
||||||
context.unregisterReceiver(systemReceiver)
|
|
||||||
widgetComponent.release()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withCommands(commands: SessionCommands) =
|
fun withCommands(commands: SessionCommands) =
|
||||||
|
@ -178,103 +163,3 @@ object PlaybackActions {
|
||||||
const val ACTION_SKIP_NEXT = BuildConfig.APPLICATION_ID + ".action.NEXT"
|
const val ACTION_SKIP_NEXT = BuildConfig.APPLICATION_ID + ".action.NEXT"
|
||||||
const val ACTION_EXIT = BuildConfig.APPLICATION_ID + ".action.EXIT"
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,7 +57,7 @@ constructor(
|
||||||
) : PlaybackStateManager.Listener, UISettings.Listener, ImageSettings.Listener {
|
) : PlaybackStateManager.Listener, UISettings.Listener, ImageSettings.Listener {
|
||||||
private val widgetProvider = WidgetProvider()
|
private val widgetProvider = WidgetProvider()
|
||||||
|
|
||||||
init {
|
fun attach() {
|
||||||
playbackManager.addListener(this)
|
playbackManager.addListener(this)
|
||||||
uiSettings.registerListener(this)
|
uiSettings.registerListener(this)
|
||||||
imageSettings.registerListener(this)
|
imageSettings.registerListener(this)
|
||||||
|
@ -90,7 +90,7 @@ constructor(
|
||||||
} else if (uiSettings.roundMode) {
|
} else if (uiSettings.roundMode) {
|
||||||
// < Android 12, but the user still enabled round mode.
|
// < Android 12, but the user still enabled round mode.
|
||||||
logD("Using default corner radius")
|
logD("Using default corner radius")
|
||||||
context.getDimenPixels(R.dimen.size_corners_medium)
|
context.getDimenPixels(R.dimen.spacing_medium)
|
||||||
} else {
|
} else {
|
||||||
// User did not enable round mode.
|
// User did not enable round mode.
|
||||||
logD("Using no corner radius")
|
logD("Using no corner radius")
|
||||||
|
|
Loading…
Reference in a new issue