Merge branch 'StrictMode' into 'develop'

Fix a bunch of StrictMode warnings by executing methods on the right threads

See merge request ultrasonic/ultrasonic!958
This commit is contained in:
birdbird 2023-04-14 08:01:54 +00:00
commit c7edfbcae6
10 changed files with 175 additions and 173 deletions

View File

@ -41,6 +41,7 @@ import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.model.SearchListModel
import org.moire.ultrasonic.service.DownloadService
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler
@ -203,7 +204,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
private fun downloadBackground(save: Boolean, songs: List<Track?>) {
val onValid = Runnable {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.downloadBackground(songs, save)
DownloadService.download(songs.filterNotNull(), save)
}
onValid.run()
}
@ -437,7 +438,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
songs.size
)
)
mediaPlayerController.unpin(songs)
DownloadService.unpin(songs)
}
R.id.song_menu_share -> {
songs.add(item)

View File

@ -40,6 +40,7 @@ import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.model.TrackCollectionModel
import org.moire.ultrasonic.service.DownloadService
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign
@ -429,7 +430,7 @@ open class TrackCollectionFragment(
) {
val onValid = Runnable {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.downloadBackground(songs, save)
DownloadService.download(songs.filterNotNull(), save)
if (save) {
Util.toast(
@ -458,7 +459,7 @@ open class TrackCollectionFragment(
)
)
mediaPlayerController.delete(songs)
DownloadService.delete(songs)
}
internal fun unpin(songs: List<Track> = getSelectedSongs()) {
@ -468,7 +469,7 @@ open class TrackCollectionFragment(
R.plurals.select_album_n_songs_unpinned, songs.size, songs.size
)
)
mediaPlayerController.unpin(songs)
DownloadService.unpin(songs)
}
override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = {

View File

@ -19,11 +19,12 @@ import java.io.IOException
import java.util.concurrent.Executors
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
@SuppressLint("UnsafeOptInUsageError")
class ArtworkBitmapLoader : BitmapLoader, KoinComponent {
private val imageLoader: ImageLoader by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject()
private val executorService: ListeningExecutorService by lazy {
MoreExecutors.listeningDecorator(
@ -55,6 +56,6 @@ class ArtworkBitmapLoader : BitmapLoader, KoinComponent {
val parts = uri.path?.trim('/')?.split('|')
require(parts!!.count() == 2) { "Invalid bitmap Uri" }
return imageLoader.getImage(parts[0], parts[1], false, 0)
return imageLoaderProvider.getImageLoader().getImage(parts[0], parts[1], false, 0)
}
}

View File

@ -18,6 +18,9 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.throwOnFailure
@ -36,7 +39,7 @@ class ImageLoader(
context: Context,
apiClient: SubsonicAPIClient,
private val config: ImageLoaderConfig,
) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
) : CoroutineScope by CoroutineScope(Dispatchers.Main + SupervisorJob()) {
private val cacheInProgress: ConcurrentHashMap<String, CountDownLatch> = ConcurrentHashMap()
// Shortcut
@ -126,10 +129,13 @@ class ImageLoader(
large: Boolean,
size: Int,
defaultResourceId: Int = R.drawable.unknown_album
) {
) = launch {
val id = entry?.coverArt
// TODO getAlbumArtKey() accesses the disk from the UI thread..
val key = FileUtil.getAlbumArtKey(entry, large)
val key: String?
withContext(Dispatchers.IO) {
key = FileUtil.getAlbumArtKey(entry, large)
}
loadImage(view, id, key, large, size, defaultResourceId)
}
@ -194,48 +200,49 @@ class ImageLoader(
cacheCoverArt(track.coverArt!!, FileUtil.getAlbumArtFile(track))
}
fun cacheCoverArt(id: String, file: String) {
if (id.isBlank()) return
// Return if have a cache hit
if (File(file).exists()) return
fun cacheCoverArt(id: String, file: String) = launch {
if (id.isBlank()) return@launch
// If another thread has started caching, wait until it finishes
val latch = cacheInProgress.putIfAbsent(file, CountDownLatch(1))
if (latch != null) {
latch.await()
return
}
withContext(Dispatchers.IO) {
// Return if have a cache hit
if (File(file).exists()) return@withContext
// If another coroutine has started caching, abort
if (cacheInProgress[file] != null) return@withContext
try {
// Always download the large size..
val size = config.largeSize
File(file).createNewFile()
// Query the API
Timber.d("Loading cover art for: %s", id)
val response = API.getCoverArt(id, size.toLong()).execute().toStreamResponse()
response.throwOnFailure()
// Check for failure
if (response.stream == null) return
// Write Response stream to file
var inputStream: InputStream? = null
try {
inputStream = response.stream
val bytes = inputStream!!.readBytes()
var outputStream: OutputStream? = null
val response = API.getCoverArt(id, size.toLong()).execute().toStreamResponse()
response.throwOnFailure()
// Check for failure
if (response.stream == null) return@withContext
// Write Response stream to file
var inputStream: InputStream? = null
try {
outputStream = FileOutputStream(file)
outputStream.write(bytes)
inputStream = response.stream
val bytes = inputStream!!.readBytes()
var outputStream: OutputStream? = null
try {
outputStream = FileOutputStream(file)
outputStream.write(bytes)
} finally {
outputStream.safeClose()
}
} finally {
outputStream.safeClose()
inputStream.safeClose()
}
} finally {
inputStream.safeClose()
cacheInProgress.remove(file)?.countDown()
}
} finally {
cacheInProgress.remove(file)?.countDown()
}
}

View File

@ -445,6 +445,9 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr
private fun playFromSearch(
query: String,
): ListenableFuture<List<MediaItem>> {
Timber.w("App state: %s", UApp.instance != null)
Timber.i("AutoMediaBrowserService onSearch query: %s", query)
val mediaItems: MutableList<MediaItem> = ArrayList()

View File

@ -27,6 +27,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R
@ -34,7 +35,6 @@ import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.DownloadState.Companion.isFinalState
import org.moire.ultrasonic.util.CacheCleaner
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.FileUtil.getCompleteFile
import org.moire.ultrasonic.util.FileUtil.getPartialFile
import org.moire.ultrasonic.util.FileUtil.getPinnedFile
@ -77,8 +77,7 @@ class DownloadService : Service(), KoinComponent {
// Create Coroutine lifecycle scope. We use a SupervisorJob(), otherwise the failure of one
// would mean the failure of all jobs!
val supervisor = SupervisorJob()
scope = CoroutineScope(Dispatchers.IO + supervisor)
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
val notificationManagerCompat = NotificationManagerCompat.from(this)
@ -147,7 +146,7 @@ class DownloadService : Service(), KoinComponent {
val downloadTask = DownloadTask(track, scope!!, ::downloadStateChangedCallback)
activeDownloads[track.id] = downloadTask
FileUtil.createDirectoryForParent(track.pinnedFile)
downloadTask.start()
listChanged = true
}
@ -200,7 +199,7 @@ class DownloadService : Service(), KoinComponent {
private fun updateLiveData() {
val temp: MutableList<Track> = ArrayList()
temp.addAll(activeDownloads.values.map { it.track.track })
temp.addAll(activeDownloads.values.map { it.downloadTrack.track })
temp.addAll(downloadQueue.map { x -> x.track })
observableDownloads.postValue(temp.distinct().sorted())
}
@ -257,7 +256,7 @@ class DownloadService : Service(), KoinComponent {
return notificationBuilder.build()
}
@Suppress("MagicNumber", "NestedBlockDepth")
@Suppress("MagicNumber", "NestedBlockDepth", "TooManyFunctions")
companion object {
private var startFuture: SettableFuture<DownloadService>? = null
@ -278,57 +277,60 @@ class DownloadService : Service(), KoinComponent {
save: Boolean,
isHighPriority: Boolean = false
) {
// First handle and filter out those tracks that are already completed
var filteredTracks: List<Track>
if (save) {
tracks.filter { Storage.isPathExists(it.getCompleteFile()) }.forEach { track ->
Storage.getFromPath(track.getCompleteFile())?.let {
Storage.renameOrDeleteIfAlreadyExists(it, track.getPinnedFile())
postState(track, DownloadState.PINNED)
CoroutineScope(Dispatchers.IO).launch {
// First handle and filter out those tracks that are already completed
var filteredTracks: List<Track>
if (save) {
tracks.filter { Storage.isPathExists(it.getCompleteFile()) }.forEach { track ->
Storage.getFromPath(track.getCompleteFile())?.let {
Storage.renameOrDeleteIfAlreadyExists(it, track.getPinnedFile())
postState(track, DownloadState.PINNED)
}
}
}
filteredTracks = tracks.filter { !Storage.isPathExists(it.getPinnedFile()) }
} else {
tracks.filter { Storage.isPathExists(it.getPinnedFile()) }.forEach { track ->
Storage.getFromPath(track.getPinnedFile())?.let {
Storage.renameOrDeleteIfAlreadyExists(it, track.getCompleteFile())
postState(track, DownloadState.DONE)
filteredTracks = tracks.filter { !Storage.isPathExists(it.getPinnedFile()) }
} else {
tracks.filter { Storage.isPathExists(it.getPinnedFile()) }.forEach { track ->
Storage.getFromPath(track.getPinnedFile())?.let {
Storage.renameOrDeleteIfAlreadyExists(it, track.getCompleteFile())
postState(track, DownloadState.DONE)
}
}
}
filteredTracks = tracks.filter { !Storage.isPathExists(it.getCompleteFile()) }
}
// Update Pinned flag of items in progress
downloadQueue.filter { item -> tracks.any { it.id == item.id } }
.forEach { it.pinned = save }
tracks.forEach {
activeDownloads[it.id]?.track?.pinned = save
}
tracks.forEach {
failedList[it.id]?.pinned = save
}
filteredTracks = filteredTracks.filter {
!downloadQueue.any { i -> i.id == it.id } && !activeDownloads.containsKey(it.id)
}
// The remainder tracks should be added to the download queue
// By using the counter we ensure that the songs are added in the correct order
var priority = 0
val tracksToDownload =
filteredTracks.map {
DownloadableTrack(
it,
save,
0,
if (isHighPriority) priority++ else backgroundPriorityCounter++
)
filteredTracks = tracks.filter { !Storage.isPathExists(it.getCompleteFile()) }
}
if (tracksToDownload.isNotEmpty()) {
downloadQueue.addAll(tracksToDownload)
tracksToDownload.forEach { postState(it.track, DownloadState.QUEUED) }
processNextTracksOnService()
// Update Pinned flag of items in progress
downloadQueue.filter { item -> tracks.any { it.id == item.id } }
.forEach { it.pinned = save }
tracks.forEach {
activeDownloads[it.id]?.downloadTrack?.pinned = save
}
tracks.forEach {
failedList[it.id]?.pinned = save
}
filteredTracks = filteredTracks.filter {
!downloadQueue.any { i -> i.id == it.id } && !activeDownloads.containsKey(it.id)
}
// The remainder tracks should be added to the download queue
// By using the counter we ensure that the songs are added in the correct order
var priority = 0
val tracksToDownload =
filteredTracks.map {
DownloadableTrack(
it,
save,
0,
if (isHighPriority) priority++ else backgroundPriorityCounter++
)
}
if (tracksToDownload.isNotEmpty()) {
downloadQueue.addAll(tracksToDownload)
tracksToDownload.forEach { postState(it.track, DownloadState.QUEUED) }
processNextTracksOnService()
}
}
}
@ -340,23 +342,34 @@ class DownloadService : Service(), KoinComponent {
}
fun delete(track: Track) {
CoroutineScope(Dispatchers.IO).launch {
downloadQueue.get(track.id)?.let { downloadQueue.remove(it) }
failedList[track.id]?.let { downloadQueue.remove(it) }
cancelDownload(track)
downloadQueue.get(track.id)?.let { downloadQueue.remove(it) }
failedList[track.id]?.let { downloadQueue.remove(it) }
cancelDownload(track)
Storage.delete(track.getPartialFile())
Storage.delete(track.getCompleteFile())
Storage.delete(track.getPinnedFile())
postState(track, DownloadState.IDLE)
CacheCleaner().cleanDatabaseSelective(track)
Util.scanMedia(track.getPinnedFile())
}
}
Storage.delete(track.getPartialFile())
Storage.delete(track.getCompleteFile())
Storage.delete(track.getPinnedFile())
postState(track, DownloadState.IDLE)
CacheCleaner().cleanDatabaseSelective(track)
Util.scanMedia(track.getPinnedFile())
@Synchronized
fun unpin(tracks: List<Track>) {
tracks.forEach(::unpin)
}
@Synchronized
fun delete(tracks: List<Track>) {
tracks.forEach(::delete)
}
fun unpin(track: Track) {
// Update Pinned flag of items in progress
downloadQueue.get(track.id)?.pinned = false
activeDownloads[track.id]?.track?.pinned = false
activeDownloads[track.id]?.downloadTrack?.pinned = false
failedList[track.id]?.pinned = false
val pinnedFile = track.getPinnedFile()
@ -376,7 +389,7 @@ class DownloadService : Service(), KoinComponent {
if (activeDownloads.contains(track.id)) return DownloadState.QUEUED
if (downloadQueue.contains(track.id)) return DownloadState.QUEUED
val downloadableTrack = activeDownloads[track.id]?.track
val downloadableTrack = activeDownloads[track.id]?.downloadTrack
if (downloadableTrack != null) {
if (downloadableTrack.tryCount > 0) return DownloadState.RETRYING
return DownloadState.DOWNLOADING

View File

@ -36,7 +36,7 @@ private const val MAX_RETRIES = 5
private const val REFRESH_INTERVAL = 50
class DownloadTask(
private val item: DownloadableTrack,
val downloadTrack: DownloadableTrack,
private val scope: CoroutineScope,
private val stateChangedCallback: (DownloadableTrack, DownloadState, progress: Int?) -> Unit
) : KoinComponent {
@ -49,38 +49,35 @@ class DownloadTask(
private var outputStream: OutputStream? = null
private var lastPostTime: Long = 0
val track: DownloadableTrack
get() = item
private fun checkIfExists(): Boolean {
if (Storage.isPathExists(item.pinnedFile)) {
Timber.i("%s already exists. Skipping.", item.pinnedFile)
stateChangedCallback(item, DownloadState.PINNED, null)
if (Storage.isPathExists(downloadTrack.pinnedFile)) {
Timber.i("%s already exists. Skipping.", downloadTrack.pinnedFile)
stateChangedCallback(downloadTrack, DownloadState.PINNED, null)
return true
}
if (Storage.isPathExists(item.completeFile)) {
if (Storage.isPathExists(downloadTrack.completeFile)) {
var newStatus: DownloadState = DownloadState.DONE
if (item.pinned) {
if (downloadTrack.pinned) {
Storage.rename(
item.completeFile,
item.pinnedFile
downloadTrack.completeFile,
downloadTrack.pinnedFile
)
newStatus = DownloadState.PINNED
} else {
Timber.i(
"%s already exists. Skipping.",
item.completeFile
downloadTrack.completeFile
)
}
// Hidden feature: If track is toggled between pinned/saved, refresh the metadata..
try {
item.track.cacheMetadataAndArtwork()
downloadTrack.track.cacheMetadataAndArtwork()
} catch (ignore: Exception) {
Timber.w(ignore)
}
stateChangedCallback(item, newStatus, null)
stateChangedCallback(downloadTrack, newStatus, null)
return true
}
@ -88,15 +85,15 @@ class DownloadTask(
}
fun download() {
stateChangedCallback(item, DownloadState.DOWNLOADING, null)
stateChangedCallback(downloadTrack, DownloadState.DOWNLOADING, null)
val fileLength = Storage.getFromPath(item.partialFile)?.length ?: 0
val fileLength = Storage.getFromPath(downloadTrack.partialFile)?.length ?: 0
// Attempt partial HTTP GET, appending to the file if it exists.
val (inStream, isPartial) = musicService.getDownloadInputStream(
item.track, fileLength,
downloadTrack.track, fileLength,
Settings.maxBitRate,
item.pinned
downloadTrack.pinned
)
inputStream = inStream
@ -105,7 +102,7 @@ class DownloadTask(
Timber.i("Executed partial HTTP GET, skipping %d bytes", fileLength)
}
outputStream = Storage.getOrCreateFileFromPath(item.partialFile)
outputStream = Storage.getOrCreateFileFromPath(downloadTrack.partialFile)
.getFileOutputStream(isPartial)
val len = inputStream!!.copyWithProgress(outputStream!!) { totalBytesCopied ->
@ -113,7 +110,7 @@ class DownloadTask(
publishProgressUpdate(fileLength + totalBytesCopied)
}
Timber.i("Downloaded %d bytes to %s", len, item.partialFile)
Timber.i("Downloaded %d bytes to %s", len, downloadTrack.partialFile)
inputStream?.close()
outputStream?.flush()
@ -131,7 +128,7 @@ class DownloadTask(
lastPostTime = SystemClock.elapsedRealtime()
// If the file size is unknown we can only provide null as the progress
val size = item.track.size ?: 0
val size = downloadTrack.track.size ?: 0
val progress = if (size <= 0) {
null
} else {
@ -139,7 +136,7 @@ class DownloadTask(
}
stateChangedCallback(
item,
downloadTrack,
DownloadState.DOWNLOADING,
progress
)
@ -148,39 +145,39 @@ class DownloadTask(
private fun afterDownload() {
try {
item.track.cacheMetadataAndArtwork()
downloadTrack.track.cacheMetadataAndArtwork()
} catch (ignore: Exception) {
Timber.w(ignore)
}
if (item.pinned) {
if (downloadTrack.pinned) {
Storage.rename(
item.partialFile,
item.pinnedFile
downloadTrack.partialFile,
downloadTrack.pinnedFile
)
Timber.i("Renamed file to ${item.pinnedFile}")
stateChangedCallback(item, DownloadState.PINNED, null)
Util.scanMedia(item.pinnedFile)
Timber.i("Renamed file to ${downloadTrack.pinnedFile}")
stateChangedCallback(downloadTrack, DownloadState.PINNED, null)
Util.scanMedia(downloadTrack.pinnedFile)
} else {
Storage.rename(
item.partialFile,
item.completeFile
downloadTrack.partialFile,
downloadTrack.completeFile
)
Timber.i("Renamed file to ${item.completeFile}")
stateChangedCallback(item, DownloadState.DONE, null)
Timber.i("Renamed file to ${downloadTrack.completeFile}")
stateChangedCallback(downloadTrack, DownloadState.DONE, null)
}
}
private fun onCompletion(e: Throwable?) {
if (e is CancellationException) {
Timber.w(e, "CompletionHandler ${item.pinnedFile}")
stateChangedCallback(item, DownloadState.CANCELLED, null)
Timber.w(e, "CompletionHandler ${downloadTrack.pinnedFile}")
stateChangedCallback(downloadTrack, DownloadState.CANCELLED, null)
} else if (e != null) {
Timber.w(e, "CompletionHandler ${item.pinnedFile}")
if (item.tryCount < MAX_RETRIES) {
stateChangedCallback(item, DownloadState.RETRYING, null)
Timber.w(e, "CompletionHandler ${downloadTrack.pinnedFile}")
if (downloadTrack.tryCount < MAX_RETRIES) {
stateChangedCallback(downloadTrack, DownloadState.RETRYING, null)
} else {
stateChangedCallback(item, DownloadState.FAILED, null)
stateChangedCallback(downloadTrack, DownloadState.FAILED, null)
}
}
inputStream.safeClose()
@ -189,15 +186,16 @@ class DownloadTask(
private fun exceptionHandler(): CoroutineExceptionHandler {
return CoroutineExceptionHandler { _, exception ->
Timber.w(exception, "Exception in DownloadTask ${item.pinnedFile}")
Storage.delete(item.completeFile)
Storage.delete(item.pinnedFile)
Timber.w(exception, "Exception in DownloadTask ${downloadTrack.pinnedFile}")
Storage.delete(downloadTrack.completeFile)
Storage.delete(downloadTrack.pinnedFile)
}
}
fun start() {
Timber.i("Launching new Job ${item.pinnedFile}")
Timber.i("Launching new Job ${downloadTrack.pinnedFile}")
job = scope.launch(exceptionHandler()) {
FileUtil.createDirectoryForParent(downloadTrack.pinnedFile)
if (!checkIfExists() && isActive) {
download()
afterDownload()

View File

@ -418,13 +418,6 @@ class MediaPlayerController(
}
}
@Synchronized
fun downloadBackground(songs: List<Track?>?, save: Boolean) {
if (songs == null) return
val filteredSongs = songs.filterNotNull()
DownloadService.download(filteredSongs, save)
}
@set:Synchronized
var isShufflePlayEnabled: Boolean
get() = controller?.shuffleModeEnabled == true
@ -500,22 +493,6 @@ class MediaPlayerController(
)
}
@Synchronized
// TODO: Make it require not null
fun delete(tracks: List<Track?>) {
for (track in tracks.filterNotNull()) {
DownloadService.delete(track)
}
}
@Synchronized
// TODO: Make it require not null
fun unpin(tracks: List<Track?>) {
for (track in tracks.filterNotNull()) {
DownloadService.unpin(track)
}
}
@Synchronized
fun previous() {
controller?.seekToPrevious()

View File

@ -20,6 +20,7 @@ import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.DownloadService
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.CommunicationError
@ -266,7 +267,7 @@ class DownloadHandler(
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
if (!background) {
if (unpin) {
mediaPlayerController.unpin(songs)
DownloadService.unpin(songs)
} else {
val insertionMode = when {
append -> MediaPlayerController.InsertionMode.APPEND
@ -293,9 +294,9 @@ class DownloadHandler(
}
} else {
if (unpin) {
mediaPlayerController.unpin(songs)
DownloadService.unpin(songs)
} else {
mediaPlayerController.downloadBackground(songs, save)
DownloadService.download(songs, save)
}
}
}

View File

@ -33,7 +33,7 @@ ImageLoaderProvider(val context: Context) :
}
init {
Timber.e("Prepping Loader")
Timber.d("Prepping Loader")
// Populate the ImageLoader async & early
launch {
getImageLoader()