image: use di w/coil

Use dependency injection with Coil.

This allows me to use the coil-base artifact which should remove a bit
of superfluous dexcode, assuming dagger uses less. It probably doesn't.
This commit is contained in:
Alexander Capehart 2023-02-12 19:07:00 -07:00
parent 63e5a7ee69
commit dd2017c510
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
13 changed files with 112 additions and 81 deletions

View file

@ -113,7 +113,7 @@ dependencies {
implementation fileTree(dir: "libs", include: ["extension-*.aar"])
// Image loading
implementation "io.coil-kt:coil:2.1.0"
implementation "io.coil-kt:coil-base:2.1.0"
// Material
// Locked below 1.7.0-alpha03 to avoid the same ripple bug

View file

@ -22,17 +22,9 @@ import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
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
import org.oxycblt.auxio.image.extractor.ErrorCrossfadeTransitionFactory
import org.oxycblt.auxio.image.extractor.GenreImageFetcher
import org.oxycblt.auxio.image.extractor.MusicKeyer
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.ui.UISettings
@ -41,7 +33,7 @@ import org.oxycblt.auxio.ui.UISettings
* @author Alexander Capehart (OxygenCobalt)
*/
@HiltAndroidApp
class Auxio : Application(), ImageLoaderFactory {
class Auxio : Application() {
@Inject lateinit var imageSettings: ImageSettings
@Inject lateinit var playbackSettings: PlaybackSettings
@Inject lateinit var uiSettings: UISettings
@ -68,22 +60,6 @@ class Auxio : Application(), ImageLoaderFactory {
.build()))
}
override fun newImageLoader() =
ImageLoader.Builder(applicationContext)
.components {
// Add fetchers for Music components to make them usable with ImageRequest
add(MusicKeyer())
add(AlbumCoverFetcher.SongFactory())
add(AlbumCoverFetcher.AlbumFactory())
add(ArtistImageFetcher.Factory())
add(GenreImageFetcher.Factory())
}
// Use our own crossfade with error drawable support
.transitionFactory(ErrorCrossfadeTransitionFactory())
// Not downloading anything, so no disk-caching
.diskCachePolicy(CachePolicy.DISABLED)
.build()
companion object {
/** The [Intent] name for the "Shuffle All" shortcut. */
const val INTENT_KEY_SHORTCUT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE_ALL"

View file

@ -20,10 +20,12 @@ package org.oxycblt.auxio.image
import android.content.Context
import android.graphics.Bitmap
import androidx.core.graphics.drawable.toBitmap
import coil.imageLoader
import coil.ImageLoader
import coil.request.Disposable
import coil.request.ImageRequest
import coil.size.Size
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.auxio.image.extractor.SquareFrameTransform
import org.oxycblt.auxio.music.Song
@ -38,7 +40,12 @@ import org.oxycblt.auxio.music.Song
* @param context [Context] required to load images.
* @author Alexander Capehart (OxygenCobalt)
*/
class BitmapProvider(private val context: Context) {
class BitmapProvider
@Inject
constructor(
@ApplicationContext private val context: Context,
private val imageLoader: ImageLoader
) {
/**
* An extension of [Disposable] with an additional [Target] to deliver the final [Bitmap] to.
*/
@ -94,7 +101,7 @@ class BitmapProvider(private val context: Context) {
onSuccess = {
synchronized(this) {
if (currentHandle == handle) {
// Has not been superceded by a new request, can deliver
// Has not been superseded by a new request, can deliver
// this result.
target.onCompleted(it.toBitmap())
}
@ -103,13 +110,13 @@ class BitmapProvider(private val context: Context) {
onError = {
synchronized(this) {
if (currentHandle == handle) {
// Has not been superceded by a new request, can deliver
// Has not been superseded by a new request, can deliver
// this result.
target.onCompleted(null)
}
}
})
currentRequest = Request(context.imageLoader.enqueue(imageRequest.build()), target)
currentRequest = Request(imageLoader.enqueue(imageRequest.build()), target)
}
/** Release this instance, cancelling any currently running operations. */

View file

@ -17,13 +17,52 @@
package org.oxycblt.auxio.image
import android.content.Context
import coil.ImageLoader
import coil.request.CachePolicy
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import org.oxycblt.auxio.image.extractor.AlbumCoverFetcher
import org.oxycblt.auxio.image.extractor.ArtistImageFetcher
import org.oxycblt.auxio.image.extractor.ErrorCrossfadeTransitionFactory
import org.oxycblt.auxio.image.extractor.GenreImageFetcher
import org.oxycblt.auxio.image.extractor.MusicKeyer
@Module
@InstallIn(SingletonComponent::class)
interface ImageModule {
@Binds fun settings(imageSettings: ImageSettingsImpl): ImageSettings
}
@Module
@InstallIn(SingletonComponent::class)
class CoilModule {
@Singleton
@Provides
fun imageLoader(
@ApplicationContext context: Context,
songFactory: AlbumCoverFetcher.SongFactory,
albumFactory: AlbumCoverFetcher.AlbumFactory,
artistFactory: ArtistImageFetcher.Factory,
genreFactory: GenreImageFetcher.Factory
) =
ImageLoader.Builder(context)
.components {
// Add fetchers for Music components to make them usable with ImageRequest
add(MusicKeyer())
add(songFactory)
add(albumFactory)
add(artistFactory)
add(genreFactory)
}
// Use our own crossfade with error drawable support
.transitionFactory(ErrorCrossfadeTransitionFactory())
// Not downloading anything, so no disk-caching
.diskCachePolicy(CachePolicy.DISABLED)
.build()
}

View file

@ -37,14 +37,6 @@ interface ImageSettings : Settings<ImageSettings.Listener> {
/** Called when [coverMode] changes. */
fun onCoverModeChanged() {}
}
companion object {
/**
* Get a framework-backed implementation.
* @param context [Context] required.
*/
fun from(context: Context): ImageSettings = ImageSettingsImpl(context)
}
}
class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context) :

View file

@ -29,8 +29,9 @@ import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.DrawableCompat
import coil.dispose
import coil.load
import coil.ImageLoader
import coil.request.ImageRequest
import coil.util.CoilUtils
import com.google.android.material.shape.MaterialShapeDrawable
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -60,6 +61,7 @@ class StyledImageView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
AppCompatImageView(context, attrs, defStyleAttr) {
@Inject lateinit var imageLoader: ImageLoader
@Inject lateinit var uiSettings: UISettings
init {
@ -125,13 +127,16 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
* field for the name of the [Music].
*/
private fun bindImpl(music: Music, @DrawableRes errorRes: Int, @StringRes descRes: Int) {
val request =
ImageRequest.Builder(context)
.data(music)
.error(StyledDrawable(context, context.getDrawableCompat(errorRes)))
.transformations(SquareFrameTransform.INSTANCE)
.target(this)
.build()
// Dispose of any previous image request and load a new image.
dispose()
load(music) {
error(StyledDrawable(context, context.getDrawableCompat(errorRes)))
transformations(SquareFrameTransform.INSTANCE)
}
CoilUtils.dispose(this)
imageLoader.enqueue(request)
// Update the content description to the specified resource.
contentDescription = context.getString(descRes, music.resolveName(context))
}

View file

@ -27,9 +27,11 @@ import coil.fetch.SourceResult
import coil.key.Keyer
import coil.request.Options
import coil.size.Size
import javax.inject.Inject
import kotlin.math.min
import okio.buffer
import okio.source
import org.oxycblt.auxio.image.ImageSettings
import org.oxycblt.auxio.list.Sort
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
@ -57,9 +59,13 @@ class MusicKeyer : Keyer<Music> {
* @author Alexander Capehart (OxygenCobalt)
*/
class AlbumCoverFetcher
private constructor(private val context: Context, private val album: Album) : Fetcher {
private constructor(
private val context: Context,
private val imageSettings: ImageSettings,
private val album: Album
) : Fetcher {
override suspend fun fetch(): FetchResult? =
Covers.fetch(context, album)?.run {
Covers.fetch(context, imageSettings, album)?.run {
SourceResult(
source = ImageSource(source().buffer(), context),
mimeType = null,
@ -67,15 +73,17 @@ private constructor(private val context: Context, private val album: Album) : Fe
}
/** A [Fetcher.Factory] implementation that works with [Song]s. */
class SongFactory : Fetcher.Factory<Song> {
class SongFactory @Inject constructor(private val imageSettings: ImageSettings) :
Fetcher.Factory<Song> {
override fun create(data: Song, options: Options, imageLoader: ImageLoader) =
AlbumCoverFetcher(options.context, data.album)
AlbumCoverFetcher(options.context, imageSettings, data.album)
}
/** A [Fetcher.Factory] implementation that works with [Album]s. */
class AlbumFactory : Fetcher.Factory<Album> {
class AlbumFactory @Inject constructor(private val imageSettings: ImageSettings) :
Fetcher.Factory<Album> {
override fun create(data: Album, options: Options, imageLoader: ImageLoader) =
AlbumCoverFetcher(options.context, data)
AlbumCoverFetcher(options.context, imageSettings, data)
}
}
@ -86,20 +94,23 @@ private constructor(private val context: Context, private val album: Album) : Fe
class ArtistImageFetcher
private constructor(
private val context: Context,
private val imageSettings: ImageSettings,
private val size: Size,
private val artist: Artist
) : Fetcher {
override suspend fun fetch(): FetchResult? {
// Pick the "most prominent" albums (i.e albums with the most songs) to show in the image.
val albums = Sort(Sort.Mode.ByCount, Sort.Direction.DESCENDING).albums(artist.albums)
val results = albums.mapAtMostNotNull(4) { album -> Covers.fetch(context, album) }
val results =
albums.mapAtMostNotNull(4) { album -> Covers.fetch(context, imageSettings, album) }
return Images.createMosaic(context, results, size)
}
/** [Fetcher.Factory] implementation. */
class Factory : Fetcher.Factory<Artist> {
class Factory @Inject constructor(private val imageSettings: ImageSettings) :
Fetcher.Factory<Artist> {
override fun create(data: Artist, options: Options, imageLoader: ImageLoader) =
ArtistImageFetcher(options.context, options.size, data)
ArtistImageFetcher(options.context, imageSettings, options.size, data)
}
}
@ -110,18 +121,20 @@ private constructor(
class GenreImageFetcher
private constructor(
private val context: Context,
private val imageSettings: ImageSettings,
private val size: Size,
private val genre: Genre
) : Fetcher {
override suspend fun fetch(): FetchResult? {
val results = genre.albums.mapAtMostNotNull(4) { Covers.fetch(context, it) }
val results = genre.albums.mapAtMostNotNull(4) { Covers.fetch(context, imageSettings, it) }
return Images.createMosaic(context, results, size)
}
/** [Fetcher.Factory] implementation. */
class Factory : Fetcher.Factory<Genre> {
class Factory @Inject constructor(private val imageSettings: ImageSettings) :
Fetcher.Factory<Genre> {
override fun create(data: Genre, options: Options, imageLoader: ImageLoader) =
GenreImageFetcher(options.context, options.size, data)
GenreImageFetcher(options.context, imageSettings, options.size, data)
}
}

View file

@ -42,13 +42,14 @@ object Covers {
/**
* Fetch an album cover, respecting the current cover configuration.
* @param context [Context] required to load the image.
* @param imageSettings [ImageSettings] required to obtain configuration information.
* @param album [Album] to load the cover from.
* @return An [InputStream] of image data if the cover loading was successful, null if the cover
* loading failed or should not occur.
*/
suspend fun fetch(context: Context, album: Album): InputStream? {
suspend fun fetch(context: Context, imageSettings: ImageSettings, album: Album): InputStream? {
return try {
when (ImageSettings.from(context).coverMode) {
when (imageSettings.coverMode) {
CoverMode.OFF -> null
CoverMode.MEDIA_STORE -> fetchMediaStoreCovers(context, album)
CoverMode.QUALITY -> fetchQualityCovers(context, album)

View file

@ -25,7 +25,7 @@ import android.os.IBinder
import android.os.Looper
import android.os.PowerManager
import android.provider.MediaStore
import coil.imageLoader
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@ -68,6 +68,7 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var indexerContentObserver: SystemContentObserver
@Inject lateinit var musicSettings: MusicSettings
@Inject lateinit var imageLoader: ImageLoader
override fun onCreate() {
super.onCreate()

View file

@ -52,8 +52,9 @@ class MediaSessionComponent
@Inject
constructor(
@ApplicationContext private val context: Context,
private val bitmapProvider: BitmapProvider,
private val playbackManager: PlaybackStateManager,
private val playbackSettings: PlaybackSettings
private val playbackSettings: PlaybackSettings,
) :
MediaSessionCompat.Callback(),
PlaybackStateManager.Listener,
@ -66,7 +67,6 @@ constructor(
}
private val notification = NotificationComponent(context, mediaSession.sessionToken)
private val provider = BitmapProvider(context)
private var listener: Listener? = null
@ -98,7 +98,7 @@ constructor(
*/
fun release() {
listener = null
provider.release()
bitmapProvider.release()
playbackSettings.unregisterListener(this)
playbackManager.removeListener(this)
mediaSession.apply {
@ -148,7 +148,7 @@ constructor(
override fun onStateChanged(state: InternalPlayer.State) {
invalidateSessionState()
notification.updatePlaying(playbackManager.playerState.isPlaying)
if (!provider.isBusy) {
if (!bitmapProvider.isBusy) {
listener?.onPostNotification(notification)
}
}
@ -321,7 +321,7 @@ constructor(
// We are normally supposed to use URIs for album art, but that removes some of the
// nice things we can do like square cropping or high quality covers. Instead,
// we load a full-size bitmap into the media session and take the performance hit.
provider.load(
bitmapProvider.load(
song,
object : BitmapProvider.Target {
override fun onCompleted(bitmap: Bitmap?) {
@ -416,7 +416,7 @@ constructor(
else -> notification.updateRepeatMode(playbackManager.repeatMode)
}
if (!provider.isBusy) {
if (!bitmapProvider.isBusy) {
listener?.onPostNotification(notification)
}
}

View file

@ -19,7 +19,9 @@ package org.oxycblt.auxio.settings.categories
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import coil.Coil
import coil.ImageLoader
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
@ -28,7 +30,10 @@ import org.oxycblt.auxio.settings.ui.WrappedDialogPreference
* "Content" settings.
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music) {
@Inject lateinit var imageLoader: ImageLoader
override fun onOpenDialogPreference(preference: WrappedDialogPreference) {
if (preference.key == getString(R.string.set_key_separators)) {
findNavController().navigate(MusicPreferenceFragmentDirections.goToSeparatorsDialog())
@ -39,7 +44,7 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music)
if (preference.key == getString(R.string.set_key_cover_mode)) {
preference.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ ->
Coil.imageLoader(requireContext()).memoryCache?.clear()
imageLoader.memoryCache?.clear()
true
}
}

View file

@ -46,14 +46,6 @@ interface UISettings : Settings<UISettings.Listener> {
/** Called when [roundMode] changes. */
fun onRoundModeChanged()
}
companion object {
/**
* Get a framework-backed implementation.
* @param context [Context] required.
*/
fun from(context: Context): UISettings = UISettingsImpl(context)
}
}
class UISettingsImpl @Inject constructor(@ApplicationContext context: Context) :

View file

@ -48,11 +48,11 @@ class WidgetComponent
constructor(
@ApplicationContext private val context: Context,
private val imageSettings: ImageSettings,
private val bitmapProvider: BitmapProvider,
private val playbackManager: PlaybackStateManager,
private val uiSettings: UISettings
) : PlaybackStateManager.Listener, UISettings.Listener, ImageSettings.Listener {
private val widgetProvider = WidgetProvider()
private val provider = BitmapProvider(context)
init {
playbackManager.addListener(this)
@ -74,7 +74,7 @@ constructor(
val repeatMode = playbackManager.repeatMode
val isShuffled = playbackManager.queue.isShuffled
provider.load(
bitmapProvider.load(
song,
object : BitmapProvider.Target {
override fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder {
@ -112,7 +112,7 @@ constructor(
/** Release this instance, preventing any further events from updating the widget instances. */
fun release() {
provider.release()
bitmapProvider.release()
imageSettings.unregisterListener(this)
playbackManager.removeListener(this)
uiSettings.unregisterListener(this)