all: use binds di

Use @Binds more heavily with dependency injection, whee currently
reasonable.

Reduces the amount of boilerplate "fun from" functions that need to be
used.
This commit is contained in:
Alexander Capehart 2023-02-11 16:29:47 -07:00
parent 6e55801513
commit 833ddceba4
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
34 changed files with 250 additions and 194 deletions

View file

@ -26,6 +26,7 @@ import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.request.CachePolicy
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import org.oxycblt.auxio.image.ImageSettings
import org.oxycblt.auxio.image.extractor.AlbumCoverFetcher
import org.oxycblt.auxio.image.extractor.ArtistImageFetcher
@ -41,12 +42,16 @@ import org.oxycblt.auxio.ui.UISettings
*/
@HiltAndroidApp
class Auxio : Application(), ImageLoaderFactory {
@Inject lateinit var imageSettings: ImageSettings
@Inject lateinit var playbackSettings: PlaybackSettings
@Inject lateinit var uiSettings: UISettings
override fun onCreate() {
super.onCreate()
// Migrate any settings that may have changed in an app update.
ImageSettings.from(this).migrate()
PlaybackSettings.from(this).migrate()
UISettings.from(this).migrate()
imageSettings.migrate()
playbackSettings.migrate()
uiSettings.migrate()
// Adding static shortcuts in a dynamic manner is better than declaring them
// manually, as it will properly handle the difference between debug and release
// Auxio instances.

View file

@ -26,6 +26,7 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.WindowCompat
import androidx.core.view.updatePadding
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.databinding.ActivityMainBinding
import org.oxycblt.auxio.music.system.IndexerService
import org.oxycblt.auxio.playback.PlaybackViewModel
@ -54,6 +55,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val playbackModel: PlaybackViewModel by viewModels()
@Inject lateinit var uiSettings: UISettings
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -83,17 +85,16 @@ class MainActivity : AppCompatActivity() {
}
private fun setupTheme() {
val settings = UISettings.from(this)
// Apply the theme configuration.
AppCompatDelegate.setDefaultNightMode(settings.theme)
AppCompatDelegate.setDefaultNightMode(uiSettings.theme)
// Apply the color scheme. The black theme requires it's own set of themes since
// it's not possible to modify the themes at run-time.
if (isNight && settings.useBlackTheme) {
logD("Applying black theme [accent ${settings.accent}]")
setTheme(settings.accent.blackTheme)
if (isNight && uiSettings.useBlackTheme) {
logD("Applying black theme [accent ${uiSettings.accent}]")
setTheme(uiSettings.accent.blackTheme)
} else {
logD("Applying normal theme [accent ${settings.accent}]")
setTheme(settings.accent.theme)
logD("Applying normal theme [accent ${uiSettings.accent}]")
setTheme(uiSettings.accent.theme)
}
}

View file

@ -17,15 +17,13 @@
package org.oxycblt.auxio.home
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
@Module
@InstallIn(SingletonComponent::class)
class HomeModule {
@Provides fun settings(@ApplicationContext context: Context) = HomeSettings.from(context)
interface HomeModule {
@Binds fun settings(homeSettings: HomeSettingsImpl): HomeSettings
}

View file

@ -19,6 +19,8 @@ package org.oxycblt.auxio.home
import android.content.Context
import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.settings.Settings
@ -40,18 +42,10 @@ interface HomeSettings : Settings<HomeSettings.Listener> {
/** Called when the [shouldHideCollaborators] configuration changes. */
fun onHideCollaboratorsChanged()
}
companion object {
/**
* Get a framework-backed implementation.
* @param context [Context] required.
*/
fun from(context: Context): HomeSettings = RealHomeSettings(context)
}
}
private class RealHomeSettings(context: Context) :
Settings.Real<HomeSettings.Listener>(context), HomeSettings {
class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context) :
Settings.Impl<HomeSettings.Listener>(context), HomeSettings {
override var homeTabs: Array<Tab>
get() =
Tab.fromIntCode(

View file

@ -23,6 +23,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogTabsBinding
@ -40,6 +41,7 @@ class TabCustomizeDialog :
ViewBindingDialogFragment<DialogTabsBinding>(), EditableListListener<Tab> {
private val tabAdapter = TabAdapter(this)
private var touchHelper: ItemTouchHelper? = null
@Inject lateinit var homeSettings: HomeSettings
override fun onCreateBinding(inflater: LayoutInflater) = DialogTabsBinding.inflate(inflater)
@ -48,13 +50,13 @@ class TabCustomizeDialog :
.setTitle(R.string.set_lib_tabs)
.setPositiveButton(R.string.lbl_ok) { _, _ ->
logD("Committing tab changes")
HomeSettings.from(requireContext()).homeTabs = tabAdapter.tabs
homeSettings.homeTabs = tabAdapter.tabs
}
.setNegativeButton(R.string.lbl_cancel, null)
}
override fun onBindingCreated(binding: DialogTabsBinding, savedInstanceState: Bundle?) {
var tabs = HomeSettings.from(requireContext()).homeTabs
var tabs = homeSettings.homeTabs
// Try to restore a pending tab configuration that was saved prior.
if (savedInstanceState != null) {
val savedTabs = Tab.fromIntCode(savedInstanceState.getInt(KEY_TABS))

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.image
import android.content.Context
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -26,6 +27,6 @@ import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
class ImageModule {
@Provides fun settings(@ApplicationContext context: Context) = ImageSettings.from(context)
interface ImageModule {
@Binds fun settings(imageSettings: ImageSettingsImpl): ImageSettings
}

View file

@ -41,12 +41,12 @@ interface ImageSettings : Settings<ImageSettings.Listener> {
* Get a framework-backed implementation.
* @param context [Context] required.
*/
fun from(context: Context): ImageSettings = RealImageSettings(context)
fun from(context: Context): ImageSettings = ImageSettingsImpl(context)
}
}
private class RealImageSettings(context: Context) :
Settings.Real<ImageSettings.Listener>(context), ImageSettings {
class ImageSettingsImpl(context: Context) :
Settings.Impl<ImageSettings.Listener>(context), ImageSettings {
override val coverMode: CoverMode
get() =
CoverMode.fromIntCode(

View file

@ -26,6 +26,8 @@ import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.widget.ImageViewCompat
import com.google.android.material.shape.MaterialShapeDrawable
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.math.max
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.UISettings
@ -41,6 +43,7 @@ import org.oxycblt.auxio.util.getDrawableCompat
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class PlaybackIndicatorView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
@ -52,6 +55,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
private val indicatorMatrix = Matrix()
private val indicatorMatrixSrc = RectF()
private val indicatorMatrixDst = RectF()
@Inject lateinit var uiSettings: UISettings
/**
* The corner radius of this view. This allows the outer ImageGroup to apply it's corner radius
@ -61,7 +65,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
set(value) {
field = value
(background as? MaterialShapeDrawable)?.let { bg ->
if (UISettings.from(context).roundMode) {
if (uiSettings.roundMode) {
bg.setCornerSize(value)
} else {
bg.setCornerSize(0f)

View file

@ -32,6 +32,8 @@ import androidx.core.graphics.drawable.DrawableCompat
import coil.dispose
import coil.load
import com.google.android.material.shape.MaterialShapeDrawable
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.extractor.SquareFrameTransform
import org.oxycblt.auxio.music.Album
@ -53,10 +55,13 @@ import org.oxycblt.auxio.util.getDrawableCompat
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class StyledImageView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
AppCompatImageView(context, attrs, defStyleAttr) {
@Inject lateinit var uiSettings: UISettings
init {
// Load view attributes
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
@ -81,7 +86,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
background =
MaterialShapeDrawable().apply {
fillColor = context.getColorCompat(R.color.sel_cover_bg)
if (UISettings.from(context).roundMode) {
if (uiSettings.roundMode) {
// Only use the specified corner radius when round mode is enabled.
setCornerSize(cornerRadius)
}

View file

@ -63,7 +63,7 @@ interface ListDiffer<T, I> {
class Async<T>(private val diffCallback: DiffUtil.ItemCallback<T>) :
Factory<T, BasicListInstructions>() {
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T, BasicListInstructions> =
RealAsyncListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
AsyncListDifferImpl(AdapterListUpdateCallback(adapter), diffCallback)
}
/**
@ -75,7 +75,7 @@ interface ListDiffer<T, I> {
class Blocking<T>(private val diffCallback: DiffUtil.ItemCallback<T>) :
Factory<T, BasicListInstructions>() {
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T, BasicListInstructions> =
RealBlockingListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
BlockingListDifferImpl(AdapterListUpdateCallback(adapter), diffCallback)
}
}
@ -113,7 +113,7 @@ private abstract class BasicListDiffer<T> : ListDiffer<T, BasicListInstructions>
protected abstract fun replaceList(newList: List<T>, onDone: () -> Unit)
}
private class RealAsyncListDiffer<T>(
private class AsyncListDifferImpl<T>(
updateCallback: ListUpdateCallback,
diffCallback: DiffUtil.ItemCallback<T>
) : BasicListDiffer<T>() {
@ -132,7 +132,7 @@ private class RealAsyncListDiffer<T>(
}
}
private class RealBlockingListDiffer<T>(
private class BlockingListDifferImpl<T>(
private val updateCallback: ListUpdateCallback,
private val diffCallback: DiffUtil.ItemCallback<T>
) : BasicListDiffer<T>() {

View file

@ -28,7 +28,7 @@ import org.oxycblt.auxio.music.system.IndexerImpl
@Module
@InstallIn(SingletonComponent::class)
interface MusicModule {
@Singleton @Binds fun musicRepository(musicRepository: MusicRepositoryImpl): MusicRepository
@Singleton @Binds fun repository(musicRepository: MusicRepositoryImpl): MusicRepository
@Singleton @Binds fun indexer(indexer: IndexerImpl): Indexer
@Binds fun settings(musicSettingsImpl: MusicSettingsImpl): MusicSettings
}

View file

@ -63,18 +63,10 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
/** Called when the [shouldBeObserving] configuration has changed. */
fun onObservingChanged() {}
}
companion object {
/**
* Get a framework-backed implementation.
* @param context [Context] required.
*/
fun from(context: Context): MusicSettings = MusicSettingsImpl(context)
}
}
class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context) :
Settings.Real<MusicSettings.Listener>(context), MusicSettings {
Settings.Impl<MusicSettings.Listener>(context), MusicSettings {
private val storageManager = context.getSystemServiceCompat(StorageManager::class)
override var musicDirs: MusicDirectories

View file

@ -17,17 +17,15 @@
package org.oxycblt.auxio.music.cache
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 org.oxycblt.auxio.music.extractor.CacheRepository
import org.oxycblt.auxio.music.extractor.CacheRepositoryImpl
@Module
@InstallIn(SingletonComponent::class)
class CacheModule {
@Provides
fun cacheRepository(@ApplicationContext context: Context) = CacheRepository.from(context)
interface CacheModule {
@Binds fun cacheRepository(cacheRepository: CacheRepositoryImpl): CacheRepository
}

View file

@ -18,12 +18,61 @@
package org.oxycblt.auxio.music.extractor
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.music.cache.CacheDatabase
import org.oxycblt.auxio.music.cache.CachedSong
import org.oxycblt.auxio.music.cache.CachedSongsDao
import org.oxycblt.auxio.music.model.RawSong
import org.oxycblt.auxio.util.*
/**
* A repository allowing access to cached metadata obtained in prior music loading operations.
* @author Alexander Capehart (OxygenCobalt)
*/
interface CacheRepository {
/**
* Read the current [Cache], if it exists.
* @return The stored [Cache], or null if it could not be obtained.
*/
suspend fun readCache(): Cache?
/**
* Write the list of newly-loaded [RawSong]s to the cache, replacing the prior data.
* @param rawSongs The [rawSongs] to write to the cache.
*/
suspend fun writeCache(rawSongs: List<RawSong>)
}
class CacheRepositoryImpl @Inject constructor(@ApplicationContext private val context: Context) :
CacheRepository {
private val cachedSongsDao: CachedSongsDao by lazy {
CacheDatabase.getInstance(context).cachedSongsDao()
}
override suspend fun readCache(): Cache? =
try {
// Faster to load the whole database into memory than do a query on each
// populate call.
CacheImpl(cachedSongsDao.readSongs())
} catch (e: Exception) {
logE("Unable to load cache database.")
logE(e.stackTraceToString())
null
}
override suspend fun writeCache(rawSongs: List<RawSong>) {
try {
// Still write out whatever data was extracted.
cachedSongsDao.nukeSongs()
cachedSongsDao.insertSongs(rawSongs.map(CachedSong::fromRaw))
} catch (e: Exception) {
logE("Unable to save cache database.")
logE(e.stackTraceToString())
}
}
}
/**
* A cache of music metadata obtained in prior music loading operations. Obtain an instance with
* [CacheRepository].
@ -41,7 +90,7 @@ interface Cache {
fun populate(rawSong: RawSong): Boolean
}
private class RealCache(cachedSongs: List<CachedSong>) : Cache {
private class CacheImpl(cachedSongs: List<CachedSong>) : Cache {
private val cacheMap = buildMap {
for (cachedSong in cachedSongs) {
put(cachedSong.mediaStoreId, cachedSong)
@ -69,58 +118,3 @@ private class RealCache(cachedSongs: List<CachedSong>) : Cache {
return false
}
}
/**
* A repository allowing access to cached metadata obtained in prior music loading operations.
* @author Alexander Capehart (OxygenCobalt)
*/
interface CacheRepository {
/**
* Read the current [Cache], if it exists.
* @return The stored [Cache], or null if it could not be obtained.
*/
suspend fun readCache(): Cache?
/**
* Write the list of newly-loaded [RawSong]s to the cache, replacing the prior data.
* @param rawSongs The [rawSongs] to write to the cache.
*/
suspend fun writeCache(rawSongs: List<RawSong>)
companion object {
/**
* Create a framework-backed instance.
* @param context [Context] required.
* @return A new instance.
*/
fun from(context: Context): CacheRepository = RealCacheRepository(context)
}
}
private class RealCacheRepository(private val context: Context) : CacheRepository {
private val cachedSongsDao: CachedSongsDao by lazy {
CacheDatabase.getInstance(context).cachedSongsDao()
}
override suspend fun readCache() =
try {
// Faster to load the whole database into memory than do a query on each
// populate call.
RealCache(cachedSongsDao.readSongs())
} catch (e: Exception) {
logE("Unable to load cache database.")
logE(e.stackTraceToString())
null
}
override suspend fun writeCache(rawSongs: List<RawSong>) {
try {
// Still write out whatever data was extracted.
cachedSongsDao.nukeSongs()
cachedSongsDao.insertSongs(rawSongs.map(CachedSong::fromRaw))
} catch (e: Exception) {
logE("Unable to save cache database.")
logE(e.stackTraceToString())
}
}
}

View file

@ -22,6 +22,8 @@ import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.core.view.children
import com.google.android.material.checkbox.MaterialCheckBox
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
@ -33,7 +35,10 @@ import org.oxycblt.auxio.ui.ViewBindingDialogFragment
* split tags with multiple values.
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
@Inject lateinit var musicSettings: MusicSettings
override fun onCreateBinding(inflater: LayoutInflater) =
DialogSeparatorsBinding.inflate(inflater)
@ -42,7 +47,7 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
.setTitle(R.string.set_separators)
.setNegativeButton(R.string.lbl_cancel, null)
.setPositiveButton(R.string.lbl_save) { _, _ ->
MusicSettings.from(requireContext()).multiValueSeparators = getCurrentSeparators()
musicSettings.multiValueSeparators = getCurrentSeparators()
}
}
@ -59,7 +64,7 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
// the corresponding CheckBox for each character instead of doing an iteration
// through the separator list for each CheckBox.
(savedInstanceState?.getString(KEY_PENDING_SEPARATORS)
?: MusicSettings.from(requireContext()).multiValueSeparators)
?: musicSettings.multiValueSeparators)
.forEach {
when (it) {
Separators.COMMA -> binding.separatorComma.isChecked = true

View file

@ -81,7 +81,7 @@ interface MediaStoreExtractor {
* Create a framework-backed instance.
* @param context [Context] required.
* @param musicSettings [MusicSettings] required.
* @return A new [RealMediaStoreExtractor] that will work best on the device's API level.
* @return A new [MediaStoreExtractor] that will work best on the device's API level.
*/
fun from(context: Context, musicSettings: MusicSettings): MediaStoreExtractor =
when {
@ -94,7 +94,7 @@ interface MediaStoreExtractor {
}
}
private abstract class RealMediaStoreExtractor(
private abstract class BaseMediaStoreExtractor(
protected val context: Context,
private val musicSettings: MusicSettings
) : MediaStoreExtractor {
@ -352,7 +352,7 @@ private abstract class RealMediaStoreExtractor(
// speed, we only want to add redundancy on known issues, not with possible issues.
private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSettings) :
RealMediaStoreExtractor(context, musicSettings) {
BaseMediaStoreExtractor(context, musicSettings) {
override val projection: Array<String>
get() =
super.projection +
@ -385,7 +385,7 @@ private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSet
cursor: Cursor,
genreNamesMap: Map<Long, String>,
storageManager: StorageManager
) : RealMediaStoreExtractor.Query(cursor, genreNamesMap) {
) : BaseMediaStoreExtractor.Query(cursor, genreNamesMap) {
// Set up cursor indices for later use.
private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
private val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
@ -430,7 +430,7 @@ private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSet
}
/**
* A [RealMediaStoreExtractor] that implements common behavior supported from API 29 onwards.
* A [BaseMediaStoreExtractor] that implements common behavior supported from API 29 onwards.
* @param context [Context] required to query the media database.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -438,7 +438,7 @@ private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSet
private abstract class BaseApi29MediaStoreExtractor(
context: Context,
musicSettings: MusicSettings
) : RealMediaStoreExtractor(context, musicSettings) {
) : BaseMediaStoreExtractor(context, musicSettings) {
override val projection: Array<String>
get() =
super.projection +
@ -471,7 +471,7 @@ private abstract class BaseApi29MediaStoreExtractor(
cursor: Cursor,
genreNamesMap: Map<Long, String>,
storageManager: StorageManager
) : RealMediaStoreExtractor.Query(cursor, genreNamesMap) {
) : BaseMediaStoreExtractor.Query(cursor, genreNamesMap) {
private val volumeIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME)
private val relativePathIndex =
@ -493,7 +493,7 @@ private abstract class BaseApi29MediaStoreExtractor(
}
/**
* A [RealMediaStoreExtractor] that completes the music loading process in a way compatible with at
* A [BaseMediaStoreExtractor] that completes the music loading process in a way compatible with at
* API
* 29.
* @param context [Context] required to query the media database.
@ -533,7 +533,7 @@ private class Api29MediaStoreExtractor(context: Context, musicSettings: MusicSet
}
/**
* A [RealMediaStoreExtractor] that completes the music loading process in a way compatible from API
* A [BaseMediaStoreExtractor] that completes the music loading process in a way compatible from API
* 30 onwards.
* @param context [Context] required to query the media database.
* @author Alexander Capehart (OxygenCobalt)

View file

@ -29,6 +29,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
@ -48,6 +49,7 @@ class MusicDirsDialog :
private val dirAdapter = DirectoryAdapter(this)
private var openDocumentTreeLauncher: ActivityResultLauncher<Uri?>? = null
private var storageManager: StorageManager? = null
@Inject lateinit var musicSettings: MusicSettings
override fun onCreateBinding(inflater: LayoutInflater) =
DialogMusicDirsBinding.inflate(inflater)
@ -57,11 +59,10 @@ class MusicDirsDialog :
.setTitle(R.string.set_dirs)
.setNegativeButton(R.string.lbl_cancel, null)
.setPositiveButton(R.string.lbl_save) { _, _ ->
val settings = MusicSettings.from(requireContext())
val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
if (settings.musicDirs != newDirs) {
if (musicSettings.musicDirs != newDirs) {
logD("Committing changes")
settings.musicDirs = newDirs
musicSettings.musicDirs = newDirs
}
}
}
@ -98,7 +99,7 @@ class MusicDirsDialog :
itemAnimator = null
}
var dirs = MusicSettings.from(context).musicDirs
var dirs = musicSettings.musicDirs
if (savedInstanceState != null) {
val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS)
if (pendingDirs != null) {

View file

@ -67,7 +67,7 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
private lateinit var observingNotification: ObservingNotification
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var indexerContentObserver: SystemContentObserver
private lateinit var settings: MusicSettings
@Inject lateinit var musicSettings: MusicSettings
override fun onCreate() {
super.onCreate()
@ -82,8 +82,7 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
// Initialize any listener-dependent components last as we wouldn't want a listener race
// condition to cause us to load music before we were fully initialize.
indexerContentObserver = SystemContentObserver()
settings = MusicSettings.from(this)
settings.registerListener(this)
musicSettings.registerListener(this)
indexer.registerController(this)
// An indeterminate indexer and a missing library implies we are extremely early
// in app initialization so start loading music.
@ -107,7 +106,7 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
// Then cancel the listener-dependent components to ensure that stray reloading
// events will not occur.
indexerContentObserver.release()
settings.unregisterListener(this)
musicSettings.unregisterListener(this)
indexer.unregisterController(this)
// Then cancel any remaining music loading jobs.
serviceJob.cancel()
@ -197,7 +196,7 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
* currently monitoring the music library for changes.
*/
private fun updateIdleSession() {
if (settings.shouldBeObserving) {
if (musicSettings.shouldBeObserving) {
// There are a few reasons why we stay in the foreground with automatic rescanning:
// 1. Newer versions of Android have become more and more restrictive regarding
// how a foreground service starts. Thus, it's best to go foreground now so that
@ -287,7 +286,7 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
override fun run() {
// Check here if we should even start a reindex. This is much less bug-prone than
// registering and de-registering this component as this setting changes.
if (settings.shouldBeObserving) {
if (musicSettings.shouldBeObserving) {
onStartIndexing(true)
}
}

View file

@ -23,17 +23,11 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.oxycblt.auxio.playback.persist.PersistenceRepository
import org.oxycblt.auxio.playback.state.PlaybackStateManager
@Module
@InstallIn(SingletonComponent::class)
class PlaybackModule {
@Provides fun playbackStateManager() = PlaybackStateManager.get()
@Provides fun stateManager() = PlaybackStateManager.get()
@Provides fun settings(@ApplicationContext context: Context) = PlaybackSettings.from(context)
@Provides
fun persistenceRepository(@ApplicationContext context: Context) =
PersistenceRepository.from(context)
}

View file

@ -71,12 +71,12 @@ interface PlaybackSettings : Settings<PlaybackSettings.Listener> {
* Get a framework-backed implementation.
* @param context [Context] required.
*/
fun from(context: Context): PlaybackSettings = RealPlaybackSettings(context)
fun from(context: Context): PlaybackSettings = PlaybackSettingsImpl(context)
}
}
private class RealPlaybackSettings(context: Context) :
Settings.Real<PlaybackSettings.Listener>(context), PlaybackSettings {
class PlaybackSettingsImpl(context: Context) :
Settings.Impl<PlaybackSettings.Listener>(context), PlaybackSettings {
override val inListPlaybackMode: MusicMode
get() =
MusicMode.fromIntCode(

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.playback.persist
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface PersistenceModule {
@Binds fun repository(persistenceRepository: PersistenceRepositoryImpl): PersistenceRepository
}

View file

@ -18,6 +18,8 @@
package org.oxycblt.auxio.playback.persist
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.model.Library
import org.oxycblt.auxio.playback.queue.Queue
@ -47,11 +49,13 @@ interface PersistenceRepository {
* Get a framework-backed implementation.
* @param context [Context] required.
*/
fun from(context: Context): PersistenceRepository = RealPersistenceRepository(context)
fun from(context: Context): PersistenceRepository = PersistenceRepositoryImpl(context)
}
}
private class RealPersistenceRepository(private val context: Context) : PersistenceRepository {
class PersistenceRepositoryImpl
@Inject
constructor(@ApplicationContext private val context: Context) : PersistenceRepository {
private val database: PersistenceDatabase by lazy { PersistenceDatabase.getInstance(context) }
private val playbackStateDao: PlaybackStateDao by lazy { database.playbackStateDao() }
private val queueDao: QueueDao by lazy { database.queueDao() }

View file

@ -287,7 +287,7 @@ interface PlaybackStateManager {
}
synchronized(this) {
val newInstance = RealPlaybackStateManager()
val newInstance = PlaybackStateManagerImpl()
INSTANCE = newInstance
return newInstance
}
@ -295,7 +295,7 @@ interface PlaybackStateManager {
}
}
private class RealPlaybackStateManager : PlaybackStateManager {
private class PlaybackStateManagerImpl : PlaybackStateManager {
private val listeners = mutableListOf<PlaybackStateManager.Listener>()
@Volatile private var internalPlayer: InternalPlayer? = null
@Volatile private var pendingAction: InternalPlayer.Action? = null

View file

@ -18,7 +18,9 @@
package org.oxycblt.auxio.search
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import java.text.Normalizer
import javax.inject.Inject
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
@ -51,17 +53,10 @@ interface SearchEngine {
val artists: List<Artist>?,
val genres: List<Genre>?
)
companion object {
/**
* Get a framework-backed implementation.
* @param context [Context] required.
*/
fun from(context: Context): SearchEngine = RealSearchEngine(context)
}
}
private class RealSearchEngine(private val context: Context) : SearchEngine {
class SearchEngineImpl @Inject constructor(@ApplicationContext private val context: Context) :
SearchEngine {
override suspend fun search(items: SearchEngine.Items, query: String) =
SearchEngine.Items(
songs = items.songs?.searchListImpl(query) { q, song -> song.path.name.contains(q) },

View file

@ -17,16 +17,14 @@
package org.oxycblt.auxio.search
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
@Module
@InstallIn(SingletonComponent::class)
class SearchModule {
@Provides fun engine(@ApplicationContext context: Context) = SearchEngine.from(context)
@Provides fun settings(@ApplicationContext context: Context) = SearchSettings.from(context)
interface SearchModule {
@Binds fun engine(searchEngine: SearchEngineImpl): SearchEngine
@Binds fun settings(searchSettings: SearchSettingsImpl): SearchSettings
}

View file

@ -19,6 +19,8 @@ package org.oxycblt.auxio.search
import android.content.Context
import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.settings.Settings
@ -30,18 +32,10 @@ import org.oxycblt.auxio.settings.Settings
interface SearchSettings : Settings<Nothing> {
/** The type of Music the search view is currently filtering to. */
var searchFilterMode: MusicMode?
companion object {
/**
* Get a framework-backed implementation.
* @param context [Context] required.
*/
fun from(context: Context): SearchSettings = RealSearchSettings(context)
}
}
private class RealSearchSettings(context: Context) :
Settings.Real<Nothing>(context), SearchSettings {
class SearchSettingsImpl @Inject constructor(@ApplicationContext context: Context) :
Settings.Impl<Nothing>(context), SearchSettings {
override var searchFilterMode: MusicMode?
get() =
MusicMode.fromIntCode(

View file

@ -54,7 +54,7 @@ interface Settings<L> {
* A framework-backed [Settings] implementation.
* @param context [Context] required.
*/
abstract class Real<L>(private val context: Context) :
abstract class Impl<L>(private val context: Context) :
Settings<L>, SharedPreferences.OnSharedPreferenceChangeListener {
protected val sharedPreferences: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context.applicationContext)

View file

@ -20,6 +20,8 @@ package org.oxycblt.auxio.settings.categories
import androidx.appcompat.app.AppCompatDelegate
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.settings.BasePreferenceFragment
import org.oxycblt.auxio.settings.ui.WrappedDialogPreference
@ -30,7 +32,10 @@ import org.oxycblt.auxio.util.isNight
* Display preferences.
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) {
@Inject lateinit var uiSettings: UISettings
override fun onOpenDialogPreference(preference: WrappedDialogPreference) {
if (preference.key == getString(R.string.set_key_accent)) {
findNavController().navigate(UIPreferenceFragmentDirections.goToAccentDialog())
@ -47,7 +52,7 @@ class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) {
}
}
getString(R.string.set_key_accent) -> {
preference.summary = getString(UISettings.from(requireContext()).accent.name)
preference.summary = getString(uiSettings.accent.name)
}
getString(R.string.set_key_black_theme) -> {
preference.onPreferenceChangeListener =

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface UIModule {
@Binds fun settings(uiSettings: UISettingsImpl): UISettings
}

View file

@ -21,6 +21,8 @@ import android.content.Context
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.accent.Accent
@ -50,12 +52,12 @@ interface UISettings : Settings<UISettings.Listener> {
* Get a framework-backed implementation.
* @param context [Context] required.
*/
fun from(context: Context): UISettings = RealUISettings(context)
fun from(context: Context): UISettings = UISettingsImpl(context)
}
}
private class RealUISettings(context: Context) :
Settings.Real<UISettings.Listener>(context), UISettings {
class UISettingsImpl @Inject constructor(@ApplicationContext context: Context) :
Settings.Impl<UISettings.Listener>(context), UISettings {
override val theme: Int
get() =
sharedPreferences.getInt(

View file

@ -22,6 +22,7 @@ import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogAccentBinding
@ -39,6 +40,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
class AccentCustomizeDialog :
ViewBindingDialogFragment<DialogAccentBinding>(), ClickableListListener<Accent> {
private var accentAdapter = AccentAdapter(this)
@Inject lateinit var uiSettings: UISettings
override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater)
@ -46,14 +48,13 @@ class AccentCustomizeDialog :
builder
.setTitle(R.string.set_accent)
.setPositiveButton(R.string.lbl_ok) { _, _ ->
val settings = UISettings.from(requireContext())
if (accentAdapter.selectedAccent == settings.accent) {
if (accentAdapter.selectedAccent == uiSettings.accent) {
// Nothing to do.
return@setPositiveButton
}
logD("Applying new accent")
settings.accent = unlikelyToBeNull(accentAdapter.selectedAccent)
uiSettings.accent = unlikelyToBeNull(accentAdapter.selectedAccent)
requireActivity().recreate()
dismiss()
}
@ -67,7 +68,7 @@ class AccentCustomizeDialog :
if (savedInstanceState != null) {
Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT))
} else {
UISettings.from(requireContext()).accent
uiSettings.accent
})
}

View file

@ -27,10 +27,13 @@ import android.os.Bundle
import android.util.SizeF
import android.view.View
import android.widget.RemoteViews
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.system.PlaybackService
import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.*
/**
@ -38,7 +41,10 @@ import org.oxycblt.auxio.util.*
* state alongside actions to control it.
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class WidgetProvider : AppWidgetProvider() {
@Inject lateinit var uiSettings: UISettings
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
@ -139,7 +145,7 @@ class WidgetProvider : AppWidgetProvider() {
*/
private fun newThinLayout(context: Context, state: WidgetComponent.PlaybackState) =
newRemoteViews(context, R.layout.widget_thin)
.setupBackground(context)
.setupBackground()
.setupPlaybackState(context, state)
.setupTimelineControls(context, state)
@ -150,7 +156,7 @@ class WidgetProvider : AppWidgetProvider() {
*/
private fun newSmallLayout(context: Context, state: WidgetComponent.PlaybackState) =
newRemoteViews(context, R.layout.widget_small)
.setupBar(context)
.setupBar()
.setupCover(context, state)
.setupTimelineControls(context, state)
@ -161,7 +167,7 @@ class WidgetProvider : AppWidgetProvider() {
*/
private fun newMediumLayout(context: Context, state: WidgetComponent.PlaybackState) =
newRemoteViews(context, R.layout.widget_medium)
.setupBackground(context)
.setupBackground()
.setupPlaybackState(context, state)
.setupTimelineControls(context, state)
@ -172,7 +178,7 @@ class WidgetProvider : AppWidgetProvider() {
*/
private fun newWideLayout(context: Context, state: WidgetComponent.PlaybackState) =
newRemoteViews(context, R.layout.widget_wide)
.setupBar(context)
.setupBar()
.setupCover(context, state)
.setupFullControls(context, state)
@ -183,7 +189,7 @@ class WidgetProvider : AppWidgetProvider() {
*/
private fun newLargeLayout(context: Context, state: WidgetComponent.PlaybackState) =
newRemoteViews(context, R.layout.widget_large)
.setupBackground(context)
.setupBackground()
.setupPlaybackState(context, state)
.setupFullControls(context, state)
@ -192,11 +198,11 @@ class WidgetProvider : AppWidgetProvider() {
* "floating" drawable that sits in front of the cover and contains the controls.
* @param context [Context] required to set up the view.
*/
private fun RemoteViews.setupBar(context: Context): RemoteViews {
private fun RemoteViews.setupBar(): RemoteViews {
// Below API 31, enable a rounded bar only if round mode is enabled.
// On API 31+, the bar should always be round in order to fit in with other widgets.
val background =
if (useRoundedRemoteViews(context)) {
if (useRoundedRemoteViews(uiSettings)) {
R.drawable.ui_widget_bar_round
} else {
R.drawable.ui_widget_bar_system
@ -210,12 +216,12 @@ class WidgetProvider : AppWidgetProvider() {
* self-explanatory, being a solid-color background that sits behind the cover and controls.
* @param context [Context] required to set up the view.
*/
private fun RemoteViews.setupBackground(context: Context): RemoteViews {
private fun RemoteViews.setupBackground(): RemoteViews {
// Below API 31, enable a rounded background only if round mode is enabled.
// On API 31+, the background should always be round in order to fit in with other
// widgets.
val background =
if (useRoundedRemoteViews(context)) {
if (useRoundedRemoteViews(uiSettings)) {
R.drawable.ui_widget_bg_round
} else {
R.drawable.ui_widget_bg_system

View file

@ -137,8 +137,8 @@ fun AppWidgetManager.updateAppWidgetCompat(
/**
* Returns whether rounded UI elements are appropriate for the widget, either based on the current
* settings or if the widget has to fit in aesthetically with other widgets.
* @param context [Context] configuration to use.
* @param [uiSettings] [UISettings] required to obtain round mode configuration.
* @return true if to use round mode, false otherwise.
*/
fun useRoundedRemoteViews(context: Context) =
UISettings.from(context).roundMode || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
fun useRoundedRemoteViews(uiSettings: UISettings) =
uiSettings.roundMode || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S

View file

@ -117,7 +117,7 @@ class RawMusicTest {
}
@Test
fun albumRaw_equals_withRealArtists() {
fun albumRaw_equals_withArtists() {
val a =
RawAlbum(
mediaStoreId = -1,
@ -125,7 +125,7 @@ class RawMusicTest {
name = "Album",
sortName = null,
releaseType = null,
rawArtists = listOf(RawArtist(name = "RealArtist A")))
rawArtists = listOf(RawArtist(name = "Artist A")))
val b =
RawAlbum(
mediaStoreId = -1,
@ -133,7 +133,7 @@ class RawMusicTest {
name = "Album",
sortName = null,
releaseType = null,
rawArtists = listOf(RawArtist(name = "RealArtist B")))
rawArtists = listOf(RawArtist(name = "Artist B")))
assertTrue(a != b)
assertTrue(a.hashCode() != b.hashCode())
}