diff --git a/app/build.gradle b/app/build.gradle index cfc8f743b..cd4a767d1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/Auxio.kt b/app/src/main/java/org/oxycblt/auxio/Auxio.kt index 913f7022a..0c652e922 100644 --- a/app/src/main/java/org/oxycblt/auxio/Auxio.kt +++ b/app/src/main/java/org/oxycblt/auxio/Auxio.kt @@ -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" diff --git a/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt index 8bb6d83a6..70a4a912b 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt @@ -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. */ diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt index cf8b04047..1520abf1e 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageModule.kt @@ -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() +} diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt index 14b49c627..866cdca2f 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt @@ -37,14 +37,6 @@ interface ImageSettings : Settings { /** 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) : diff --git a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt index ed78c47e5..61e197263 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt @@ -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)) } diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt index edbcc69bc..64c9a948a 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt @@ -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 { * @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 { + class SongFactory @Inject constructor(private val imageSettings: ImageSettings) : + Fetcher.Factory { 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 { + class AlbumFactory @Inject constructor(private val imageSettings: ImageSettings) : + Fetcher.Factory { 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 { + class Factory @Inject constructor(private val imageSettings: ImageSettings) : + Fetcher.Factory { 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 { + class Factory @Inject constructor(private val imageSettings: ImageSettings) : + Fetcher.Factory { override fun create(data: Genre, options: Options, imageLoader: ImageLoader) = - GenreImageFetcher(options.context, options.size, data) + GenreImageFetcher(options.context, imageSettings, options.size, data) } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt index b26141f7b..7d1aeb9d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index f7553dfda..0a1b80254 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -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() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index 7ec25f7aa..73ee968e3 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -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) } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt index b38177d1e..1ee5e05e6 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/categories/MusicPreferenceFragment.kt @@ -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 } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt index 34c07d2af..10f8bbc52 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt @@ -46,14 +46,6 @@ interface UISettings : Settings { /** 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) : diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index 65955fec4..05de81070 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -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)