2021-06-07 00:22:29 +02:00
|
|
|
package org.moire.ultrasonic.imageloader
|
2018-06-26 21:11:39 +02:00
|
|
|
|
2021-06-23 17:30:16 +02:00
|
|
|
import android.app.ActivityManager
|
2018-06-26 21:11:39 +02:00
|
|
|
import android.content.Context
|
2021-06-23 17:30:16 +02:00
|
|
|
import android.content.pm.ApplicationInfo
|
2021-06-08 17:12:55 +02:00
|
|
|
import android.text.TextUtils
|
2021-06-07 00:22:29 +02:00
|
|
|
import android.view.View
|
2018-06-26 21:11:39 +02:00
|
|
|
import android.widget.ImageView
|
2021-06-23 17:30:16 +02:00
|
|
|
import androidx.core.content.ContextCompat
|
|
|
|
import com.squareup.picasso.LruCache
|
2018-06-26 21:11:39 +02:00
|
|
|
import com.squareup.picasso.Picasso
|
2018-07-14 20:55:45 +02:00
|
|
|
import com.squareup.picasso.RequestCreator
|
2021-06-07 13:17:00 +02:00
|
|
|
import java.io.File
|
2021-06-08 17:12:55 +02:00
|
|
|
import java.io.FileOutputStream
|
|
|
|
import java.io.InputStream
|
|
|
|
import java.io.OutputStream
|
2021-06-07 00:22:29 +02:00
|
|
|
import org.moire.ultrasonic.BuildConfig
|
|
|
|
import org.moire.ultrasonic.R
|
2018-06-26 21:11:39 +02:00
|
|
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
2021-06-09 12:19:34 +02:00
|
|
|
import org.moire.ultrasonic.api.subsonic.throwOnFailure
|
|
|
|
import org.moire.ultrasonic.api.subsonic.toStreamResponse
|
2021-06-07 00:22:29 +02:00
|
|
|
import org.moire.ultrasonic.domain.MusicDirectory
|
2021-06-07 13:17:00 +02:00
|
|
|
import org.moire.ultrasonic.util.FileUtil
|
2021-06-08 17:12:55 +02:00
|
|
|
import org.moire.ultrasonic.util.Util
|
|
|
|
import timber.log.Timber
|
2018-06-26 21:11:39 +02:00
|
|
|
|
2021-06-07 00:22:29 +02:00
|
|
|
/**
|
|
|
|
* Our new image loader which uses Picasso as a backend.
|
|
|
|
*/
|
|
|
|
class ImageLoader(
|
2018-06-28 22:03:47 +02:00
|
|
|
context: Context,
|
2021-06-09 12:19:34 +02:00
|
|
|
apiClient: SubsonicAPIClient,
|
2021-06-07 00:22:29 +02:00
|
|
|
private val config: ImageLoaderConfig
|
2021-06-07 13:17:00 +02:00
|
|
|
) {
|
2021-06-09 12:19:34 +02:00
|
|
|
// Shortcut
|
|
|
|
@Suppress("VariableNaming", "PropertyName")
|
|
|
|
val API = apiClient.api
|
2021-06-07 00:22:29 +02:00
|
|
|
|
2018-06-26 21:11:39 +02:00
|
|
|
private val picasso = Picasso.Builder(context)
|
2018-07-14 20:55:45 +02:00
|
|
|
.addRequestHandler(CoverArtRequestHandler(apiClient))
|
|
|
|
.addRequestHandler(AvatarRequestHandler(apiClient))
|
2021-06-23 17:30:16 +02:00
|
|
|
.memoryCache(LruCache(calculateMemoryCacheSize(context)))
|
2021-06-02 20:37:13 +02:00
|
|
|
.build().apply {
|
|
|
|
setIndicatorsEnabled(BuildConfig.DEBUG)
|
|
|
|
}
|
2018-06-26 21:11:39 +02:00
|
|
|
|
2021-06-07 00:22:29 +02:00
|
|
|
private fun load(request: ImageRequest) = when (request) {
|
2018-06-28 22:03:47 +02:00
|
|
|
is ImageRequest.CoverArt -> loadCoverArt(request)
|
2018-07-14 20:55:45 +02:00
|
|
|
is ImageRequest.Avatar -> loadAvatar(request)
|
2018-06-26 21:11:39 +02:00
|
|
|
}
|
2018-06-28 22:03:47 +02:00
|
|
|
|
|
|
|
private fun loadCoverArt(request: ImageRequest.CoverArt) {
|
2021-06-02 20:37:13 +02:00
|
|
|
picasso.load(createLoadCoverArtRequest(request.entityId, request.size.toLong()))
|
2018-07-14 20:55:45 +02:00
|
|
|
.addPlaceholder(request)
|
|
|
|
.addError(request)
|
2021-06-07 13:17:00 +02:00
|
|
|
.stableKey(request.cacheKey)
|
2018-06-28 22:03:47 +02:00
|
|
|
.into(request.imageView)
|
|
|
|
}
|
2018-07-14 20:55:45 +02:00
|
|
|
|
|
|
|
private fun loadAvatar(request: ImageRequest.Avatar) {
|
|
|
|
picasso.load(createLoadAvatarRequest(request.username))
|
|
|
|
.addPlaceholder(request)
|
|
|
|
.addError(request)
|
2021-06-02 15:21:45 +02:00
|
|
|
.stableKey(request.username)
|
2018-07-14 20:55:45 +02:00
|
|
|
.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
|
|
|
|
}
|
2021-06-07 00:22:29 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 requestedSize = resolveSize(size, large)
|
|
|
|
|
|
|
|
if (id != null && id.isNotEmpty() && view is ImageView) {
|
2021-06-07 21:38:39 +02:00
|
|
|
val key = FileUtil.getAlbumArtKey(entry, large)
|
2021-06-07 00:22:29 +02:00
|
|
|
val request = ImageRequest.CoverArt(
|
2021-06-07 13:17:00 +02:00
|
|
|
id, key, view, requestedSize,
|
2021-06-07 00:22:29 +02:00
|
|
|
placeHolderDrawableRes = defaultResourceId,
|
|
|
|
errorDrawableRes = defaultResourceId
|
|
|
|
)
|
|
|
|
load(request)
|
2021-06-07 20:32:44 +02:00
|
|
|
} else if (view is ImageView) {
|
|
|
|
view.setImageResource(defaultResourceId)
|
2021-06-07 00:22:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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)
|
2021-06-07 20:32:44 +02:00
|
|
|
} else {
|
|
|
|
view.setImageResource(R.drawable.ic_contact_picture)
|
2021-06-07 00:22:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-08 17:12:55 +02:00
|
|
|
/**
|
|
|
|
* 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.exists()) return
|
|
|
|
|
|
|
|
// 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)
|
2021-06-09 12:19:34 +02:00
|
|
|
val response = API.getCoverArt(id!!, size.toLong()).execute().toStreamResponse()
|
|
|
|
response.throwOnFailure()
|
2021-06-08 17:12:55 +02:00
|
|
|
|
|
|
|
// 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()
|
2021-06-08 17:12:55 +02:00
|
|
|
var outputStream: OutputStream? = null
|
|
|
|
try {
|
|
|
|
outputStream = FileOutputStream(file)
|
|
|
|
outputStream.write(bytes)
|
|
|
|
} finally {
|
|
|
|
Util.close(outputStream)
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
Util.close(inputStream)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-07 00:22:29 +02:00
|
|
|
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()
|
|
|
|
}
|
2018-06-28 22:03:47 +02:00
|
|
|
}
|
|
|
|
|
2021-06-07 00:22:29 +02:00
|
|
|
/**
|
|
|
|
* Data classes to hold all the info we need later on to process the request
|
|
|
|
*/
|
2018-06-28 22:03:47 +02:00
|
|
|
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,
|
2018-06-28 22:03:47 +02:00
|
|
|
imageView: ImageView,
|
2021-06-02 15:21:45 +02:00
|
|
|
val size: Int,
|
2018-06-28 22:03:47 +02:00
|
|
|
placeHolderDrawableRes: Int? = null,
|
2021-06-02 15:21:45 +02:00
|
|
|
errorDrawableRes: Int? = null,
|
2018-06-28 22:03:47 +02:00
|
|
|
) : ImageRequest(
|
|
|
|
placeHolderDrawableRes,
|
|
|
|
errorDrawableRes,
|
|
|
|
imageView
|
|
|
|
)
|
2018-07-14 20:55:45 +02:00
|
|
|
|
|
|
|
class Avatar(
|
|
|
|
val username: String,
|
|
|
|
imageView: ImageView,
|
|
|
|
placeHolderDrawableRes: Int? = null,
|
|
|
|
errorDrawableRes: Int? = null
|
|
|
|
) : ImageRequest(
|
|
|
|
placeHolderDrawableRes,
|
|
|
|
errorDrawableRes,
|
|
|
|
imageView
|
|
|
|
)
|
2018-06-26 21:11:39 +02:00
|
|
|
}
|
2021-06-07 00:22:29 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Used to configure an instance of the ImageLoader
|
|
|
|
*/
|
2021-06-07 13:17:00 +02:00
|
|
|
data class ImageLoaderConfig(
|
2021-06-07 00:22:29 +02:00
|
|
|
val largeSize: Int = 0,
|
|
|
|
val defaultSize: Int = 0,
|
|
|
|
val cacheFolder: File?
|
|
|
|
)
|