Use file cache in Picasso

This commit is contained in:
tzugen 2021-06-07 13:17:00 +02:00
parent 9161f9dc99
commit 566e429e4c
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
20 changed files with 249 additions and 251 deletions

View File

@ -30,6 +30,8 @@ performance:
exceptions: exceptions:
active: true active: true
TooGenericExceptionCaught:
allowedExceptionNameRegex: '_|(all|ignore|expected).*'
empty-blocks: empty-blocks:
active: true active: true

View File

@ -15,10 +15,10 @@ import android.widget.RemoteViews;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.NavigationActivity; import org.moire.ultrasonic.activity.NavigationActivity;
import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.imageloader.BitmapUtils;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.service.MediaPlayerController; import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FileUtil;
import timber.log.Timber; import timber.log.Timber;
@ -160,7 +160,7 @@ public class UltrasonicAppWidgetProvider extends AppWidgetProvider
// Set the cover art // Set the cover art
try try
{ {
Bitmap bitmap = currentSong == null ? null : FileUtil.getAlbumArtBitmapFromDisk(currentSong, 240, true); Bitmap bitmap = currentSong == null ? null : BitmapUtils.Companion.getAlbumArtBitmapFromDisk(currentSong, 240);
if (bitmap == null) if (bitmap == null)
{ {

View File

@ -19,8 +19,6 @@
package org.moire.ultrasonic.util; package org.moire.ultrasonic.util;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build; import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.text.TextUtils; import android.text.TextUtils;
@ -38,6 +36,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -127,6 +126,24 @@ public class FileUtil
return getAlbumArtFile(albumDir); return getAlbumArtFile(albumDir);
} }
/**
* Get the cache key for a given album entry
* @param entry The album entry
* @return String The hash key
*/
public static String getAlbumArtKey(MusicDirectory.Entry entry)
{
File albumDir = getAlbumDirectory(entry);
File albumArtDir = getAlbumArtDirectory();
if (albumArtDir == null || albumDir == null) {
return null;
}
return String.format(Locale.ROOT, "%s.jpeg", Util.md5Hex(albumDir.getPath()));
}
public static File getAvatarFile(String username) public static File getAvatarFile(String username)
{ {
File albumArtDir = getAlbumArtDirectory(); File albumArtDir = getAlbumArtDirectory();
@ -158,119 +175,24 @@ public class FileUtil
return new File(albumArtDir, String.format("%s.jpeg", md5Hex)); return new File(albumArtDir, String.format("%s.jpeg", md5Hex));
} }
public static Bitmap getAvatarBitmapFromDisk(String username, int size, boolean highQuality)
/**
* Get the album art file for a given cache key
* @param cacheKey
* @return File object. Not guaranteed that it exists
*/
public static File getAlbumArtFile(String cacheKey)
{ {
if (username == null) return null; File albumArtDir = getAlbumArtDirectory();
File avatarFile = getAvatarFile(username); if (albumArtDir == null || cacheKey == null)
Bitmap bitmap = null;
if (avatarFile != null && avatarFile.exists())
{ {
final BitmapFactory.Options opt = new BitmapFactory.Options(); return null;
if (size > 0)
{
opt.inJustDecodeBounds = true;
BitmapFactory.decodeFile(avatarFile.getPath(), opt);
if (highQuality)
{
opt.inDither = true;
opt.inPreferQualityOverSpeed = true;
}
opt.inPurgeable = true;
opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size));
opt.inJustDecodeBounds = false;
}
try
{
bitmap = BitmapFactory.decodeFile(avatarFile.getPath(), opt);
}
catch (Exception ex)
{
Timber.e(ex, "Exception in BitmapFactory.decodeFile()");
}
Timber.i("getAvatarBitmapFromDisk %s", String.valueOf(size));
return bitmap;
} }
return null; return new File(albumArtDir, cacheKey);
} }
public static Bitmap getAlbumArtBitmapFromDisk(MusicDirectory.Entry entry, int size, boolean highQuality)
{
if (entry == null) return null;
File albumArtFile = getAlbumArtFile(entry);
Bitmap bitmap = null;
if (albumArtFile != null && albumArtFile.exists())
{
final BitmapFactory.Options opt = new BitmapFactory.Options();
if (size > 0)
{
opt.inJustDecodeBounds = true;
BitmapFactory.decodeFile(albumArtFile.getPath(), opt);
if (highQuality)
{
opt.inDither = true;
opt.inPreferQualityOverSpeed = true;
}
opt.inPurgeable = true;
opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size));
opt.inJustDecodeBounds = false;
}
try
{
bitmap = BitmapFactory.decodeFile(albumArtFile.getPath(), opt);
}
catch (Exception ex)
{
Timber.e(ex, "Exception in BitmapFactory.decodeFile()");
}
Timber.i("getAlbumArtBitmapFromDisk %s", String.valueOf(size));
return bitmap;
}
return null;
}
public static Bitmap getSampledBitmap(byte[] bytes, int size, boolean highQuality)
{
final BitmapFactory.Options opt = new BitmapFactory.Options();
if (size > 0)
{
opt.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt);
if (highQuality)
{
opt.inDither = true;
opt.inPreferQualityOverSpeed = true;
}
opt.inPurgeable = true;
opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size));
opt.inJustDecodeBounds = false;
}
Timber.i("getSampledBitmap %s", String.valueOf(size));
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt);
}
public static File getAlbumArtDirectory() public static File getAlbumArtDirectory()
{ {

View File

@ -1,17 +0,0 @@
package org.moire.ultrasonic.util;
import android.view.View;
import org.moire.ultrasonic.domain.MusicDirectory;
public interface ImageLoader {
void loadAvatarImage(View view, String username, boolean large, int size, boolean crossFade,
boolean highQuality);
void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size,
boolean crossFade, boolean highQuality);
void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size,
boolean crossFade, boolean highQuality, int defaultResourceId);
}

View File

@ -12,6 +12,7 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
import org.moire.ultrasonic.cache.PermanentFileStorage import org.moire.ultrasonic.cache.PermanentFileStorage
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.imageloader.ImageLoader
import org.moire.ultrasonic.log.TimberOkHttpLogger import org.moire.ultrasonic.log.TimberOkHttpLogger
import org.moire.ultrasonic.service.ApiCallResponseChecker import org.moire.ultrasonic.service.ApiCallResponseChecker
import org.moire.ultrasonic.service.CachedMusicService import org.moire.ultrasonic.service.CachedMusicService
@ -19,10 +20,10 @@ import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.OfflineMusicService import org.moire.ultrasonic.service.OfflineMusicService
import org.moire.ultrasonic.service.RESTMusicService import org.moire.ultrasonic.service.RESTMusicService
import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.subsonic.VideoPlayer import org.moire.ultrasonic.subsonic.VideoPlayer
import org.moire.ultrasonic.imageloader.SubsonicImageLoader
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
/** /**
@ -77,6 +78,8 @@ val musicServiceModule = module {
OfflineMusicService() OfflineMusicService()
} }
single { ImageLoader(androidContext(), get(), ImageLoaderProvider.config) }
single { DownloadHandler(get(), get()) } single { DownloadHandler(get(), get()) }
single { NetworkAndStorageChecker(androidContext()) } single { NetworkAndStorageChecker(androidContext()) }
single { VideoPlayer() } single { VideoPlayer() }

View File

@ -55,8 +55,7 @@ class AlbumRowAdapter(
holder.coverArtId = entry.coverArt holder.coverArtId = entry.coverArt
imageLoader.loadImage( imageLoader.loadImage(
holder.coverArt, holder.coverArt, entry,
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
false, 0, R.drawable.unknown_album false, 0, R.drawable.unknown_album
) )
} }

View File

@ -61,7 +61,10 @@ class ArtistRowAdapter(
holder.coverArt.visibility = View.VISIBLE holder.coverArt.visibility = View.VISIBLE
imageLoader.loadImage( imageLoader.loadImage(
holder.coverArt, holder.coverArt,
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId }, MusicDirectory.Entry("-1").apply {
coverArt = holder.coverArtId
artist = itemList[listPosition].name
},
false, 0, R.drawable.ic_contact_picture false, 0, R.drawable.ic_contact_picture
) )
} else { } else {

View File

@ -0,0 +1,128 @@
package org.moire.ultrasonic.imageloader
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util
import timber.log.Timber
@Suppress("UtilityClassWithPublicConstructor")
class BitmapUtils {
companion object {
fun getAvatarBitmapFromDisk(
username: String?,
size: Int
): Bitmap? {
if (username == null) return null
val avatarFile = FileUtil.getAvatarFile(username)
val bitmap: Bitmap? = null
if (avatarFile != null && avatarFile.exists()) {
return getBitmapFromDisk(avatarFile.path, size, bitmap)
}
return null
}
fun getAlbumArtBitmapFromDisk(
entry: MusicDirectory.Entry?,
size: Int
): Bitmap? {
if (entry == null) return null
val albumArtFile = FileUtil.getAlbumArtFile(entry)
val bitmap: Bitmap? = null
if (albumArtFile != null && albumArtFile.exists()) {
return getBitmapFromDisk(albumArtFile.path, size, bitmap)
}
return null
}
fun getAlbumArtBitmapFromDisk(
filename: String,
size: Int?
): Bitmap? {
val albumArtFile = FileUtil.getAlbumArtFile(filename)
val bitmap: Bitmap? = null
if (albumArtFile != null && albumArtFile.exists()) {
return getBitmapFromDisk(albumArtFile.path, size, bitmap)
}
return null
}
@Suppress("DEPRECATION")
fun getSampledBitmap(bytes: ByteArray, size: Int): Bitmap? {
val opt = BitmapFactory.Options()
if (size > 0) {
// With this flag we only calculate the size first
opt.inJustDecodeBounds = true
// Decode the size
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, opt)
// Now set the remaining flags
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
opt.inDither = true
opt.inPreferQualityOverSpeed = true
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
opt.inPurgeable = true
}
opt.inSampleSize = Util.calculateInSampleSize(
opt,
size,
Util.getScaledHeight(opt.outHeight.toDouble(), opt.outWidth.toDouble(), size)
)
// Enable real decoding
opt.inJustDecodeBounds = false
}
Timber.i("getSampledBitmap %s", size.toString())
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, opt)
}
@Suppress("DEPRECATION")
private fun getBitmapFromDisk(
path: String,
size: Int?,
bitmap: Bitmap?
): Bitmap? {
var bitmap1 = bitmap
val opt = BitmapFactory.Options()
if (size != null && size > 0) {
// With this flag we only calculate the size first
opt.inJustDecodeBounds = true
// Decode the size
BitmapFactory.decodeFile(path, opt)
// Now set the remaining flags
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
opt.inDither = true
opt.inPreferQualityOverSpeed = true
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
opt.inPurgeable = true
}
opt.inSampleSize = Util.calculateInSampleSize(
opt,
size,
Util.getScaledHeight(opt.outHeight.toDouble(), opt.outWidth.toDouble(), size)
)
// Enable real decoding
opt.inJustDecodeBounds = false
}
try {
bitmap1 = BitmapFactory.decodeFile(path, opt)
} catch (expected: Exception) {
Timber.e(expected, "Exception in BitmapFactory.decodeFile()")
}
Timber.i("getBitmapFromDisk %s", size.toString())
return bitmap1
}
}
}

View File

@ -1,5 +1,6 @@
package org.moire.ultrasonic.imageloader package org.moire.ultrasonic.imageloader
import com.squareup.picasso.Picasso.LoadedFrom.DISK
import com.squareup.picasso.Picasso.LoadedFrom.NETWORK import com.squareup.picasso.Picasso.LoadedFrom.NETWORK
import com.squareup.picasso.Request import com.squareup.picasso.Request
import com.squareup.picasso.RequestHandler import com.squareup.picasso.RequestHandler
@ -23,6 +24,12 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request
?: throw IllegalArgumentException("Nullable id") ?: throw IllegalArgumentException("Nullable id")
val size = request.uri.getQueryParameter(SIZE)?.toLong() val size = request.uri.getQueryParameter(SIZE)?.toLong()
// Check if we have a hit in the disk cache
val cache = BitmapUtils.getAlbumArtBitmapFromDisk(request.stableKey!!, size?.toInt())
if (cache != null) {
return Result(cache, DISK)
}
val response = apiClient.getCoverArt(id, size) val response = apiClient.getCoverArt(id, size)
if (response.hasError() || response.stream == null) { if (response.hasError() || response.stream == null) {
throw IOException("${response.apiError}") throw IOException("${response.apiError}")

View File

@ -5,11 +5,12 @@ import android.view.View
import android.widget.ImageView import android.widget.ImageView
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import com.squareup.picasso.RequestCreator import com.squareup.picasso.RequestCreator
import java.io.File
import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.BuildConfig
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import java.io.File import org.moire.ultrasonic.util.FileUtil
/** /**
* Our new image loader which uses Picasso as a backend. * Our new image loader which uses Picasso as a backend.
@ -18,14 +19,13 @@ class ImageLoader(
context: Context, context: Context,
apiClient: SubsonicAPIClient, apiClient: SubsonicAPIClient,
private val config: ImageLoaderConfig private val config: ImageLoaderConfig
) { ) {
private val picasso = Picasso.Builder(context) private val picasso = Picasso.Builder(context)
.addRequestHandler(CoverArtRequestHandler(apiClient)) .addRequestHandler(CoverArtRequestHandler(apiClient))
.addRequestHandler(AvatarRequestHandler(apiClient)) .addRequestHandler(AvatarRequestHandler(apiClient))
.build().apply { .build().apply {
setIndicatorsEnabled(BuildConfig.DEBUG) setIndicatorsEnabled(BuildConfig.DEBUG)
Picasso.setSingletonInstance(this)
} }
private fun load(request: ImageRequest) = when (request) { private fun load(request: ImageRequest) = when (request) {
@ -37,7 +37,7 @@ class ImageLoader(
picasso.load(createLoadCoverArtRequest(request.entityId, request.size.toLong())) picasso.load(createLoadCoverArtRequest(request.entityId, request.size.toLong()))
.addPlaceholder(request) .addPlaceholder(request)
.addError(request) .addError(request)
.stableKey("${request.entityId}-${request.size}") .stableKey(request.cacheKey)
.into(request.imageView) .into(request.imageView)
} }
@ -80,8 +80,9 @@ class ImageLoader(
val requestedSize = resolveSize(size, large) val requestedSize = resolveSize(size, large)
if (id != null && id.isNotEmpty() && view is ImageView) { if (id != null && id.isNotEmpty() && view is ImageView) {
val key = FileUtil.getAlbumArtKey(entry)
val request = ImageRequest.CoverArt( val request = ImageRequest.CoverArt(
id, view, requestedSize, id, key, view, requestedSize,
placeHolderDrawableRes = defaultResourceId, placeHolderDrawableRes = defaultResourceId,
errorDrawableRes = defaultResourceId errorDrawableRes = defaultResourceId
) )
@ -125,6 +126,7 @@ sealed class ImageRequest(
) { ) {
class CoverArt( class CoverArt(
val entityId: String, val entityId: String,
val cacheKey: String,
imageView: ImageView, imageView: ImageView,
val size: Int, val size: Int,
placeHolderDrawableRes: Int? = null, placeHolderDrawableRes: Int? = null,
@ -150,7 +152,7 @@ sealed class ImageRequest(
/** /**
* Used to configure an instance of the ImageLoader * Used to configure an instance of the ImageLoader
*/ */
data class ImageLoaderConfig ( data class ImageLoaderConfig(
val largeSize: Int = 0, val largeSize: Int = 0,
val defaultSize: Int = 0, val defaultSize: Int = 0,
val cacheFolder: File? val cacheFolder: File?

View File

@ -1,57 +0,0 @@
package org.moire.ultrasonic.imageloader
import android.content.Context
import com.squareup.picasso.Picasso
import com.squareup.picasso.RequestCreator
import org.moire.ultrasonic.BuildConfig
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
class SubsonicImageLoader(
context: Context,
apiClient: SubsonicAPIClient
) {
private val picasso = Picasso.Builder(context)
.addRequestHandler(CoverArtRequestHandler(apiClient))
.addRequestHandler(AvatarRequestHandler(apiClient))
.build().apply {
setIndicatorsEnabled(BuildConfig.DEBUG)
Picasso.setSingletonInstance(this)
}
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)
.stableKey("${request.entityId}-${request.size}")
.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
}
}

View File

@ -259,10 +259,9 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
override fun getCoverArt( override fun getCoverArt(
entry: MusicDirectory.Entry, entry: MusicDirectory.Entry,
size: Int, size: Int,
saveToFile: Boolean, saveToFile: Boolean
highQuality: Boolean
): Bitmap? { ): Bitmap? {
return musicService.getCoverArt(entry, size, saveToFile, highQuality) return musicService.getCoverArt(entry, size, saveToFile)
} }
@Throws(Exception::class) @Throws(Exception::class)
@ -451,10 +450,9 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
override fun getAvatar( override fun getAvatar(
username: String?, username: String?,
size: Int, size: Int,
saveToFile: Boolean, saveToFile: Boolean
highQuality: Boolean
): Bitmap? { ): Bitmap? {
return musicService.getAvatar(username, size, saveToFile, highQuality) return musicService.getAvatar(username, size, saveToFile)
} }
companion object { companion object {

View File

@ -24,6 +24,7 @@ import org.koin.core.component.inject
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.CacheCleaner
import org.moire.ultrasonic.util.CancellableTask import org.moire.ultrasonic.util.CancellableTask
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
@ -332,8 +333,9 @@ class DownloadFile(
private fun downloadAndSaveCoverArt(musicService: MusicService) { private fun downloadAndSaveCoverArt(musicService: MusicService) {
try { try {
if (!TextUtils.isEmpty(song.coverArt)) { if (!TextUtils.isEmpty(song.coverArt)) {
val size = Util.getMinDisplayMetric() // Download the largest size that we can display in the UI
musicService.getCoverArt(song, size, true, true) val size = ImageLoaderProvider.config.largeSize
musicService.getCoverArt(song, size = size, saveToFile = true)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to get cover art.") Timber.e(e, "Failed to get cover art.")

View File

@ -30,6 +30,7 @@ import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.domain.RepeatMode import org.moire.ultrasonic.domain.RepeatMode
import org.moire.ultrasonic.imageloader.BitmapUtils
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X1 import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X1
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X2 import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X2
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3 import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3
@ -37,7 +38,6 @@ import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.NowPlayingEventDistributor import org.moire.ultrasonic.util.NowPlayingEventDistributor
import org.moire.ultrasonic.util.ShufflePlayBuffer import org.moire.ultrasonic.util.ShufflePlayBuffer
import org.moire.ultrasonic.util.SimpleServiceBinder import org.moire.ultrasonic.util.SimpleServiceBinder
@ -478,9 +478,8 @@ class MediaPlayerService : Service() {
if (currentPlaying != null) { if (currentPlaying != null) {
try { try {
val song = currentPlaying.song val song = currentPlaying.song
val cover = FileUtil.getAlbumArtBitmapFromDisk( val cover = BitmapUtils.getAlbumArtBitmapFromDisk(
song, Util.getMinDisplayMetric(), song, Util.getMinDisplayMetric()
true
) )
metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, -1L) metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, -1L)
metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.artist) metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.artist)
@ -648,7 +647,7 @@ class MediaPlayerService : Service() {
// Set song title, artist and cover if possible // Set song title, artist and cover if possible
if (song != null) { if (song != null) {
val iconSize = (256 * context.resources.displayMetrics.density).toInt() val iconSize = (256 * context.resources.displayMetrics.density).toInt()
val bitmap = FileUtil.getAlbumArtBitmapFromDisk(song, iconSize, true) val bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(song, iconSize)
notificationBuilder!!.setContentTitle(song.title) notificationBuilder!!.setContentTitle(song.title)
notificationBuilder!!.setContentText(song.artist) notificationBuilder!!.setContentText(song.artist)
notificationBuilder!!.setLargeIcon(bitmap) notificationBuilder!!.setLargeIcon(bitmap)

View File

@ -115,12 +115,15 @@ interface MusicService {
fun getCoverArt( fun getCoverArt(
entry: MusicDirectory.Entry, entry: MusicDirectory.Entry,
size: Int, size: Int,
saveToFile: Boolean, saveToFile: Boolean
highQuality: Boolean
): Bitmap? ): Bitmap?
@Throws(Exception::class) @Throws(Exception::class)
fun getAvatar(username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean): Bitmap? fun getAvatar(
username: String?,
size: Int,
saveToFile: Boolean
): Bitmap?
/** /**
* Return response [InputStream] and a [Boolean] that indicates if this response is * Return response [InputStream] and a [Boolean] that indicates if this response is

View File

@ -40,6 +40,7 @@ import org.moire.ultrasonic.domain.SearchCriteria
import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.domain.Share
import org.moire.ultrasonic.domain.UserInfo import org.moire.ultrasonic.domain.UserInfo
import org.moire.ultrasonic.imageloader.BitmapUtils
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
@ -121,11 +122,10 @@ class OfflineMusicService : MusicService, KoinComponent {
override fun getAvatar( override fun getAvatar(
username: String?, username: String?,
size: Int, size: Int,
saveToFile: Boolean, saveToFile: Boolean
highQuality: Boolean
): Bitmap? { ): Bitmap? {
return try { return try {
val bitmap = FileUtil.getAvatarBitmapFromDisk(username, size, highQuality) val bitmap = BitmapUtils.getAvatarBitmapFromDisk(username, size)
Util.scaleBitmap(bitmap, size) Util.scaleBitmap(bitmap, size)
} catch (ignored: Exception) { } catch (ignored: Exception) {
null null
@ -135,11 +135,10 @@ class OfflineMusicService : MusicService, KoinComponent {
override fun getCoverArt( override fun getCoverArt(
entry: MusicDirectory.Entry, entry: MusicDirectory.Entry,
size: Int, size: Int,
saveToFile: Boolean, saveToFile: Boolean
highQuality: Boolean
): Bitmap? { ): Bitmap? {
return try { return try {
val bitmap = FileUtil.getAlbumArtBitmapFromDisk(entry, size, highQuality) val bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(entry, size)
Util.scaleBitmap(bitmap, size) Util.scaleBitmap(bitmap, size)
} catch (ignored: Exception) { } catch (ignored: Exception) {
null null

View File

@ -46,6 +46,7 @@ import org.moire.ultrasonic.domain.toDomainEntitiesList
import org.moire.ultrasonic.domain.toDomainEntity import org.moire.ultrasonic.domain.toDomainEntity
import org.moire.ultrasonic.domain.toDomainEntityList import org.moire.ultrasonic.domain.toDomainEntityList
import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity
import org.moire.ultrasonic.imageloader.BitmapUtils
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
@ -488,21 +489,18 @@ open class RESTMusicService(
return response.body()!!.starred2.toDomainEntity() return response.body()!!.starred2.toDomainEntity()
} }
// TODO: Implement file caching in Picasso CoverArtRequestHandler,
// and then use Picasso to handle this cache
// This is only called by DownloadFile to cache the cover art for offline use // This is only called by DownloadFile to cache the cover art for offline use
@Throws(Exception::class) @Throws(Exception::class)
override fun getCoverArt( override fun getCoverArt(
entry: MusicDirectory.Entry, entry: MusicDirectory.Entry,
size: Int, size: Int,
saveToFile: Boolean, saveToFile: Boolean
highQuality: Boolean
): Bitmap? { ): Bitmap? {
// Synchronize on the entry so that we don't download concurrently for // Synchronize on the entry so that we don't download concurrently for
// the same song. // the same song.
synchronized(entry) { synchronized(entry) {
// Use cached file, if existing. // Use cached file, if existing.
var bitmap = FileUtil.getAlbumArtBitmapFromDisk(entry, size, highQuality) var bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(entry, size)
val serverScaling = isServerScalingEnabled() val serverScaling = isServerScalingEnabled()
if (bitmap == null) { if (bitmap == null) {
@ -541,7 +539,7 @@ open class RESTMusicService(
} }
} }
bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality) bitmap = BitmapUtils.getSampledBitmap(bytes, size)
} finally { } finally {
Util.close(inputStream) Util.close(inputStream)
} }
@ -820,8 +818,7 @@ open class RESTMusicService(
override fun getAvatar( override fun getAvatar(
username: String?, username: String?,
size: Int, size: Int,
saveToFile: Boolean, saveToFile: Boolean
highQuality: Boolean
): Bitmap? { ): Bitmap? {
// Synchronize on the username so that we don't download concurrently for // Synchronize on the username so that we don't download concurrently for
// the same user. // the same user.
@ -831,7 +828,7 @@ open class RESTMusicService(
synchronized(username) { synchronized(username) {
// Use cached file, if existing. // Use cached file, if existing.
var bitmap = FileUtil.getAvatarBitmapFromDisk(username, size, highQuality) var bitmap = BitmapUtils.getAvatarBitmapFromDisk(username, size)
if (bitmap == null) { if (bitmap == null) {
var inputStream: InputStream? = null var inputStream: InputStream? = null
@ -858,7 +855,7 @@ open class RESTMusicService(
} }
} }
bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality) bitmap = BitmapUtils.getSampledBitmap(bytes, size)
} finally { } finally {
Util.close(inputStream) Util.close(inputStream)
} }

View File

@ -4,11 +4,12 @@ import android.content.Context
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import org.koin.core.qualifier.named
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.imageloader.ImageLoader
import org.moire.ultrasonic.imageloader.ImageLoaderConfig import org.moire.ultrasonic.imageloader.ImageLoaderConfig
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
/** /**
@ -16,26 +17,7 @@ import org.moire.ultrasonic.util.Util
*/ */
class ImageLoaderProvider(val context: Context) : KoinComponent { class ImageLoaderProvider(val context: Context) : KoinComponent {
private var imageLoader: ImageLoader? = null private var imageLoader: ImageLoader? = null
private var serverID: String = get(named("ServerID"))
private val config by lazy {
var defaultSize = 0
val fallbackImage = ResourcesCompat.getDrawable(
UApp.applicationContext().resources, R.drawable.unknown_album, null
)
// Determine the density-dependent image sizes by taking the fallback album
// image and querying its size.
if (fallbackImage != null) {
defaultSize = fallbackImage.intrinsicHeight
}
ImageLoaderConfig(
Util.getMaxDisplayMetric(),
defaultSize,
FileUtil.getAlbumArtDirectory()
)
}
@Synchronized @Synchronized
fun clearImageLoader() { fun clearImageLoader() {
@ -44,9 +26,33 @@ class ImageLoaderProvider(val context: Context) : KoinComponent {
@Synchronized @Synchronized
fun getImageLoader(): ImageLoader { fun getImageLoader(): ImageLoader {
if (imageLoader == null) { // We need to generate a new ImageLoader if the server has changed...
imageLoader = ImageLoader(get(), get(), config) val currentID = get<String>(named("ServerID"))
if (imageLoader == null || currentID != serverID) {
imageLoader = get()
serverID = currentID
} }
return imageLoader!! return imageLoader!!
} }
companion object {
val config by lazy {
var defaultSize = 0
val fallbackImage = ResourcesCompat.getDrawable(
UApp.applicationContext().resources, R.drawable.unknown_album, null
)
// Determine the density-dependent image sizes by taking the fallback album
// image and querying its size.
if (fallbackImage != null) {
defaultSize = fallbackImage.intrinsicHeight
}
ImageLoaderConfig(
Util.getMaxDisplayMetric(),
defaultSize,
FileUtil.getAlbumArtDirectory()
)
}
}
} }

View File

@ -64,7 +64,8 @@ class AvatarRequestHandlerTest {
.thenReturn(streamResponse) .thenReturn(streamResponse)
val response = handler.load( val response = handler.load(
createLoadAvatarRequest("some-username").buildRequest(), 0) createLoadAvatarRequest("some-username").buildRequest(), 0
)
response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK
response.source `should not be` null response.source `should not be` null

View File

@ -77,7 +77,8 @@ class CoverArtRequestHandlerTest {
.thenReturn(streamResponse) .thenReturn(streamResponse)
val response = handler.load( val response = handler.load(
createLoadCoverArtRequest("some").buildRequest(), 0) createLoadCoverArtRequest("some").buildRequest(), 0
)
response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK
response.source `should not be` null response.source `should not be` null