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:
parent
9f74fe8a20
commit
63e5a7ee69
11 changed files with 75 additions and 82 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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() =
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 ---
|
||||
|
|
|
@ -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(
|
Loading…
Reference in a new issue