playback: fully use di

Fully use DI in the playback module.

Previously use was split among different components that could leverage
injection, and components that could not. This fully unifies them.
This commit is contained in:
Alexander Capehart 2023-02-12 18:46:11 -07:00
parent 9f74fe8a20
commit 63e5a7ee69
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 75 additions and 82 deletions

View file

@ -196,7 +196,7 @@ private abstract class BaseMediaStoreExtractor(
if (cache?.populate(rawSong) == true) { if (cache?.populate(rawSong) == true) {
completeSongs.send(rawSong) completeSongs.send(rawSong)
} else { } else {
query.populateFileInfo(rawSong) query.populateTags(rawSong)
incompleteSongs.send(rawSong) incompleteSongs.send(rawSong)
} }
yield() yield()

View file

@ -17,17 +17,19 @@
package org.oxycblt.auxio.playback package org.oxycblt.auxio.playback
import android.content.Context import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.PlaybackStateManagerImpl
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class PlaybackModule { interface PlaybackModule {
@Provides fun stateManager() = PlaybackStateManager.get() @Singleton
@Provides fun settings(@ApplicationContext context: Context) = PlaybackSettings.from(context) @Binds
fun stateManager(playbackManager: PlaybackStateManagerImpl): PlaybackStateManager
@Binds fun settings(playbackSettings: PlaybackSettingsImpl): PlaybackSettings
} }

View file

@ -19,6 +19,8 @@ package org.oxycblt.auxio.playback
import android.content.Context import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
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.MusicMode import org.oxycblt.auxio.music.MusicMode
@ -65,17 +67,9 @@ interface PlaybackSettings : Settings<PlaybackSettings.Listener> {
/** Called when [notificationAction] has changed. */ /** Called when [notificationAction] has changed. */
fun onNotificationActionChanged() {} fun onNotificationActionChanged() {}
} }
companion object {
/**
* Get a framework-backed implementation.
* @param context [Context] required.
*/
fun from(context: Context): PlaybackSettings = PlaybackSettingsImpl(context)
}
} }
class PlaybackSettingsImpl(context: Context) : class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Context) :
Settings.Impl<PlaybackSettings.Listener>(context), PlaybackSettings { Settings.Impl<PlaybackSettings.Listener>(context), PlaybackSettings {
override val inListPlaybackMode: MusicMode override val inListPlaybackMode: MusicMode
get() = get() =

View file

@ -21,6 +21,8 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.math.abs import kotlin.math.abs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.databinding.DialogPreAmpBinding
@ -31,7 +33,10 @@ import org.oxycblt.auxio.ui.ViewBindingDialogFragment
* aa [ViewBindingDialogFragment] that allows user configuration of the current [ReplayGainPreAmp]. * aa [ViewBindingDialogFragment] that allows user configuration of the current [ReplayGainPreAmp].
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@AndroidEntryPoint
class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() { class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
@Inject lateinit var playbackSettings: PlaybackSettings
override fun onCreateBinding(inflater: LayoutInflater) = DialogPreAmpBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = DialogPreAmpBinding.inflate(inflater)
override fun onConfigDialog(builder: AlertDialog.Builder) { override fun onConfigDialog(builder: AlertDialog.Builder) {
@ -39,11 +44,11 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
.setTitle(R.string.set_pre_amp) .setTitle(R.string.set_pre_amp)
.setPositiveButton(R.string.lbl_ok) { _, _ -> .setPositiveButton(R.string.lbl_ok) { _, _ ->
val binding = requireBinding() val binding = requireBinding()
PlaybackSettings.from(requireContext()).replayGainPreAmp = playbackSettings.replayGainPreAmp =
ReplayGainPreAmp(binding.withTagsSlider.value, binding.withoutTagsSlider.value) ReplayGainPreAmp(binding.withTagsSlider.value, binding.withoutTagsSlider.value)
} }
.setNeutralButton(R.string.lbl_reset) { _, _ -> .setNeutralButton(R.string.lbl_reset) { _, _ ->
PlaybackSettings.from(requireContext()).replayGainPreAmp = ReplayGainPreAmp(0f, 0f) playbackSettings.replayGainPreAmp = ReplayGainPreAmp(0f, 0f)
} }
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
} }
@ -53,7 +58,7 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
// First initialization, we need to supply the sliders with the values from // First initialization, we need to supply the sliders with the values from
// settings. After this, the sliders save their own state, so we do not need to // settings. After this, the sliders save their own state, so we do not need to
// do any restore behavior. // do any restore behavior.
val preAmp = PlaybackSettings.from(requireContext()).replayGainPreAmp val preAmp = playbackSettings.replayGainPreAmp
binding.withTagsSlider.value = preAmp.with binding.withTagsSlider.value = preAmp.with
binding.withoutTagsSlider.value = preAmp.without binding.withoutTagsSlider.value = preAmp.without
} }

View file

@ -17,7 +17,6 @@
package org.oxycblt.auxio.playback.replaygain package org.oxycblt.auxio.playback.replaygain
import android.content.Context
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.Format import com.google.android.exoplayer2.Format
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
@ -26,6 +25,7 @@ import com.google.android.exoplayer2.audio.AudioProcessor
import com.google.android.exoplayer2.audio.BaseAudioProcessor import com.google.android.exoplayer2.audio.BaseAudioProcessor
import com.google.android.exoplayer2.util.MimeTypes import com.google.android.exoplayer2.util.MimeTypes
import java.nio.ByteBuffer import java.nio.ByteBuffer
import javax.inject.Inject
import kotlin.math.pow import kotlin.math.pow
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.metadata.TextTags import org.oxycblt.auxio.music.metadata.TextTags
@ -43,10 +43,12 @@ import org.oxycblt.auxio.util.logD
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class ReplayGainAudioProcessor(context: Context) : class ReplayGainAudioProcessor
BaseAudioProcessor(), Player.Listener, PlaybackSettings.Listener { @Inject
private val playbackManager = PlaybackStateManager.get() constructor(
private val playbackSettings = PlaybackSettings.from(context) private val playbackManager: PlaybackStateManager,
private val playbackSettings: PlaybackSettings
) : BaseAudioProcessor(), Player.Listener, PlaybackSettings.Listener {
private var lastFormat: Format? = null private var lastFormat: Format? = null
private var volume = 1f private var volume = 1f

View file

@ -17,6 +17,7 @@
package org.oxycblt.auxio.playback.state package org.oxycblt.auxio.playback.state
import javax.inject.Inject
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
@ -40,8 +41,6 @@ import org.oxycblt.auxio.util.logW
* Internal consumers should usually use [Listener], however the component that manages the player * Internal consumers should usually use [Listener], however the component that manages the player
* itself should instead use [InternalPlayer]. * itself should instead use [InternalPlayer].
* *
* All access should be done with [get].
*
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
interface PlaybackStateManager { interface PlaybackStateManager {
@ -270,32 +269,9 @@ interface PlaybackStateManager {
val positionMs: Long, val positionMs: Long,
val repeatMode: RepeatMode, val repeatMode: RepeatMode,
) )
companion object {
@Volatile private var INSTANCE: PlaybackStateManager? = null
/**
* Get a singleton instance.
* @return The (possibly newly-created) singleton instance.
*/
fun get(): PlaybackStateManager {
val currentInstance = INSTANCE
logD(currentInstance)
if (currentInstance != null) {
return currentInstance
}
synchronized(this) {
val newInstance = PlaybackStateManagerImpl()
INSTANCE = newInstance
return newInstance
}
}
}
} }
private class PlaybackStateManagerImpl : PlaybackStateManager { class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
private val listeners = mutableListOf<PlaybackStateManager.Listener>() private val listeners = mutableListOf<PlaybackStateManager.Listener>()
@Volatile private var internalPlayer: InternalPlayer? = null @Volatile private var internalPlayer: InternalPlayer? = null
@Volatile private var pendingAction: InternalPlayer.Action? = null @Volatile private var pendingAction: InternalPlayer.Action? = null

View file

@ -22,15 +22,19 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
/** /**
* A [BroadcastReceiver] that forwards [Intent.ACTION_MEDIA_BUTTON] [Intent]s to [PlaybackService]. * A [BroadcastReceiver] that forwards [Intent.ACTION_MEDIA_BUTTON] [Intent]s to [PlaybackService].
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@AndroidEntryPoint
class MediaButtonReceiver : BroadcastReceiver() { class MediaButtonReceiver : BroadcastReceiver() {
@Inject lateinit var playbackManager: PlaybackStateManager
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val playbackManager = PlaybackStateManager.get()
if (playbackManager.queue.currentSong != null) { if (playbackManager.queue.currentSong != null) {
// We have a song, so we can assume that the service will start a foreground state. // We have a song, so we can assume that the service will start a foreground state.
// At least, I hope. Again, *this is why we don't do this*. I cannot describe how // At least, I hope. Again, *this is why we don't do this*. I cannot describe how

View file

@ -27,6 +27,8 @@ import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import androidx.media.session.MediaButtonReceiver import androidx.media.session.MediaButtonReceiver
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.BitmapProvider import org.oxycblt.auxio.image.BitmapProvider
@ -44,11 +46,15 @@ import org.oxycblt.auxio.util.logD
/** /**
* A component that mirrors the current playback state into the [MediaSessionCompat] and * A component that mirrors the current playback state into the [MediaSessionCompat] and
* [NotificationComponent]. * [NotificationComponent].
* @param context [Context] required to initialize components.
* @param listener [Listener] to forward notification updates to.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class MediaSessionComponent(private val context: Context, private val listener: Listener) : class MediaSessionComponent
@Inject
constructor(
@ApplicationContext private val context: Context,
private val playbackManager: PlaybackStateManager,
private val playbackSettings: PlaybackSettings
) :
MediaSessionCompat.Callback(), MediaSessionCompat.Callback(),
PlaybackStateManager.Listener, PlaybackStateManager.Listener,
ImageSettings.Listener, ImageSettings.Listener,
@ -59,12 +65,11 @@ class MediaSessionComponent(private val context: Context, private val listener:
setQueueTitle(context.getString(R.string.lbl_queue)) setQueueTitle(context.getString(R.string.lbl_queue))
} }
private val playbackManager = PlaybackStateManager.get()
private val playbackSettings = PlaybackSettings.from(context)
private val notification = NotificationComponent(context, mediaSession.sessionToken) private val notification = NotificationComponent(context, mediaSession.sessionToken)
private val provider = BitmapProvider(context) private val provider = BitmapProvider(context)
private var listener: Listener? = null
init { init {
playbackManager.addListener(this) playbackManager.addListener(this)
playbackSettings.registerListener(this) playbackSettings.registerListener(this)
@ -79,11 +84,20 @@ class MediaSessionComponent(private val context: Context, private val listener:
MediaButtonReceiver.handleIntent(mediaSession, intent) MediaButtonReceiver.handleIntent(mediaSession, intent)
} }
/**
* Register a [Listener] for notification updates to this service.
* @param listener The [Listener] to register.
*/
fun registerListener(listener: Listener) {
this.listener = listener
}
/** /**
* Release this instance, closing the [MediaSessionCompat] and preventing any further updates to * Release this instance, closing the [MediaSessionCompat] and preventing any further updates to
* the [NotificationComponent]. * the [NotificationComponent].
*/ */
fun release() { fun release() {
listener = null
provider.release() provider.release()
playbackSettings.unregisterListener(this) playbackSettings.unregisterListener(this)
playbackManager.removeListener(this) playbackManager.removeListener(this)
@ -135,7 +149,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
invalidateSessionState() invalidateSessionState()
notification.updatePlaying(playbackManager.playerState.isPlaying) notification.updatePlaying(playbackManager.playerState.isPlaying)
if (!provider.isBusy) { if (!provider.isBusy) {
listener.onPostNotification(notification) listener?.onPostNotification(notification)
} }
} }
@ -316,7 +330,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
val metadata = builder.build() val metadata = builder.build()
mediaSession.setMetadata(metadata) mediaSession.setMetadata(metadata)
notification.updateMetadata(metadata) notification.updateMetadata(metadata)
listener.onPostNotification(notification) listener?.onPostNotification(notification)
} }
}) })
} }
@ -403,7 +417,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
} }
if (!provider.isBusy) { if (!provider.isBusy) {
listener.onPostNotification(notification) listener?.onPostNotification(notification)
} }
} }

View file

@ -86,11 +86,11 @@ class PlaybackService :
MusicRepository.Listener { MusicRepository.Listener {
// Player components // Player components
private lateinit var player: ExoPlayer private lateinit var player: ExoPlayer
private lateinit var replayGainProcessor: ReplayGainAudioProcessor @Inject lateinit var replayGainProcessor: ReplayGainAudioProcessor
// System backend components // System backend components
private lateinit var mediaSessionComponent: MediaSessionComponent @Inject lateinit var mediaSessionComponent: MediaSessionComponent
private lateinit var widgetComponent: WidgetComponent @Inject lateinit var widgetComponent: WidgetComponent
private val systemReceiver = PlaybackReceiver() private val systemReceiver = PlaybackReceiver()
// Shared components // Shared components
@ -115,8 +115,6 @@ class PlaybackService :
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// Initialize the player component.
replayGainProcessor = ReplayGainAudioProcessor(this)
// Enable constant bitrate seeking so that certain MP3s/AACs are seekable // Enable constant bitrate seeking so that certain MP3s/AACs are seekable
val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true) val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)
// Since Auxio is a music player, only specify an audio renderer to save // Since Auxio is a music player, only specify an audio renderer to save
@ -155,8 +153,7 @@ class PlaybackService :
// condition to cause us to load music before we were fully initialize. // condition to cause us to load music before we were fully initialize.
playbackManager.registerInternalPlayer(this) playbackManager.registerInternalPlayer(this)
musicRepository.addListener(this) musicRepository.addListener(this)
widgetComponent = WidgetComponent(this) mediaSessionComponent.registerListener(this)
mediaSessionComponent = MediaSessionComponent(this, this)
registerReceiver( registerReceiver(
systemReceiver, systemReceiver,
IntentFilter().apply { IntentFilter().apply {

View file

@ -22,6 +22,8 @@ import android.graphics.Bitmap
import android.os.Build import android.os.Build
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.BitmapProvider import org.oxycblt.auxio.image.BitmapProvider
import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.image.ImageSettings
@ -39,14 +41,16 @@ import org.oxycblt.auxio.util.logD
/** /**
* A component that manages the "Now Playing" state. This is kept separate from the [WidgetProvider] * A component that manages the "Now Playing" state. This is kept separate from the [WidgetProvider]
* itself to prevent possible memory leaks and enable extension to more widgets in the future. * itself to prevent possible memory leaks and enable extension to more widgets in the future.
* @param context [Context] required to manage AppWidgetProviders.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class WidgetComponent(private val context: Context) : class WidgetComponent
PlaybackStateManager.Listener, UISettings.Listener, ImageSettings.Listener { @Inject
private val playbackManager = PlaybackStateManager.get() constructor(
private val uiSettings = UISettings.from(context) @ApplicationContext private val context: Context,
private val imageSettings = ImageSettings.from(context) private val imageSettings: ImageSettings,
private val playbackManager: PlaybackStateManager,
private val uiSettings: UISettings
) : PlaybackStateManager.Listener, UISettings.Listener, ImageSettings.Listener {
private val widgetProvider = WidgetProvider() private val widgetProvider = WidgetProvider()
private val provider = BitmapProvider(context) private val provider = BitmapProvider(context)
@ -109,9 +113,10 @@ class WidgetComponent(private val context: Context) :
/** Release this instance, preventing any further events from updating the widget instances. */ /** Release this instance, preventing any further events from updating the widget instances. */
fun release() { fun release() {
provider.release() provider.release()
imageSettings.unregisterListener(this)
playbackManager.removeListener(this)
uiSettings.unregisterListener(this) uiSettings.unregisterListener(this)
widgetProvider.reset(context) widgetProvider.reset(context)
playbackManager.removeListener(this)
} }
// --- CALLBACKS --- // --- CALLBACKS ---

View file

@ -15,19 +15,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music.parsing package org.oxycblt.auxio.music.metadata
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.oxycblt.auxio.music.FakeMusicSettings import org.oxycblt.auxio.music.FakeMusicSettings
import org.oxycblt.auxio.music.metadata.correctWhitespace
import org.oxycblt.auxio.music.metadata.parseId3GenreNames
import org.oxycblt.auxio.music.metadata.parseId3v2PositionField
import org.oxycblt.auxio.music.metadata.parseMultiValue
import org.oxycblt.auxio.music.metadata.parseVorbisPositionField
import org.oxycblt.auxio.music.metadata.splitEscaped
class ParsingUtilTest { class TagUtilTest {
@Test @Test
fun parseMultiValue_single() { fun parseMultiValue_single() {
assertEquals( assertEquals(