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) {
completeSongs.send(rawSong)
} else {
query.populateFileInfo(rawSong)
query.populateTags(rawSong)
incompleteSongs.send(rawSong)
}
yield()

View file

@ -17,17 +17,19 @@
package org.oxycblt.auxio.playback
import android.content.Context
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.PlaybackStateManagerImpl
@Module
@InstallIn(SingletonComponent::class)
class PlaybackModule {
@Provides fun stateManager() = PlaybackStateManager.get()
@Provides fun settings(@ApplicationContext context: Context) = PlaybackSettings.from(context)
interface PlaybackModule {
@Singleton
@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 androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicMode
@ -65,17 +67,9 @@ interface PlaybackSettings : Settings<PlaybackSettings.Listener> {
/** Called when [notificationAction] has changed. */
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 {
override val inListPlaybackMode: MusicMode
get() =

View file

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

View file

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

View file

@ -17,6 +17,7 @@
package org.oxycblt.auxio.playback.state
import javax.inject.Inject
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.MusicParent
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
* itself should instead use [InternalPlayer].
*
* All access should be done with [get].
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface PlaybackStateManager {
@ -270,32 +269,9 @@ interface PlaybackStateManager {
val positionMs: Long,
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>()
@Volatile private var internalPlayer: InternalPlayer? = 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.Intent
import androidx.core.content.ContextCompat
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.playback.state.PlaybackStateManager
/**
* A [BroadcastReceiver] that forwards [Intent.ACTION_MEDIA_BUTTON] [Intent]s to [PlaybackService].
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class MediaButtonReceiver : BroadcastReceiver() {
@Inject lateinit var playbackManager: PlaybackStateManager
override fun onReceive(context: Context, intent: Intent) {
val playbackManager = PlaybackStateManager.get()
if (playbackManager.queue.currentSong != null) {
// 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

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.PlaybackStateCompat
import androidx.media.session.MediaButtonReceiver
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
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
* [NotificationComponent].
* @param context [Context] required to initialize components.
* @param listener [Listener] to forward notification updates to.
* @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(),
PlaybackStateManager.Listener,
ImageSettings.Listener,
@ -59,12 +65,11 @@ class MediaSessionComponent(private val context: Context, private val listener:
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 provider = BitmapProvider(context)
private var listener: Listener? = null
init {
playbackManager.addListener(this)
playbackSettings.registerListener(this)
@ -79,11 +84,20 @@ class MediaSessionComponent(private val context: Context, private val listener:
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
* the [NotificationComponent].
*/
fun release() {
listener = null
provider.release()
playbackSettings.unregisterListener(this)
playbackManager.removeListener(this)
@ -135,7 +149,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
invalidateSessionState()
notification.updatePlaying(playbackManager.playerState.isPlaying)
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()
mediaSession.setMetadata(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) {
listener.onPostNotification(notification)
listener?.onPostNotification(notification)
}
}

View file

@ -86,11 +86,11 @@ class PlaybackService :
MusicRepository.Listener {
// Player components
private lateinit var player: ExoPlayer
private lateinit var replayGainProcessor: ReplayGainAudioProcessor
@Inject lateinit var replayGainProcessor: ReplayGainAudioProcessor
// System backend components
private lateinit var mediaSessionComponent: MediaSessionComponent
private lateinit var widgetComponent: WidgetComponent
@Inject lateinit var mediaSessionComponent: MediaSessionComponent
@Inject lateinit var widgetComponent: WidgetComponent
private val systemReceiver = PlaybackReceiver()
// Shared components
@ -115,8 +115,6 @@ class PlaybackService :
override fun onCreate() {
super.onCreate()
// Initialize the player component.
replayGainProcessor = ReplayGainAudioProcessor(this)
// Enable constant bitrate seeking so that certain MP3s/AACs are seekable
val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)
// 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.
playbackManager.registerInternalPlayer(this)
musicRepository.addListener(this)
widgetComponent = WidgetComponent(this)
mediaSessionComponent = MediaSessionComponent(this, this)
mediaSessionComponent.registerListener(this)
registerReceiver(
systemReceiver,
IntentFilter().apply {

View file

@ -22,6 +22,8 @@ import android.graphics.Bitmap
import android.os.Build
import coil.request.ImageRequest
import coil.transform.RoundedCornersTransformation
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.BitmapProvider
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]
* 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)
*/
class WidgetComponent(private val context: Context) :
PlaybackStateManager.Listener, UISettings.Listener, ImageSettings.Listener {
private val playbackManager = PlaybackStateManager.get()
private val uiSettings = UISettings.from(context)
private val imageSettings = ImageSettings.from(context)
class WidgetComponent
@Inject
constructor(
@ApplicationContext private val context: 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 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. */
fun release() {
provider.release()
imageSettings.unregisterListener(this)
playbackManager.removeListener(this)
uiSettings.unregisterListener(this)
widgetProvider.reset(context)
playbackManager.removeListener(this)
}
// --- CALLBACKS ---

View file

@ -15,19 +15,13 @@
* 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.Test
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
fun parseMultiValue_single() {
assertEquals(