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

View file

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

View file

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

View file

@ -19,6 +19,8 @@ package org.oxycblt.auxio.home
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.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
@ -40,18 +42,10 @@ interface HomeSettings : Settings<HomeSettings.Listener> {
/** Called when the [shouldHideCollaborators] configuration changes. */ /** Called when the [shouldHideCollaborators] configuration changes. */
fun onHideCollaboratorsChanged() 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) : class HomeSettingsImpl @Inject constructor(@ApplicationContext context: Context) :
Settings.Real<HomeSettings.Listener>(context), HomeSettings { Settings.Impl<HomeSettings.Listener>(context), HomeSettings {
override var homeTabs: Array<Tab> override var homeTabs: Array<Tab>
get() = get() =
Tab.fromIntCode( Tab.fromIntCode(

View file

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

View file

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

View file

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

View file

@ -26,6 +26,8 @@ import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.core.widget.ImageViewCompat import androidx.core.widget.ImageViewCompat
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.math.max import kotlin.math.max
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.ui.UISettings
@ -41,6 +43,7 @@ import org.oxycblt.auxio.util.getDrawableCompat
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@AndroidEntryPoint
class PlaybackIndicatorView class PlaybackIndicatorView
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : 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 indicatorMatrix = Matrix()
private val indicatorMatrixSrc = RectF() private val indicatorMatrixSrc = RectF()
private val indicatorMatrixDst = 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 * 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) { set(value) {
field = value field = value
(background as? MaterialShapeDrawable)?.let { bg -> (background as? MaterialShapeDrawable)?.let { bg ->
if (UISettings.from(context).roundMode) { if (uiSettings.roundMode) {
bg.setCornerSize(value) bg.setCornerSize(value)
} else { } else {
bg.setCornerSize(0f) bg.setCornerSize(0f)

View file

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

View file

@ -63,7 +63,7 @@ interface ListDiffer<T, I> {
class Async<T>(private val diffCallback: DiffUtil.ItemCallback<T>) : class Async<T>(private val diffCallback: DiffUtil.ItemCallback<T>) :
Factory<T, BasicListInstructions>() { Factory<T, BasicListInstructions>() {
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<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>) : class Blocking<T>(private val diffCallback: DiffUtil.ItemCallback<T>) :
Factory<T, BasicListInstructions>() { Factory<T, BasicListInstructions>() {
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<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) protected abstract fun replaceList(newList: List<T>, onDone: () -> Unit)
} }
private class RealAsyncListDiffer<T>( private class AsyncListDifferImpl<T>(
updateCallback: ListUpdateCallback, updateCallback: ListUpdateCallback,
diffCallback: DiffUtil.ItemCallback<T> diffCallback: DiffUtil.ItemCallback<T>
) : BasicListDiffer<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 updateCallback: ListUpdateCallback,
private val diffCallback: DiffUtil.ItemCallback<T> private val diffCallback: DiffUtil.ItemCallback<T>
) : BasicListDiffer<T>() { ) : BasicListDiffer<T>() {

View file

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

View file

@ -63,18 +63,10 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
/** Called when the [shouldBeObserving] configuration has changed. */ /** Called when the [shouldBeObserving] configuration has changed. */
fun onObservingChanged() {} 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) : 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) private val storageManager = context.getSystemServiceCompat(StorageManager::class)
override var musicDirs: MusicDirectories override var musicDirs: MusicDirectories

View file

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

View file

@ -18,12 +18,61 @@
package org.oxycblt.auxio.music.extractor package org.oxycblt.auxio.music.extractor
import android.content.Context 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.CacheDatabase
import org.oxycblt.auxio.music.cache.CachedSong import org.oxycblt.auxio.music.cache.CachedSong
import org.oxycblt.auxio.music.cache.CachedSongsDao import org.oxycblt.auxio.music.cache.CachedSongsDao
import org.oxycblt.auxio.music.model.RawSong import org.oxycblt.auxio.music.model.RawSong
import org.oxycblt.auxio.util.* 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 * A cache of music metadata obtained in prior music loading operations. Obtain an instance with
* [CacheRepository]. * [CacheRepository].
@ -41,7 +90,7 @@ interface Cache {
fun populate(rawSong: RawSong): Boolean fun populate(rawSong: RawSong): Boolean
} }
private class RealCache(cachedSongs: List<CachedSong>) : Cache { private class CacheImpl(cachedSongs: List<CachedSong>) : Cache {
private val cacheMap = buildMap { private val cacheMap = buildMap {
for (cachedSong in cachedSongs) { for (cachedSong in cachedSongs) {
put(cachedSong.mediaStoreId, cachedSong) put(cachedSong.mediaStoreId, cachedSong)
@ -69,58 +118,3 @@ private class RealCache(cachedSongs: List<CachedSong>) : Cache {
return false 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.appcompat.app.AlertDialog
import androidx.core.view.children import androidx.core.view.children
import com.google.android.material.checkbox.MaterialCheckBox 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.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
@ -33,7 +35,10 @@ import org.oxycblt.auxio.ui.ViewBindingDialogFragment
* split tags with multiple values. * split tags with multiple values.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@AndroidEntryPoint
class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() { class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
@Inject lateinit var musicSettings: MusicSettings
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =
DialogSeparatorsBinding.inflate(inflater) DialogSeparatorsBinding.inflate(inflater)
@ -42,7 +47,7 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
.setTitle(R.string.set_separators) .setTitle(R.string.set_separators)
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
.setPositiveButton(R.string.lbl_save) { _, _ -> .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 // the corresponding CheckBox for each character instead of doing an iteration
// through the separator list for each CheckBox. // through the separator list for each CheckBox.
(savedInstanceState?.getString(KEY_PENDING_SEPARATORS) (savedInstanceState?.getString(KEY_PENDING_SEPARATORS)
?: MusicSettings.from(requireContext()).multiValueSeparators) ?: musicSettings.multiValueSeparators)
.forEach { .forEach {
when (it) { when (it) {
Separators.COMMA -> binding.separatorComma.isChecked = true Separators.COMMA -> binding.separatorComma.isChecked = true

View file

@ -81,7 +81,7 @@ interface MediaStoreExtractor {
* Create a framework-backed instance. * Create a framework-backed instance.
* @param context [Context] required. * @param context [Context] required.
* @param musicSettings [MusicSettings] 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 = fun from(context: Context, musicSettings: MusicSettings): MediaStoreExtractor =
when { when {
@ -94,7 +94,7 @@ interface MediaStoreExtractor {
} }
} }
private abstract class RealMediaStoreExtractor( private abstract class BaseMediaStoreExtractor(
protected val context: Context, protected val context: Context,
private val musicSettings: MusicSettings private val musicSettings: MusicSettings
) : MediaStoreExtractor { ) : MediaStoreExtractor {
@ -352,7 +352,7 @@ private abstract class RealMediaStoreExtractor(
// speed, we only want to add redundancy on known issues, not with possible issues. // speed, we only want to add redundancy on known issues, not with possible issues.
private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSettings) : private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSettings) :
RealMediaStoreExtractor(context, musicSettings) { BaseMediaStoreExtractor(context, musicSettings) {
override val projection: Array<String> override val projection: Array<String>
get() = get() =
super.projection + super.projection +
@ -385,7 +385,7 @@ private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSet
cursor: Cursor, cursor: Cursor,
genreNamesMap: Map<Long, String>, genreNamesMap: Map<Long, String>,
storageManager: StorageManager storageManager: StorageManager
) : RealMediaStoreExtractor.Query(cursor, genreNamesMap) { ) : BaseMediaStoreExtractor.Query(cursor, genreNamesMap) {
// Set up cursor indices for later use. // Set up cursor indices for later use.
private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
private val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA) 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. * @param context [Context] required to query the media database.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@ -438,7 +438,7 @@ private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSet
private abstract class BaseApi29MediaStoreExtractor( private abstract class BaseApi29MediaStoreExtractor(
context: Context, context: Context,
musicSettings: MusicSettings musicSettings: MusicSettings
) : RealMediaStoreExtractor(context, musicSettings) { ) : BaseMediaStoreExtractor(context, musicSettings) {
override val projection: Array<String> override val projection: Array<String>
get() = get() =
super.projection + super.projection +
@ -471,7 +471,7 @@ private abstract class BaseApi29MediaStoreExtractor(
cursor: Cursor, cursor: Cursor,
genreNamesMap: Map<Long, String>, genreNamesMap: Map<Long, String>,
storageManager: StorageManager storageManager: StorageManager
) : RealMediaStoreExtractor.Query(cursor, genreNamesMap) { ) : BaseMediaStoreExtractor.Query(cursor, genreNamesMap) {
private val volumeIndex = private val volumeIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME) cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME)
private val relativePathIndex = 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 * API
* 29. * 29.
* @param context [Context] required to query the media database. * @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. * 30 onwards.
* @param context [Context] required to query the media database. * @param context [Context] required to query the media database.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)

View file

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

View file

@ -67,7 +67,7 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
private lateinit var observingNotification: ObservingNotification private lateinit var observingNotification: ObservingNotification
private lateinit var wakeLock: PowerManager.WakeLock private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var indexerContentObserver: SystemContentObserver private lateinit var indexerContentObserver: SystemContentObserver
private lateinit var settings: MusicSettings @Inject lateinit var musicSettings: MusicSettings
override fun onCreate() { override fun onCreate() {
super.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 // 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. // condition to cause us to load music before we were fully initialize.
indexerContentObserver = SystemContentObserver() indexerContentObserver = SystemContentObserver()
settings = MusicSettings.from(this) musicSettings.registerListener(this)
settings.registerListener(this)
indexer.registerController(this) indexer.registerController(this)
// An indeterminate indexer and a missing library implies we are extremely early // An indeterminate indexer and a missing library implies we are extremely early
// in app initialization so start loading music. // 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 // Then cancel the listener-dependent components to ensure that stray reloading
// events will not occur. // events will not occur.
indexerContentObserver.release() indexerContentObserver.release()
settings.unregisterListener(this) musicSettings.unregisterListener(this)
indexer.unregisterController(this) indexer.unregisterController(this)
// Then cancel any remaining music loading jobs. // Then cancel any remaining music loading jobs.
serviceJob.cancel() serviceJob.cancel()
@ -197,7 +196,7 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
* currently monitoring the music library for changes. * currently monitoring the music library for changes.
*/ */
private fun updateIdleSession() { private fun updateIdleSession() {
if (settings.shouldBeObserving) { if (musicSettings.shouldBeObserving) {
// There are a few reasons why we stay in the foreground with automatic rescanning: // 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 // 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 // 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() { override fun run() {
// Check here if we should even start a reindex. This is much less bug-prone than // 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. // registering and de-registering this component as this setting changes.
if (settings.shouldBeObserving) { if (musicSettings.shouldBeObserving) {
onStartIndexing(true) onStartIndexing(true)
} }
} }

View file

@ -23,17 +23,11 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.oxycblt.auxio.playback.persist.PersistenceRepository
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class PlaybackModule { class PlaybackModule {
@Provides fun playbackStateManager() = PlaybackStateManager.get() @Provides fun stateManager() = PlaybackStateManager.get()
@Provides fun settings(@ApplicationContext context: Context) = PlaybackSettings.from(context) @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. * Get a framework-backed implementation.
* @param context [Context] required. * @param context [Context] required.
*/ */
fun from(context: Context): PlaybackSettings = RealPlaybackSettings(context) fun from(context: Context): PlaybackSettings = PlaybackSettingsImpl(context)
} }
} }
private class RealPlaybackSettings(context: Context) : class PlaybackSettingsImpl(context: Context) :
Settings.Real<PlaybackSettings.Listener>(context), PlaybackSettings { Settings.Impl<PlaybackSettings.Listener>(context), PlaybackSettings {
override val inListPlaybackMode: MusicMode override val inListPlaybackMode: MusicMode
get() = get() =
MusicMode.fromIntCode( 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 package org.oxycblt.auxio.playback.persist
import android.content.Context 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.MusicParent
import org.oxycblt.auxio.music.model.Library import org.oxycblt.auxio.music.model.Library
import org.oxycblt.auxio.playback.queue.Queue import org.oxycblt.auxio.playback.queue.Queue
@ -47,11 +49,13 @@ interface PersistenceRepository {
* Get a framework-backed implementation. * Get a framework-backed implementation.
* @param context [Context] required. * @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 database: PersistenceDatabase by lazy { PersistenceDatabase.getInstance(context) }
private val playbackStateDao: PlaybackStateDao by lazy { database.playbackStateDao() } private val playbackStateDao: PlaybackStateDao by lazy { database.playbackStateDao() }
private val queueDao: QueueDao by lazy { database.queueDao() } private val queueDao: QueueDao by lazy { database.queueDao() }

View file

@ -287,7 +287,7 @@ interface PlaybackStateManager {
} }
synchronized(this) { synchronized(this) {
val newInstance = RealPlaybackStateManager() val newInstance = PlaybackStateManagerImpl()
INSTANCE = newInstance INSTANCE = newInstance
return newInstance return newInstance
} }
@ -295,7 +295,7 @@ interface PlaybackStateManager {
} }
} }
private class RealPlaybackStateManager : PlaybackStateManager { private class PlaybackStateManagerImpl : 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

@ -18,7 +18,9 @@
package org.oxycblt.auxio.search package org.oxycblt.auxio.search
import android.content.Context import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import java.text.Normalizer import java.text.Normalizer
import javax.inject.Inject
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -51,17 +53,10 @@ interface SearchEngine {
val artists: List<Artist>?, val artists: List<Artist>?,
val genres: List<Genre>? 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) = override suspend fun search(items: SearchEngine.Items, query: String) =
SearchEngine.Items( SearchEngine.Items(
songs = items.songs?.searchListImpl(query) { q, song -> song.path.name.contains(q) }, songs = items.songs?.searchListImpl(query) { q, song -> song.path.name.contains(q) },

View file

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

View file

@ -19,6 +19,8 @@ package org.oxycblt.auxio.search
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.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
@ -30,18 +32,10 @@ import org.oxycblt.auxio.settings.Settings
interface SearchSettings : Settings<Nothing> { interface SearchSettings : Settings<Nothing> {
/** The type of Music the search view is currently filtering to. */ /** The type of Music the search view is currently filtering to. */
var searchFilterMode: MusicMode? 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) : class SearchSettingsImpl @Inject constructor(@ApplicationContext context: Context) :
Settings.Real<Nothing>(context), SearchSettings { Settings.Impl<Nothing>(context), SearchSettings {
override var searchFilterMode: MusicMode? override var searchFilterMode: MusicMode?
get() = get() =
MusicMode.fromIntCode( MusicMode.fromIntCode(

View file

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

View file

@ -20,6 +20,8 @@ package org.oxycblt.auxio.settings.categories
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.Preference import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.settings.BasePreferenceFragment import org.oxycblt.auxio.settings.BasePreferenceFragment
import org.oxycblt.auxio.settings.ui.WrappedDialogPreference import org.oxycblt.auxio.settings.ui.WrappedDialogPreference
@ -30,7 +32,10 @@ import org.oxycblt.auxio.util.isNight
* Display preferences. * Display preferences.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@AndroidEntryPoint
class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) { class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) {
@Inject lateinit var uiSettings: UISettings
override fun onOpenDialogPreference(preference: WrappedDialogPreference) { override fun onOpenDialogPreference(preference: WrappedDialogPreference) {
if (preference.key == getString(R.string.set_key_accent)) { if (preference.key == getString(R.string.set_key_accent)) {
findNavController().navigate(UIPreferenceFragmentDirections.goToAccentDialog()) findNavController().navigate(UIPreferenceFragmentDirections.goToAccentDialog())
@ -47,7 +52,7 @@ class UIPreferenceFragment : BasePreferenceFragment(R.xml.preferences_ui) {
} }
} }
getString(R.string.set_key_accent) -> { 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) -> { getString(R.string.set_key_black_theme) -> {
preference.onPreferenceChangeListener = 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 android.os.Build
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.accent.Accent import org.oxycblt.auxio.ui.accent.Accent
@ -50,12 +52,12 @@ interface UISettings : Settings<UISettings.Listener> {
* Get a framework-backed implementation. * Get a framework-backed implementation.
* @param context [Context] required. * @param context [Context] required.
*/ */
fun from(context: Context): UISettings = RealUISettings(context) fun from(context: Context): UISettings = UISettingsImpl(context)
} }
} }
private class RealUISettings(context: Context) : class UISettingsImpl @Inject constructor(@ApplicationContext context: Context) :
Settings.Real<UISettings.Listener>(context), UISettings { Settings.Impl<UISettings.Listener>(context), UISettings {
override val theme: Int override val theme: Int
get() = get() =
sharedPreferences.getInt( sharedPreferences.getInt(

View file

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

View file

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

View file

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