ultrasonic-app-subsonic-and.../ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt

260 lines
7.8 KiB
Kotlin
Raw Normal View History

package org.moire.ultrasonic.imageloader
2021-06-23 17:30:16 +02:00
import android.app.ActivityManager
import android.content.Context
2021-06-23 17:30:16 +02:00
import android.content.pm.ApplicationInfo
import android.text.TextUtils
import android.view.View
import android.widget.ImageView
2021-06-23 17:30:16 +02:00
import androidx.core.content.ContextCompat
import com.squareup.picasso.LruCache
import com.squareup.picasso.Picasso
import com.squareup.picasso.RequestCreator
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import org.moire.ultrasonic.BuildConfig
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.throwOnFailure
import org.moire.ultrasonic.api.subsonic.toStreamResponse
import org.moire.ultrasonic.domain.MusicDirectory
2021-06-07 13:17:00 +02:00
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.StorageFile
import org.moire.ultrasonic.util.Util
import timber.log.Timber
import java.io.File
/**
* Our new image loader which uses Picasso as a backend.
*/
class ImageLoader(
context: Context,
apiClient: SubsonicAPIClient,
private val config: ImageLoaderConfig
2021-06-07 13:17:00 +02:00
) {
// Shortcut
@Suppress("VariableNaming", "PropertyName")
val API = apiClient.api
private val picasso = Picasso.Builder(context)
.addRequestHandler(CoverArtRequestHandler(apiClient))
.addRequestHandler(AvatarRequestHandler(apiClient))
2021-06-23 17:30:16 +02:00
.memoryCache(LruCache(calculateMemoryCacheSize(context)))
.build().apply {
setIndicatorsEnabled(BuildConfig.DEBUG)
}
private fun load(request: ImageRequest) = when (request) {
is ImageRequest.CoverArt -> loadCoverArt(request)
is ImageRequest.Avatar -> loadAvatar(request)
}
private fun loadCoverArt(request: ImageRequest.CoverArt) {
picasso.load(createLoadCoverArtRequest(request.entityId, request.size.toLong()))
.addPlaceholder(request)
.addError(request)
2021-06-07 13:17:00 +02:00
.stableKey(request.cacheKey)
.into(request.imageView)
}
private fun loadAvatar(request: ImageRequest.Avatar) {
picasso.load(createLoadAvatarRequest(request.username))
.addPlaceholder(request)
.addError(request)
.stableKey(request.username)
.into(request.imageView)
}
private fun RequestCreator.addPlaceholder(request: ImageRequest): RequestCreator {
if (request.placeHolderDrawableRes != null) {
placeholder(request.placeHolderDrawableRes)
}
return this
}
private fun RequestCreator.addError(request: ImageRequest): RequestCreator {
if (request.errorDrawableRes != null) {
error(request.errorDrawableRes)
}
return this
}
/**
* Load the cover of a given entry into an ImageView
*/
@JvmOverloads
fun loadImage(
view: View?,
entry: MusicDirectory.Entry?,
large: Boolean,
size: Int,
defaultResourceId: Int = R.drawable.unknown_album
) {
val id = entry?.coverArt
val key = FileUtil.getAlbumArtKey(entry, large)
loadImage(view, id, key, large, size, defaultResourceId)
}
/**
* Load the cover of a given entry into an ImageView
*/
@JvmOverloads
@Suppress("LongParameterList", "ComplexCondition")
fun loadImage(
view: View?,
id: String?,
key: String?,
large: Boolean,
size: Int,
defaultResourceId: Int = R.drawable.unknown_album
) {
val requestedSize = resolveSize(size, large)
if (id != null && key != null && id.isNotEmpty() && view is ImageView) {
val request = ImageRequest.CoverArt(
2021-06-07 13:17:00 +02:00
id, key, view, requestedSize,
placeHolderDrawableRes = defaultResourceId,
errorDrawableRes = defaultResourceId
)
load(request)
} else if (view is ImageView) {
view.setImageResource(defaultResourceId)
}
}
/**
* Load the avatar of a given user into an ImageView
*/
fun loadAvatarImage(
view: ImageView,
username: String
) {
if (username.isNotEmpty()) {
val request = ImageRequest.Avatar(
username, view,
placeHolderDrawableRes = R.drawable.ic_contact_picture,
errorDrawableRes = R.drawable.ic_contact_picture
)
load(request)
} else {
view.setImageResource(R.drawable.ic_contact_picture)
}
}
/**
* Download a cover art file and cache it on disk
*/
fun cacheCoverArt(
entry: MusicDirectory.Entry
) {
// Synchronize on the entry so that we don't download concurrently for
// the same song.
synchronized(entry) {
// Always download the large size..
val size = config.largeSize
// Check cache to avoid downloading existing files
val file = FileUtil.getAlbumArtFile(entry)
// Return if have a cache hit
if (file != null && File(file).exists()) return
File(file!!).createNewFile()
// Can't load empty string ids
val id = entry.coverArt
if (TextUtils.isEmpty(id)) return
// Query the API
Timber.d("Loading cover art for: %s", entry)
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
2021-08-30 10:08:27 +02:00
val bytes = inputStream!!.readBytes()
var outputStream: OutputStream? = null
try {
outputStream = FileOutputStream(file)
outputStream.write(bytes)
} finally {
Util.close(outputStream)
}
} finally {
Util.close(inputStream)
}
}
}
private fun resolveSize(requested: Int, large: Boolean): Int {
if (requested <= 0) {
return if (large) config.largeSize else config.defaultSize
} else {
return requested
}
}
2021-06-23 17:30:16 +02:00
private fun calculateMemoryCacheSize(context: Context): Int {
val am = ContextCompat.getSystemService(
context,
ActivityManager::class.java
)
val largeHeap = context.applicationInfo.flags and ApplicationInfo.FLAG_LARGE_HEAP != 0
val memoryClass = if (largeHeap) am!!.largeMemoryClass else am!!.memoryClass
// Target 25% of the available heap.
@Suppress("MagicNumber")
return (1024L * 1024L * memoryClass / 4).toInt()
}
}
/**
* Data classes to hold all the info we need later on to process the request
*/
sealed class ImageRequest(
val placeHolderDrawableRes: Int? = null,
val errorDrawableRes: Int? = null,
val imageView: ImageView
) {
class CoverArt(
val entityId: String,
2021-06-07 13:17:00 +02:00
val cacheKey: String,
imageView: ImageView,
val size: Int,
placeHolderDrawableRes: Int? = null,
errorDrawableRes: Int? = null,
) : ImageRequest(
placeHolderDrawableRes,
errorDrawableRes,
imageView
)
class Avatar(
val username: String,
imageView: ImageView,
placeHolderDrawableRes: Int? = null,
errorDrawableRes: Int? = null
) : ImageRequest(
placeHolderDrawableRes,
errorDrawableRes,
imageView
)
}
/**
* Used to configure an instance of the ImageLoader
*/
2021-06-07 13:17:00 +02:00
data class ImageLoaderConfig(
val largeSize: Int = 0,
val defaultSize: Int = 0,
val cacheFolder: File?
)