Merge pull request #514 from tzugen/coverart

Fix bugs in new image loader and make it default.
This commit is contained in:
tzugen 2021-06-08 21:15:46 +02:00 committed by GitHub
commit 580a170f4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 626 additions and 1473 deletions

View File

@ -1,29 +0,0 @@
apply from: bootstrap.androidModule
android {
buildFeatures {
buildConfig = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
api project(':core:domain')
api project(':core:subsonic-api')
api(other.picasso) {
exclude group: "com.android.support"
}
testImplementation testing.kotlinJunit
testImplementation testing.mockito
testImplementation testing.mockitoInline
testImplementation testing.mockitoKotlin
testImplementation testing.kluent
testImplementation testing.robolectric
}

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.moire.ultrasonic.subsonic.loader.image">
</manifest>

View File

@ -1,33 +0,0 @@
package org.moire.ultrasonic.subsonic.loader.image
import com.squareup.picasso.Picasso.LoadedFrom.NETWORK
import com.squareup.picasso.Request
import com.squareup.picasso.RequestHandler
import java.io.IOException
import okio.Okio
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
/**
* Loads cover arts from subsonic api.
*/
class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : RequestHandler() {
override fun canHandleRequest(data: Request): Boolean {
return with(data.uri) {
scheme == SCHEME &&
authority == AUTHORITY &&
path == "/$COVER_ART_PATH"
}
}
override fun load(request: Request, networkPolicy: Int): Result {
val id = request.uri.getQueryParameter(QUERY_ID)
?: throw IllegalArgumentException("Nullable id")
val response = apiClient.getCoverArt(id)
if (response.hasError() || response.stream == null) {
throw IOException("${response.apiError}")
} else {
return Result(Okio.source(response.stream!!), NETWORK)
}
}
}

View File

@ -1,24 +0,0 @@
package org.moire.ultrasonic.subsonic.loader.image
import android.net.Uri
internal const val SCHEME = "subsonic_api"
internal const val AUTHORITY = BuildConfig.LIBRARY_PACKAGE_NAME
internal const val COVER_ART_PATH = "cover_art"
internal const val AVATAR_PATH = "avatar"
internal const val QUERY_ID = "id"
internal const val QUERY_USERNAME = "username"
internal fun createLoadCoverArtRequest(entityId: String): Uri = Uri.Builder()
.scheme(SCHEME)
.authority(AUTHORITY)
.appendPath(COVER_ART_PATH)
.appendQueryParameter(QUERY_ID, entityId)
.build()
internal fun createLoadAvatarRequest(username: String): Uri = Uri.Builder()
.scheme(SCHEME)
.authority(AUTHORITY)
.appendPath(AVATAR_PATH)
.appendQueryParameter(QUERY_USERNAME, username)
.build()

View File

@ -1,80 +0,0 @@
package org.moire.ultrasonic.subsonic.loader.image
import android.content.Context
import android.widget.ImageView
import com.squareup.picasso.Picasso
import com.squareup.picasso.RequestCreator
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) }
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))
.addPlaceholder(request)
.addError(request)
.into(request.imageView)
}
private fun loadAvatar(request: ImageRequest.Avatar) {
picasso.load(createLoadAvatarRequest(request.username))
.addPlaceholder(request)
.addError(request)
.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
}
}
sealed class ImageRequest(
val placeHolderDrawableRes: Int? = null,
val errorDrawableRes: Int? = null,
val imageView: ImageView
) {
class CoverArt(
val entityId: String,
imageView: ImageView,
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
)
}

View File

@ -103,6 +103,7 @@ class SubsonicAPIClient(
val api: SubsonicAPIDefinition get() = wrappedApi
/**
* TODO: Remove this in favour of handling the stream response inside RESTService
* Convenient method to get cover art from api using item [id] and optional maximum [size].
*
* It detects the response `Content-Type` and tries to parse subsonic error if there is one.
@ -114,6 +115,7 @@ class SubsonicAPIClient(
}
/**
* TODO: Remove this in favour of handling the stream response inside RESTService
* Convenient method to get media stream from api using item [id] and optional [maxBitrate].
*
* Optionally also you can provide [offset] that stream should start from.
@ -128,6 +130,7 @@ class SubsonicAPIClient(
}
/**
* TODO: Remove this in favour of handling the stream response inside RESTService
* Convenient method to get user avatar using [username].
*
* It detects the response `Content-Type` and tries to parse subsonic error if there is one.
@ -138,6 +141,7 @@ class SubsonicAPIClient(
api.getAvatar(username).execute()
}
// TODO: Move this to response checker
private inline fun handleStreamResponse(apiCall: () -> Response<ResponseBody>): StreamResponse {
val response = apiCall()
return if (response.isSuccessful) {

View File

@ -1,8 +0,0 @@
package org.moire.ultrasonic.api.subsonic.di
import org.koin.dsl.module
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
val subsonicApiModule = module {
single { SubsonicAPIClient(get(), get()) }
}

View File

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

View File

@ -1,5 +1,4 @@
include ':core:domain'
include ':core:subsonic-api'
include ':core:subsonic-api-image-loader'
include ':core:cache'
include ':ultrasonic'

View File

@ -70,9 +70,12 @@ tasks.withType(Test) {
dependencies {
implementation project(':core:domain')
implementation project(':core:subsonic-api')
implementation project(':core:subsonic-api-image-loader')
implementation project(':core:cache')
api(other.picasso) {
exclude group: "com.android.support"
}
implementation androidSupport.core
implementation androidSupport.support
implementation androidSupport.design
@ -103,8 +106,12 @@ dependencies {
testImplementation testing.junit
testRuntimeOnly testing.junitVintage
testImplementation testing.kotlinJunit
testImplementation testing.mockitoKotlin
testImplementation testing.kluent
testImplementation testing.mockito
testImplementation testing.mockitoInline
testImplementation testing.mockitoKotlin
testImplementation testing.robolectric
implementation other.dexter
implementation other.timber
}

View File

@ -108,7 +108,7 @@ public class NowPlayingFragment extends Fragment {
String title = song.getTitle();
String artist = song.getArtist();
imageLoader.getValue().getImageLoader().loadImage(nowPlayingAlbumArtImage, song, false, Util.getNotificationImageSize(getContext()), false, true);
imageLoader.getValue().getImageLoader().loadImage(nowPlayingAlbumArtImage, song, false, Util.getNotificationImageSize(getContext()));
nowPlayingTrack.setText(title);
nowPlayingArtist.setText(artist);

View File

@ -1306,7 +1306,7 @@ public class PlayerFragment extends Fragment implements GestureDetector.OnGestur
artistTextView.setText(currentSong.getArtist());
downloadTrackTextView.setText(trackFormat);
downloadTotalDurationTextView.setText(duration);
imageLoaderProvider.getValue().getImageLoader().loadImage(albumArtImageView, currentSong, true, 0, false, true);
imageLoaderProvider.getValue().getImageLoader().loadImage(albumArtImageView, currentSong, true, 0);
displaySongRating();
}
@ -1318,7 +1318,7 @@ public class PlayerFragment extends Fragment implements GestureDetector.OnGestur
artistTextView.setText(null);
downloadTrackTextView.setText(null);
downloadTotalDurationTextView.setText(null);
imageLoaderProvider.getValue().getImageLoader().loadImage(albumArtImageView, null, true, 0, false, true);
imageLoaderProvider.getValue().getImageLoader().loadImage(albumArtImageView, null, true, 0);
}
}

View File

@ -6,6 +6,8 @@ import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.provider.SearchRecentSuggestions;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.DialogFragment;
@ -18,9 +20,6 @@ import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import timber.log.Timber;
import android.view.View;
import org.jetbrains.annotations.NotNull;
import org.koin.java.KoinJavaComponent;
import org.moire.ultrasonic.R;
@ -32,12 +31,18 @@ import org.moire.ultrasonic.log.FileLoggerTree;
import org.moire.ultrasonic.provider.SearchSuggestionProvider;
import org.moire.ultrasonic.service.Consumer;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import org.moire.ultrasonic.util.*;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FileUtil;
import org.moire.ultrasonic.util.PermissionUtil;
import org.moire.ultrasonic.util.ThemeChangedEventDistributor;
import org.moire.ultrasonic.util.TimeSpanPreference;
import org.moire.ultrasonic.util.TimeSpanPreferenceDialogFragmentCompat;
import org.moire.ultrasonic.util.Util;
import java.io.File;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
import static org.moire.ultrasonic.fragment.ServerSelectorFragment.SERVER_SELECTOR_MANAGE_MODE;
@ -73,7 +78,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
private CheckBoxPreference sendBluetoothAlbumArt;
private CheckBoxPreference showArtistPicture;
private ListPreference viewRefresh;
private ListPreference imageLoaderConcurrency;
private EditTextPreference sharingDefaultDescription;
private EditTextPreference sharingDefaultGreeting;
private TimeSpanPreference sharingDefaultExpiration;
@ -84,7 +88,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
private SharedPreferences settings;
private final Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private final Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
private final Lazy<PermissionUtil> permissionUtil = inject(PermissionUtil.class);
private final Lazy<ThemeChangedEventDistributor> themeChangedEventDistributor = inject(ThemeChangedEventDistributor.class);
@ -129,7 +132,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
sendBluetoothAlbumArt = findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART);
sendBluetoothNotifications = findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS);
viewRefresh = findPreference(Constants.PREFERENCES_KEY_VIEW_REFRESH);
imageLoaderConcurrency = findPreference(Constants.PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY);
sharingDefaultDescription = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION);
sharingDefaultGreeting = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_GREETING);
sharingDefaultExpiration = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION);
@ -188,8 +190,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
setMediaButtonsEnabled(sharedPreferences.getBoolean(key, true));
} else if (Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS.equals(key)) {
setBluetoothPreferences(sharedPreferences.getBoolean(key, true));
} else if (Constants.PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY.equals(key)) {
setImageLoaderConcurrency(Integer.parseInt(sharedPreferences.getString(key, "5")));
} else if (Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE.equals(key)) {
setDebugLogToFile(sharedPreferences.getBoolean(key, false));
} else if (Constants.PREFERENCES_KEY_ID3_TAGS.equals(key)) {
@ -359,21 +359,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
private void setupFeatureFlagsPreferences() {
final FeatureStorage featureStorage = KoinJavaComponent.get(FeatureStorage.class);
CheckBoxPreference ffImageLoader = (CheckBoxPreference) findPreference(
Constants.PREFERENCES_KEY_FF_IMAGE_LOADER);
if (ffImageLoader != null) {
ffImageLoader.setChecked(featureStorage.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER));
ffImageLoader.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object o) {
featureStorage.changeFeatureFlag(Feature.NEW_IMAGE_DOWNLOADER, (Boolean) o);
imageLoader.getValue().clearImageLoader();
return true;
}
});
}
CheckBoxPreference useFiveStarRating = (CheckBoxPreference) findPreference(
Constants.PREFERENCES_KEY_USE_FIVE_STAR_RATING);
@ -443,7 +428,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
chatRefreshInterval.setSummary(chatRefreshInterval.getEntry());
directoryCacheTime.setSummary(directoryCacheTime.getEntry());
viewRefresh.setSummary(viewRefresh.getEntry());
imageLoaderConcurrency.setSummary(imageLoaderConcurrency.getEntry());
sharingDefaultExpiration.setSummary(sharingDefaultExpiration.getText());
sharingDefaultDescription.setSummary(sharingDefaultDescription.getText());
sharingDefaultGreeting.setSummary(sharingDefaultGreeting.getText());
@ -470,14 +454,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
showArtistPicture.setEnabled(Util.getShouldUseId3Tags());
}
private void setImageLoaderConcurrency(int concurrency) {
ImageLoader imageLoaderInstance = imageLoader.getValue().getImageLoader();
if (imageLoaderInstance != null) {
imageLoaderInstance.stopImageLoader();
imageLoaderInstance.setConcurrency(concurrency);
}
}
private void setHideMedia(boolean hide) {
File nomediaDir = new File(FileUtil.getUltrasonicDirectory(), ".nomedia");

View File

@ -9,17 +9,18 @@ import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.Environment;
import timber.log.Timber;
import android.view.KeyEvent;
import android.widget.RemoteViews;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.NavigationActivity;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.imageloader.BitmapUtils;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FileUtil;
import timber.log.Timber;
/**
* Widget Provider for the Ultrasonic Widgets
@ -159,7 +160,7 @@ public class UltrasonicAppWidgetProvider extends AppWidgetProvider
// Set the cover art
try
{
Bitmap bitmap = currentSong == null ? null : FileUtil.getAlbumArtBitmap(currentSong, 240, true);
Bitmap bitmap = currentSong == null ? null : BitmapUtils.Companion.getAlbumArtBitmapFromDisk(currentSong, 240);
if (bitmap == null)
{

View File

@ -2,7 +2,6 @@ package org.moire.ultrasonic.util;
import android.os.AsyncTask;
import android.os.StatFs;
import timber.log.Timber;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.Playlist;
@ -19,6 +18,7 @@ import java.util.Set;
import java.util.SortedSet;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
@ -88,6 +88,7 @@ public class CacheCleaner
// No songs left in the folder
if (children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath()))
{
// Delete Artwork files
Util.delete(FileUtil.getAlbumArtFile(dir));
children = dir.listFiles();
}

View File

@ -19,18 +19,12 @@
package org.moire.ultrasonic.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import kotlin.Lazy;
import timber.log.Timber;
import org.moire.ultrasonic.app.UApp;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import java.io.File;
import java.io.FileInputStream;
@ -42,10 +36,14 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Pattern;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
/**
@ -59,8 +57,9 @@ public class FileUtil
private static final List<String> VIDEO_FILE_EXTENSIONS = Arrays.asList("flv", "mp4", "m4v", "wmv", "avi", "mov", "mpg", "mkv");
private static final List<String> PLAYLIST_FILE_EXTENSIONS = Collections.singletonList("m3u");
private static final Pattern TITLE_WITH_TRACK = Pattern.compile("^\\d\\d-.*");
public static final String SUFFIX_LARGE = ".jpeg";
public static final String SUFFIX_SMALL = ".jpeg-small";
private static final Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
private static final Lazy<PermissionUtil> permissionUtil = inject(PermissionUtil.class);
public static File getSongFile(MusicDirectory.Entry song)
@ -118,12 +117,49 @@ public class FileUtil
return playlistDir;
}
/**
* Get the album art file for a given album entry
* @param entry The album entry
* @return File object. Not guaranteed that it exists
*/
public static File getAlbumArtFile(MusicDirectory.Entry entry)
{
File albumDir = getAlbumDirectory(entry);
return getAlbumArtFile(albumDir);
}
/**
* Get the cache key for a given album entry
* @param entry The album entry
* @param large Whether to get the key for the large or the default image
* @return String The hash key
*/
public static String getAlbumArtKey(MusicDirectory.Entry entry, boolean large)
{
File albumDir = getAlbumDirectory(entry);
return getAlbumArtKey(albumDir, large);
}
/**
* Get the cache key for a given album entry
* @param albumDir The album directory
* @param large Whether to get the key for the large or the default image
* @return String The hash key
*/
public static String getAlbumArtKey(File albumDir, boolean large)
{
if (albumDir == null) {
return null;
}
String suffix = (large) ? SUFFIX_LARGE : SUFFIX_SMALL;
return String.format(Locale.ROOT, "%s%s", Util.md5Hex(albumDir.getPath()), suffix);
}
public static File getAvatarFile(String username)
{
File albumArtDir = getAlbumArtDirectory();
@ -134,173 +170,45 @@ public class FileUtil
}
String md5Hex = Util.md5Hex(username);
return new File(albumArtDir, String.format("%s.jpeg", md5Hex));
return new File(albumArtDir, String.format("%s%s", md5Hex, SUFFIX_LARGE));
}
/**
* Get the album art file for a given album directory
* @param albumDir The album directory
* @return File object. Not guaranteed that it exists
*/
public static File getAlbumArtFile(File albumDir)
{
File albumArtDir = getAlbumArtDirectory();
String key = getAlbumArtKey(albumDir, true);
if (albumArtDir == null || albumDir == null)
if (key == null || albumArtDir == null)
{
return null;
}
String md5Hex = Util.md5Hex(albumDir.getPath());
return new File(albumArtDir, String.format("%s.jpeg", md5Hex));
return new File(albumArtDir, key);
}
public static Bitmap getAvatarBitmap(String username, int size, boolean highQuality)
/**
* Get the album art file for a given cache key
* @param cacheKey The key (== the filename)
* @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);
Bitmap bitmap = null;
ImageLoader imageLoader = imageLoaderProvider.getValue().getImageLoader();
if (imageLoader != null)
if (albumArtDir == null || cacheKey == null)
{
bitmap = imageLoader.getImageBitmap(username, size);
return null;
}
if (bitmap != null)
{
return bitmap.copy(bitmap.getConfig(), false);
}
if (avatarFile != null && avatarFile.exists())
{
final BitmapFactory.Options opt = new BitmapFactory.Options();
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("getAvatarBitmap %s", String.valueOf(size));
if (bitmap != null)
{
if (imageLoader != null)
{
imageLoader.addImageToCache(bitmap, username, size);
}
}
return bitmap;
}
return null;
return new File(albumArtDir, cacheKey);
}
public static Bitmap getAlbumArtBitmap(MusicDirectory.Entry entry, int size, boolean highQuality)
{
if (entry == null) return null;
File albumArtFile = getAlbumArtFile(entry);
Bitmap bitmap = null;
ImageLoader imageLoader = imageLoaderProvider.getValue().getImageLoader();
if (imageLoader != null)
{
bitmap = imageLoader.getImageBitmap(entry, true, size);
}
if (bitmap != null)
{
return bitmap.copy(bitmap.getConfig(), false);
}
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("getAlbumArtBitmap %s", String.valueOf(size));
if (bitmap != null)
{
if (imageLoader != null)
{
imageLoader.addImageToCache(bitmap, entry, 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()
{

View File

@ -1,36 +0,0 @@
package org.moire.ultrasonic.util;
import android.graphics.Bitmap;
import android.view.View;
import org.moire.ultrasonic.domain.MusicDirectory;
public interface ImageLoader {
boolean isRunning();
void setConcurrency(int concurrency);
void startImageLoader();
void stopImageLoader();
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);
void cancel(String coverArt);
Bitmap getImageBitmap(String username, int size);
Bitmap getImageBitmap(MusicDirectory.Entry entry, boolean large, int size);
void addImageToCache(Bitmap bitmap, MusicDirectory.Entry entry, int size);
void addImageToCache(Bitmap bitmap, String username, int size);
void clear();
}

View File

@ -1,450 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.os.Handler;
import android.text.TextUtils;
import timber.log.Timber;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.content.res.ResourcesCompat;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Asynchronous loading of images, with caching.
* <p/>
* There should normally be only one instance of this class.
*
* @author Sindre Mehus
*/
public class LegacyImageLoader implements Runnable, ImageLoader {
private final LRUCache<String, Bitmap> cache = new LRUCache<>(150);
private final BlockingQueue<Task> queue;
private int imageSizeDefault;
private final int imageSizeLarge;
private Bitmap largeUnknownImage;
private Bitmap unknownAvatarImage;
private final Context context;
private Collection<Thread> threads;
private final AtomicBoolean running = new AtomicBoolean();
private int concurrency;
public LegacyImageLoader(
Context context,
int concurrency
) {
this.context = context;
this.concurrency = concurrency;
queue = new LinkedBlockingQueue<>(1000);
Drawable drawable = ResourcesCompat.getDrawable(context.getResources(), R.drawable.unknown_album, null);
// Determine the density-dependent image sizes.
if (drawable != null) {
imageSizeDefault = drawable.getIntrinsicHeight();
}
imageSizeLarge = Util.getMaxDisplayMetric();
createLargeUnknownImage(context);
createUnknownAvatarImage(context);
}
@Override
public synchronized boolean isRunning() {
return running.get() && !threads.isEmpty();
}
@Override
public void setConcurrency(int concurrency) {
this.concurrency = concurrency;
}
@Override
public void startImageLoader() {
running.set(true);
threads = Collections.synchronizedCollection(new ArrayList<Thread>(this.concurrency));
for (int i = 0; i < this.concurrency; i++) {
Thread thread = new Thread(this, String.format(Locale.US, "ImageLoader_%d", i));
threads.add(thread);
thread.start();
}
}
@Override
public synchronized void stopImageLoader() {
clear();
for (Thread thread : threads) {
thread.interrupt();
}
running.set(false);
threads.clear();
}
private void createLargeUnknownImage(Context context) {
Drawable drawable = ResourcesCompat.getDrawable(context.getResources(), R.drawable.unknown_album, null);
Timber.i("createLargeUnknownImage");
if (drawable != null) {
largeUnknownImage = Util.createBitmapFromDrawable(drawable);
}
}
private void createUnknownAvatarImage(Context context) {
Drawable contact = ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_contact_picture, null);
unknownAvatarImage = Util.createBitmapFromDrawable(contact);
}
@Override
public void loadAvatarImage(
View view,
String username,
boolean large,
int size,
boolean crossFade,
boolean highQuality
) {
view.invalidate();
if (username == null) {
setUnknownAvatarImage(view);
return;
}
if (size <= 0) {
size = large ? imageSizeLarge : imageSizeDefault;
}
Bitmap bitmap = cache.get(getKey(username, size));
if (bitmap != null) {
setAvatarImageBitmap(view, username, bitmap, crossFade);
return;
}
setUnknownAvatarImage(view);
queue.offer(new Task(view, username, size, large, crossFade, highQuality));
}
@Override
public void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size,
boolean crossFade, boolean highQuality) {
loadImage(view, entry, large, size, crossFade, highQuality, -1);
}
public void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size,
boolean crossFade, boolean highQuality, int defaultResourceId) {
view.invalidate();
if (entry == null) {
setUnknownImage(view, large, defaultResourceId);
return;
}
String coverArt = entry.getCoverArt();
if (TextUtils.isEmpty(coverArt)) {
setUnknownImage(view, large, defaultResourceId);
return;
}
if (size <= 0) {
size = large ? imageSizeLarge : imageSizeDefault;
}
Bitmap bitmap = cache.get(getKey(coverArt, size));
if (bitmap != null) {
setImageBitmap(view, entry, bitmap, crossFade);
return;
}
setUnknownImage(view, large, defaultResourceId);
queue.offer(new Task(view, entry, size, large, crossFade, highQuality));
}
public void cancel(String coverArt) {
for (Object taskObject : queue.toArray()) {
Task task = (Task)taskObject;
if ((task.entry.getCoverArt() != null) && (coverArt.compareTo(task.entry.getCoverArt()) == 0)) {
queue.remove(taskObject);
break;
}
}
}
private static String getKey(String coverArtId, int size) {
return String.format(Locale.US, "%s:%d", coverArtId, size);
}
@Override
public Bitmap getImageBitmap(String username, int size) {
Bitmap bitmap = cache.get(getKey(username, size));
if (bitmap != null && !bitmap.isRecycled()) {
Bitmap.Config config = bitmap.getConfig();
return bitmap.copy(config, false);
}
return null;
}
@Override
public Bitmap getImageBitmap(MusicDirectory.Entry entry, boolean large, int size) {
if (entry == null) {
return null;
}
String coverArt = entry.getCoverArt();
if (TextUtils.isEmpty(coverArt)) {
return null;
}
if (size <= 0) {
size = large ? imageSizeLarge : imageSizeDefault;
}
Bitmap bitmap = cache.get(getKey(coverArt, size));
if (bitmap != null && !bitmap.isRecycled()) {
Bitmap.Config config = bitmap.getConfig();
return bitmap.copy(config, false);
}
return null;
}
private void setImageBitmap(
View view,
MusicDirectory.Entry entry,
Bitmap bitmap,
boolean crossFade
) {
if (view instanceof ImageView) {
ImageView imageView = (ImageView) view;
MusicDirectory.Entry tagEntry = (MusicDirectory.Entry) view.getTag();
// Only apply image to the view if the view is intended for this entry
if (entry != null && tagEntry != null && !entry.equals(tagEntry)) {
Timber.i("View is no longer valid, not setting ImageBitmap");
return;
}
if (crossFade) {
Drawable existingDrawable = imageView.getDrawable();
Drawable newDrawable = Util.createDrawableFromBitmap(this.context, bitmap);
if (existingDrawable == null) {
Bitmap emptyImage = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
existingDrawable = new BitmapDrawable(context.getResources(), emptyImage);
}
Drawable[] layers = new Drawable[]{existingDrawable, newDrawable};
TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
imageView.setImageDrawable(transitionDrawable);
transitionDrawable.startTransition(250);
} else {
imageView.setImageBitmap(bitmap);
}
}
}
private void setAvatarImageBitmap(
View view,
String username,
Bitmap bitmap,
boolean crossFade
) {
if (view instanceof ImageView) {
ImageView imageView = (ImageView) view;
String tagEntry = (String) view.getTag();
// Only apply image to the view if the view is intended for this entry
if (username != null &&
tagEntry != null &&
!username.equals(tagEntry)) {
Timber.i("View is no longer valid, not setting ImageBitmap");
return;
}
if (crossFade) {
Drawable existingDrawable = imageView.getDrawable();
Drawable newDrawable = Util.createDrawableFromBitmap(this.context, bitmap);
if (existingDrawable == null) {
Bitmap emptyImage = Bitmap.createBitmap(
bitmap.getWidth(),
bitmap.getHeight(),
Bitmap.Config.ARGB_8888
);
existingDrawable = new BitmapDrawable(context.getResources(), emptyImage);
}
Drawable[] layers = new Drawable[]{existingDrawable, newDrawable};
TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
imageView.setImageDrawable(transitionDrawable);
transitionDrawable.startTransition(250);
} else {
imageView.setImageBitmap(bitmap);
}
}
}
private void setUnknownAvatarImage(View view) {
setAvatarImageBitmap(view, null, unknownAvatarImage, false);
}
private void setUnknownImage(View view, boolean large, int resId) {
if (resId == -1) resId = R.drawable.unknown_album;
if (large) {
setImageBitmap(view, null, largeUnknownImage, false);
} else {
if (view instanceof TextView) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(resId, 0, 0, 0);
} else if (view instanceof ImageView) {
((ImageView) view).setImageResource(resId);
}
}
}
@Override
public void addImageToCache(Bitmap bitmap, MusicDirectory.Entry entry, int size) {
cache.put(getKey(entry.getCoverArt(), size), bitmap);
}
@Override
public void addImageToCache(Bitmap bitmap, String username, int size) {
cache.put(getKey(username, size), bitmap);
}
@Override
public void clear() {
queue.clear();
}
@Override
public void run() {
while (running.get()) {
try {
Task task = queue.take();
task.execute();
} catch (InterruptedException ignored) {
running.set(false);
break;
} catch (Throwable x) {
Timber.e(x, "Unexpected exception in ImageLoader.");
}
}
}
private class Task {
private final View view;
private final MusicDirectory.Entry entry;
private final String username;
private final Handler handler;
private final int size;
private final boolean saveToFile;
private final boolean crossFade;
private final boolean highQuality;
Task(View view, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean crossFade, boolean highQuality) {
this.view = view;
this.entry = entry;
this.username = null;
this.size = size;
this.saveToFile = saveToFile;
this.crossFade = crossFade;
this.highQuality = highQuality;
handler = new Handler();
}
Task(View view, String username, int size, boolean saveToFile, boolean crossFade, boolean highQuality) {
this.view = view;
this.entry = null;
this.username = username;
this.size = size;
this.saveToFile = saveToFile;
this.crossFade = crossFade;
this.highQuality = highQuality;
handler = new Handler();
}
public void execute() {
try {
MusicService musicService = MusicServiceFactory.getMusicService();
final boolean isAvatar = this.username != null && this.entry == null;
final Bitmap bitmap = this.entry != null ?
musicService.getCoverArt(entry, size, saveToFile, highQuality) :
musicService.getAvatar(username, size, saveToFile, highQuality);
if (bitmap == null) {
Timber.d("Found empty album art.");
return;
}
if (isAvatar)
addImageToCache(bitmap, username, size);
else
addImageToCache(bitmap, entry, size);
handler.post(new Runnable() {
@Override
public void run() {
if (isAvatar) {
setAvatarImageBitmap(view, username, bitmap, crossFade);
} else {
setImageBitmap(view, entry, bitmap, crossFade);
}
}
});
} catch (Throwable x) {
Timber.e(x, "Failed to download album art.");
}
}
}
}

View File

@ -28,7 +28,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.ImageLoader;
import org.moire.ultrasonic.imageloader.ImageLoader;
import org.moire.ultrasonic.util.Util;
/**
@ -109,7 +109,7 @@ public class AlbumView extends UpdateView
public void setAlbum(final MusicDirectory.Entry album)
{
viewHolder.cover_art.setTag(album);
imageLoader.loadImage(viewHolder.cover_art, album, false, 0, false, true);
imageLoader.loadImage(viewHolder.cover_art, album, false, 0);
this.entry = album;
String title = album.getTitle();

View File

@ -1,6 +1,7 @@
package org.moire.ultrasonic.view;
import android.content.Context;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.LayoutInflater;
@ -12,8 +13,8 @@ import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.ChatMessage;
import org.moire.ultrasonic.imageloader.ImageLoader;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import org.moire.ultrasonic.util.ImageLoader;
import java.text.DateFormat;
import java.util.Date;
@ -33,7 +34,7 @@ public class ChatAdapter extends ArrayAdapter<ChatMessage>
private static final Pattern phoneMatcher = Pattern.compile(phoneRegex);
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private final Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
private final Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
public ChatAdapter(Context context, List<ChatMessage> messages)
{
@ -95,11 +96,11 @@ public class ChatAdapter extends ArrayAdapter<ChatMessage>
DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(context);
String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime));
ImageLoader imageLoaderInstance = imageLoader.getValue().getImageLoader();
ImageLoader imageLoader = imageLoaderProvider.getValue().getImageLoader();
if (imageLoaderInstance != null)
if (holder.avatar != null && !TextUtils.isEmpty(messageUser))
{
imageLoaderInstance.loadAvatarImage(holder.avatar, messageUser, false, holder.avatar.getWidth(), false, true);
imageLoader.loadAvatarImage(holder.avatar, messageUser);
}
holder.username.setText(messageUser);

View File

@ -28,7 +28,7 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.util.ImageLoader;
import org.moire.ultrasonic.imageloader.ImageLoader;
import java.util.List;

View File

@ -217,7 +217,6 @@ class NavigationActivity : AppCompatActivity() {
if (item.itemId == R.id.menu_exit) {
setResult(Constants.RESULT_CLOSE_ALL)
mediaPlayerController.stopJukeboxService()
imageLoaderProvider.getImageLoader().stopImageLoader()
finish()
exit()
}

View File

@ -12,6 +12,7 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
import org.moire.ultrasonic.cache.PermanentFileStorage
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.imageloader.ImageLoader
import org.moire.ultrasonic.log.TimberOkHttpLogger
import org.moire.ultrasonic.service.ApiCallResponseChecker
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.RESTMusicService
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.subsonic.VideoPlayer
import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader
import org.moire.ultrasonic.util.Constants
/**
@ -77,7 +78,7 @@ val musicServiceModule = module {
OfflineMusicService()
}
single { SubsonicImageLoader(androidContext(), get()) }
single { ImageLoader(androidContext(), get(), ImageLoaderProvider.config) }
single { DownloadHandler(get(), get()) }
single { NetworkAndStorageChecker(androidContext()) }

View File

@ -15,7 +15,7 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.ImageLoader
import org.moire.ultrasonic.imageloader.ImageLoader
/**
* Creates a Row in a RecyclerView which contains the details of an Album
@ -29,7 +29,6 @@ class AlbumRowAdapter(
) : GenericRowAdapter<MusicDirectory.Entry>(
onItemClick,
onContextMenuClick,
imageLoader,
onMusicFolderUpdate
) {
@ -56,9 +55,8 @@ class AlbumRowAdapter(
holder.coverArtId = entry.coverArt
imageLoader.loadImage(
holder.coverArt,
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
false, 0, false, true, R.drawable.unknown_album
holder.coverArt, entry,
false, 0, R.drawable.unknown_album
)
}
}

View File

@ -15,7 +15,7 @@ import java.text.Collator
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.ImageLoader
import org.moire.ultrasonic.imageloader.ImageLoader
import org.moire.ultrasonic.util.Util
/**
@ -30,7 +30,6 @@ class ArtistRowAdapter(
) : GenericRowAdapter<Artist>(
onItemClick,
onContextMenuClick,
imageLoader,
onMusicFolderUpdate
),
SectionedAdapter {
@ -62,8 +61,11 @@ class ArtistRowAdapter(
holder.coverArt.visibility = View.VISIBLE
imageLoader.loadImage(
holder.coverArt,
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
false, 0, false, true, R.drawable.ic_contact_picture
MusicDirectory.Entry("-1").apply {
coverArt = holder.coverArtId
artist = itemList[listPosition].name
},
false, 0, R.drawable.ic_contact_picture
)
} else {
holder.coverArt.visibility = View.GONE

View File

@ -20,7 +20,6 @@ import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.util.ImageLoader
import org.moire.ultrasonic.view.SelectMusicFolderView
/*
@ -29,7 +28,6 @@ import org.moire.ultrasonic.view.SelectMusicFolderView
abstract class GenericRowAdapter<T>(
val onItemClick: (T) -> Unit,
val onContextMenuClick: (MenuItem, T) -> Boolean,
private val imageLoader: ImageLoader,
private val onMusicFolderUpdate: (String?) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
open var itemList: List<T> = listOf()
@ -94,13 +92,6 @@ abstract class GenericRowAdapter<T>(
}
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if ((holder is ViewHolder) && (holder.coverArtId != null)) {
imageLoader.cancel(holder.coverArtId)
}
super.onViewRecycled(holder)
}
abstract override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
override fun getItemCount(): Int {

View File

@ -763,7 +763,7 @@ class TrackCollectionFragment : Fragment() {
val artworkSelection = random.nextInt(entries.size)
imageLoaderProvider.getImageLoader().loadImage(
coverArtView, entries[artworkSelection], false,
Util.getAlbumImageSize(context), false, true
Util.getAlbumImageSize(context)
)
val albumHeader = AlbumHeader.processEntries(context, entries)

View File

@ -1,4 +1,4 @@
package org.moire.ultrasonic.subsonic.loader.image
package org.moire.ultrasonic.imageloader
import com.squareup.picasso.Picasso
import com.squareup.picasso.Request
@ -15,9 +15,7 @@ class AvatarRequestHandler(
) : RequestHandler() {
override fun canHandleRequest(data: Request): Boolean {
return with(data.uri) {
scheme == SCHEME &&
authority == AUTHORITY &&
path == "/$AVATAR_PATH"
scheme == SCHEME && path == "/$AVATAR_PATH"
}
}

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

@ -0,0 +1,49 @@
package org.moire.ultrasonic.imageloader
import com.squareup.picasso.Picasso.LoadedFrom.DISK
import com.squareup.picasso.Picasso.LoadedFrom.NETWORK
import com.squareup.picasso.Request
import com.squareup.picasso.RequestHandler
import java.io.IOException
import okio.Okio
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.util.FileUtil.SUFFIX_LARGE
import org.moire.ultrasonic.util.FileUtil.SUFFIX_SMALL
/**
* Loads cover arts from subsonic api.
*/
class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : RequestHandler() {
override fun canHandleRequest(data: Request): Boolean {
return with(data.uri) {
scheme == SCHEME &&
path == "/$COVER_ART_PATH"
}
}
override fun load(request: Request, networkPolicy: Int): Result {
val id = request.uri.getQueryParameter(QUERY_ID)
?: throw IllegalArgumentException("Nullable id")
val size = request.uri.getQueryParameter(SIZE)?.toLong()
// Check if we have a hit in the disk cache
// Note: Currently we are only caching full size images on disk
// So we modify the key to query for the full size image,
// because scaling down a larger size image on the device is quicker than
// requesting the down-sized image from the network.
val key = request.stableKey!!.replace(SUFFIX_SMALL, SUFFIX_LARGE)
val cache = BitmapUtils.getAlbumArtBitmapFromDisk(key, size?.toInt())
if (cache != null) {
return Result(cache, DISK)
}
// Try to fetch the image from the API
val response = apiClient.getCoverArt(id, size)
if (!response.hasError() && response.stream != null) {
return Result(Okio.source(response.stream!!), NETWORK)
}
// Throw an error if still not successful
throw IOException("${response.apiError}")
}
}

View File

@ -0,0 +1,220 @@
package org.moire.ultrasonic.imageloader
import android.content.Context
import android.text.TextUtils
import android.view.View
import android.widget.ImageView
import com.squareup.picasso.Picasso
import com.squareup.picasso.RequestCreator
import java.io.File
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.domain.MusicDirectory
import org.moire.ultrasonic.service.RESTMusicService
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* Our new image loader which uses Picasso as a backend.
*/
class ImageLoader(
context: Context,
private val apiClient: SubsonicAPIClient,
private val config: ImageLoaderConfig
) {
private val picasso = Picasso.Builder(context)
.addRequestHandler(CoverArtRequestHandler(apiClient))
.addRequestHandler(AvatarRequestHandler(apiClient))
.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)
.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 requestedSize = resolveSize(size, large)
if (id != null && id.isNotEmpty() && view is ImageView) {
val key = FileUtil.getAlbumArtKey(entry, large)
val request = ImageRequest.CoverArt(
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.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)
val response = apiClient.getCoverArt(id!!, size.toLong())
RESTMusicService.checkStreamResponseError(response)
// Check for failure
if (response.stream == null) return
// Write Response stream to file
var inputStream: InputStream? = null
try {
inputStream = response.stream
val bytes = Util.toByteArray(inputStream)
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
}
}
}
/**
* 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,
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
*/
data class ImageLoaderConfig(
val largeSize: Int = 0,
val defaultSize: Int = 0,
val cacheFolder: File?
)

View File

@ -0,0 +1,29 @@
package org.moire.ultrasonic.imageloader
import android.net.Uri
internal const val SCHEME = "subsonic_api"
internal const val COVER_ART_PATH = "cover_art"
internal const val AVATAR_PATH = "avatar"
internal const val QUERY_ID = "id"
internal const val SIZE = "size"
internal const val QUERY_USERNAME = "username"
/**
* Picasso.load() only accepts an URI as parameter. Therefore we create a bogus URI, in which
* we encode the data that we need in the RequestHandler.
*/
internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri =
Uri.Builder()
.scheme(SCHEME)
.appendPath(COVER_ART_PATH)
.appendQueryParameter(QUERY_ID, entityId)
.appendQueryParameter(SIZE, size.toString())
.build()
internal fun createLoadAvatarRequest(username: String): Uri =
Uri.Builder()
.scheme(SCHEME)
.appendPath(AVATAR_PATH)
.appendQueryParameter(QUERY_USERNAME, username)
.build()

View File

@ -6,7 +6,6 @@
*/
package org.moire.ultrasonic.service
import android.graphics.Bitmap
import java.io.InputStream
import java.util.concurrent.TimeUnit
import org.koin.core.component.KoinComponent
@ -255,16 +254,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
@Throws(Exception::class)
override fun getStarred2(): SearchResult = musicService.getStarred2()
@Throws(Exception::class)
override fun getCoverArt(
entry: MusicDirectory.Entry?,
size: Int,
saveToFile: Boolean,
highQuality: Boolean
): Bitmap? {
return musicService.getCoverArt(entry, size, saveToFile, highQuality)
}
@Throws(Exception::class)
override fun getDownloadInputStream(
song: MusicDirectory.Entry,
@ -447,16 +436,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
musicService.updateShare(id, description, expires)
}
@Throws(Exception::class)
override fun getAvatar(
username: String?,
size: Int,
saveToFile: Boolean,
highQuality: Boolean
): Bitmap? {
return musicService.getAvatar(username, size, saveToFile, highQuality)
}
companion object {
private const val MUSIC_DIR_CACHE_SIZE = 100
}

View File

@ -24,6 +24,7 @@ import org.koin.core.component.inject
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.CacheCleaner
import org.moire.ultrasonic.util.CancellableTask
import org.moire.ultrasonic.util.FileUtil
@ -59,6 +60,7 @@ class DownloadFile(
private var completeWhenDone = false
private val downloader: Downloader by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject()
val progress: MutableLiveData<Int> = MutableLiveData(0)
@ -275,7 +277,7 @@ class DownloadFile(
if (isCancelled) {
throw Exception(String.format("Download of '%s' was cancelled", song))
}
downloadAndSaveCoverArt(musicService)
downloadAndSaveCoverArt()
}
if (isPlaying) {
@ -329,11 +331,11 @@ class DownloadFile(
return String.format("DownloadTask (%s)", song)
}
private fun downloadAndSaveCoverArt(musicService: MusicService) {
private fun downloadAndSaveCoverArt() {
try {
if (!TextUtils.isEmpty(song.coverArt)) {
val size = Util.getMinDisplayMetric()
musicService.getCoverArt(song, size, true, true)
// Download the largest size that we can display in the UI
imageLoaderProvider.getImageLoader().cacheCoverArt(song)
}
} catch (e: Exception) {
Timber.e(e, "Failed to get cover art.")

View File

@ -489,7 +489,7 @@ class MediaPlayerController(
val currentPlayingNumberOnPlaylist: Int
get() = downloader.currentPlayingIndex
val currentDownloading: DownloadFile
val currentDownloading: DownloadFile?
get() = downloader.currentDownloading
val playList: List<DownloadFile>

View File

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

View File

@ -6,7 +6,6 @@
*/
package org.moire.ultrasonic.service
import android.graphics.Bitmap
import java.io.InputStream
import org.moire.ultrasonic.domain.Bookmark
import org.moire.ultrasonic.domain.ChatMessage
@ -111,17 +110,6 @@ interface MusicService {
@Throws(Exception::class)
fun getStarred2(): SearchResult
@Throws(Exception::class)
fun getCoverArt(
entry: MusicDirectory.Entry?,
size: Int,
saveToFile: Boolean,
highQuality: Boolean
): Bitmap?
@Throws(Exception::class)
fun getAvatar(username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean): Bitmap?
/**
* Return response [InputStream] and a [Boolean] that indicates if this response is
* partial.

View File

@ -6,7 +6,6 @@
*/
package org.moire.ultrasonic.service
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import java.io.BufferedReader
import java.io.BufferedWriter
@ -118,34 +117,6 @@ class OfflineMusicService : MusicService, KoinComponent {
return result
}
override fun getAvatar(
username: String?,
size: Int,
saveToFile: Boolean,
highQuality: Boolean
): Bitmap? {
return try {
val bitmap = FileUtil.getAvatarBitmap(username, size, highQuality)
Util.scaleBitmap(bitmap, size)
} catch (ignored: Exception) {
null
}
}
override fun getCoverArt(
entry: MusicDirectory.Entry?,
size: Int,
saveToFile: Boolean,
highQuality: Boolean
): Bitmap? {
return try {
val bitmap = FileUtil.getAlbumArtBitmap(entry, size, highQuality)
Util.scaleBitmap(bitmap, size)
} catch (ignored: Exception) {
null
}
}
override fun search(criteria: SearchCriteria): SearchResult {
val artists: MutableList<Artist> = ArrayList()
val albums: MutableList<MusicDirectory.Entry> = ArrayList()

View File

@ -6,15 +6,11 @@
*/
package org.moire.ultrasonic.service
import android.graphics.Bitmap
import android.text.TextUtils
import java.io.BufferedWriter
import java.io.File
import java.io.FileOutputStream
import java.io.FileWriter
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
@ -27,7 +23,6 @@ import org.moire.ultrasonic.cache.serializers.getIndexesSerializer
import org.moire.ultrasonic.cache.serializers.getMusicFolderListSerializer
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isServerScalingEnabled
import org.moire.ultrasonic.domain.Bookmark
import org.moire.ultrasonic.domain.ChatMessage
import org.moire.ultrasonic.domain.Genre
@ -488,83 +483,6 @@ open class RESTMusicService(
return response.body()!!.starred2.toDomainEntity()
}
@Throws(Exception::class)
override fun getCoverArt(
entry: MusicDirectory.Entry?,
size: Int,
saveToFile: Boolean,
highQuality: Boolean
): Bitmap? {
// Synchronize on the entry so that we don't download concurrently for
// the same song.
if (entry == null) {
return null
}
synchronized(entry) {
// Use cached file, if existing.
var bitmap = FileUtil.getAlbumArtBitmap(entry, size, highQuality)
val serverScaling = isServerScalingEnabled()
if (bitmap == null) {
Timber.d("Loading cover art for: %s", entry)
val id = entry.coverArt
if (TextUtils.isEmpty(id)) {
return null // Can't load
}
val response = subsonicAPIClient.getCoverArt(id!!, size.toLong())
checkStreamResponseError(response)
if (response.stream == null) {
return null // Failed to load
}
var inputStream: InputStream? = null
try {
inputStream = response.stream
val bytes = Util.toByteArray(inputStream)
// If we aren't allowing server-side scaling, always save the file to disk
// because it will be unmodified
if (!serverScaling || saveToFile) {
var outputStream: OutputStream? = null
try {
outputStream = FileOutputStream(
FileUtil.getAlbumArtFile(entry)
)
outputStream.write(bytes)
} finally {
Util.close(outputStream)
}
}
bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality)
} finally {
Util.close(inputStream)
}
}
// Return scaled bitmap
return Util.scaleBitmap(bitmap, size)
}
}
@Throws(SubsonicRESTException::class, IOException::class)
private fun checkStreamResponseError(response: StreamResponse) {
if (response.hasError() || response.stream == null) {
if (response.apiError != null) {
throw SubsonicRESTException(response.apiError!!)
} else {
throw IOException(
"Failed to make endpoint request, code: " + response.responseHttpCode
)
}
}
}
@Throws(Exception::class)
override fun getDownloadInputStream(
song: MusicDirectory.Entry,
@ -813,62 +731,23 @@ open class RESTMusicService(
}
}
@Throws(Exception::class)
override fun getAvatar(
username: String?,
size: Int,
saveToFile: Boolean,
highQuality: Boolean
): Bitmap? {
// Synchronize on the username so that we don't download concurrently for
// the same user.
if (username == null) {
return null
}
synchronized(username) {
// Use cached file, if existing.
var bitmap = FileUtil.getAvatarBitmap(username, size, highQuality)
if (bitmap == null) {
var inputStream: InputStream? = null
try {
val response = subsonicAPIClient.getAvatar(username)
if (response.hasError()) return null
inputStream = response.stream
val bytes = Util.toByteArray(inputStream)
// If we aren't allowing server-side scaling, always save the file to disk
// because it will be unmodified
if (saveToFile) {
var outputStream: OutputStream? = null
try {
outputStream = FileOutputStream(
FileUtil.getAvatarFile(username)
)
outputStream.write(bytes)
} finally {
Util.close(outputStream)
}
}
bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality)
} finally {
Util.close(inputStream)
}
}
// Return scaled bitmap
return Util.scaleBitmap(bitmap, size)
}
}
companion object {
private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
private const val INDEXES_STORAGE_NAME = "indexes"
private const val ARTISTS_STORAGE_NAME = "artists"
// TODO: Move to response checker
@Throws(SubsonicRESTException::class, IOException::class)
fun checkStreamResponseError(response: StreamResponse) {
if (response.hasError() || response.stream == null) {
if (response.apiError != null) {
throw SubsonicRESTException(response.apiError!!)
} else {
throw IOException(
"Failed to make endpoint request, code: " + response.responseHttpCode
)
}
}
}
}
}

View File

@ -1,12 +1,15 @@
package org.moire.ultrasonic.subsonic
import android.content.Context
import androidx.core.content.res.ResourcesCompat
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.moire.ultrasonic.featureflags.Feature
import org.moire.ultrasonic.featureflags.FeatureStorage
import org.moire.ultrasonic.util.ImageLoader
import org.moire.ultrasonic.util.LegacyImageLoader
import org.koin.core.qualifier.named
import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.imageloader.ImageLoader
import org.moire.ultrasonic.imageloader.ImageLoaderConfig
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util
/**
@ -14,37 +17,42 @@ import org.moire.ultrasonic.util.Util
*/
class ImageLoaderProvider(val context: Context) : KoinComponent {
private var imageLoader: ImageLoader? = null
private var serverID: String = get(named("ServerID"))
@Synchronized
fun clearImageLoader() {
if (
imageLoader != null &&
imageLoader!!.isRunning
) {
imageLoader!!.clear()
}
imageLoader = null
}
@Synchronized
fun getImageLoader(): ImageLoader {
if (imageLoader == null || !imageLoader!!.isRunning) {
val legacyImageLoader = LegacyImageLoader(
context,
Util.getImageLoaderConcurrency()
)
val features: FeatureStorage = get()
val isNewImageLoaderEnabled = features.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER)
imageLoader = if (isNewImageLoaderEnabled) {
SubsonicImageLoaderProxy(
legacyImageLoader,
get()
)
} else {
legacyImageLoader
}
imageLoader!!.startImageLoader()
// We need to generate a new ImageLoader if the server has changed...
val currentID = get<String>(named("ServerID"))
if (imageLoader == null || currentID != serverID) {
imageLoader = get()
serverID = currentID
}
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

@ -1,80 +0,0 @@
package org.moire.ultrasonic.subsonic
import android.view.View
import android.widget.ImageView
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.subsonic.loader.image.ImageRequest
import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader
import org.moire.ultrasonic.util.ImageLoader
import org.moire.ultrasonic.util.LegacyImageLoader
/**
* Temporary proxy between new [SubsonicImageLoader] and [ImageLoader] interface and old
* [LegacyImageLoader] implementation.
*
* Should be removed on [LegacyImageLoader] removal.
*/
class SubsonicImageLoaderProxy(
legacyImageLoader: LegacyImageLoader,
private val subsonicImageLoader: SubsonicImageLoader
) : ImageLoader by legacyImageLoader {
override fun loadImage(
view: View?,
entry: MusicDirectory.Entry?,
large: Boolean,
size: Int,
crossFade: Boolean,
highQuality: Boolean
) {
return loadImage(view, entry, large, size, crossFade, highQuality, -1)
}
override fun loadImage(
view: View?,
entry: MusicDirectory.Entry?,
large: Boolean,
size: Int,
crossFade: Boolean,
highQuality: Boolean,
defaultResourceId: Int
) {
val id = entry?.coverArt
val unknownImageId =
if (defaultResourceId == -1) R.drawable.unknown_album
else defaultResourceId
if (id != null &&
view != null &&
view is ImageView
) {
val request = ImageRequest.CoverArt(
id, view,
placeHolderDrawableRes = unknownImageId,
errorDrawableRes = unknownImageId
)
subsonicImageLoader.load(request)
}
}
override fun loadAvatarImage(
view: View?,
username: String?,
large: Boolean,
size: Int,
crossFade: Boolean,
highQuality: Boolean
) {
if (username != null &&
view != null &&
view is ImageView
) {
val request = ImageRequest.Avatar(
username, view,
placeHolderDrawableRes = R.drawable.ic_contact_picture,
errorDrawableRes = R.drawable.ic_contact_picture
)
subsonicImageLoader.load(request)
}
}
}

View File

@ -379,19 +379,6 @@
<string name="settings.show_all_songs_by_artist">Zobrazit všechny skladby umělce</string>
<string name="settings.show_all_songs_by_artist_summary">Přidat nový zápis v náhledu umělců pro přístup ke všem skladbám umělce</string>
<string name="download.menu_show_artist">Zobrazit umělce</string>
<string name="settings.image_loader_concurrency">Počet vláken stahování obrázků</string>
<string name="settings.image_loader_concurrency_1">1</string>
<string name="settings.image_loader_concurrency_2">2</string>
<string name="settings.image_loader_concurrency_3">3</string>
<string name="settings.image_loader_concurrency_4">4</string>
<string name="settings.image_loader_concurrency_5">5</string>
<string name="settings.image_loader_concurrency_6">6</string>
<string name="settings.image_loader_concurrency_7">7</string>
<string name="settings.image_loader_concurrency_8">8</string>
<string name="settings.image_loader_concurrency_9">9</string>
<string name="settings.image_loader_concurrency_10">10</string>
<string name="settings.image_loader_concurrency_11">11</string>
<string name="settings.image_loader_concurrency_12">12</string>
<string name="albumArt">albumArt</string>
<string name="common_multiple_years">Vícenásobné roky</string>
<string name="settings.playback.resume_on_bluetooth_device">Pokračovat v přehrávání po připojení bluetooth přístroje</string>
@ -501,10 +488,6 @@
<!-- Subsonic feature flags -->
<string name="feature_flags_category_title">Příznaky funkcí</string>
<string name="feature_flags_image_loader_title">Povolit nové načítání obrázků</string>
<string name="feature_flags_image_loader_description">Zapne novou implementaci načítání obrázků.
Toto aktuálně neukládá obrázky dlouhodobě, ale používá pouze odkládání do paměti.
</string>
<string name="feature_flags_five_star_rating_title">Používat pět hvězdiček pro hodnocení skladeb</string>
<string name="feature_flags_five_star_rating_description">Používat pět hvězdiček pro hodnocení skladeb
namísto jednoduchého jednohvězdičkového hodnocení.

View File

@ -376,19 +376,6 @@
<string name="settings.show_all_songs_by_artist">Alle Titel nach Künstler sortieren</string>
<string name="settings.show_all_songs_by_artist_summary">Einen neuen Eintrag in der Künstleransicht hinzufügen, um auf alle Lieder eines Künstlers zuzugreifen</string>
<string name="download.menu_show_artist">Künstler zeigen</string>
<string name="settings.image_loader_concurrency">Paralleles laden von Bildern</string>
<string name="settings.image_loader_concurrency_1">1</string>
<string name="settings.image_loader_concurrency_2">2</string>
<string name="settings.image_loader_concurrency_3">3</string>
<string name="settings.image_loader_concurrency_4">4</string>
<string name="settings.image_loader_concurrency_5">5</string>
<string name="settings.image_loader_concurrency_6">6</string>
<string name="settings.image_loader_concurrency_7">7</string>
<string name="settings.image_loader_concurrency_8">8</string>
<string name="settings.image_loader_concurrency_9">9</string>
<string name="settings.image_loader_concurrency_10">10</string>
<string name="settings.image_loader_concurrency_11">11</string>
<string name="settings.image_loader_concurrency_12">12</string>
<string name="common_multiple_years">Mehrere Jahre</string>
<string name="server_editor.new_label">Server hinzufügen</string>
<plurals name="select_album_n_songs">
@ -434,8 +421,6 @@
<!-- Subsonic feature flags -->
<string name="feature_flags_category_title">Funktionseinstellungem</string>
<string name="feature_flags_image_loader_title">Neuen Bild-Lader aktivieren</string>
<string name="feature_flags_image_loader_description">Neuen Bild-Lader aktivieren. Bilder werden derzeit nur im Chache gespeichert.</string>
<string name="feature_flags_five_star_rating_title">Verwenden Sie Fünf-Sterne-Bewertung für Songs</string>
<string name="feature_flags_five_star_rating_description">Verwenden Sie Fünf-Sterne-Bewertungssystem für Songs
         anstatt einfach Elemente zu markieren / zu entfernen.

View File

@ -393,19 +393,6 @@
<string name="settings.show_all_songs_by_artist">Mostrar todas las canciones por artista</string>
<string name="settings.show_all_songs_by_artist_summary">Añadir nueva entrada en la vista de artista para acceder a todas las canciones de un artista</string>
<string name="download.menu_show_artist">Mostrar artista</string>
<string name="settings.image_loader_concurrency">Concurrencia del cargador de imágenes</string>
<string name="settings.image_loader_concurrency_1">1</string>
<string name="settings.image_loader_concurrency_2">2</string>
<string name="settings.image_loader_concurrency_3">3</string>
<string name="settings.image_loader_concurrency_4">4</string>
<string name="settings.image_loader_concurrency_5">5</string>
<string name="settings.image_loader_concurrency_6">6</string>
<string name="settings.image_loader_concurrency_7">7</string>
<string name="settings.image_loader_concurrency_8">8</string>
<string name="settings.image_loader_concurrency_9">9</string>
<string name="settings.image_loader_concurrency_10">10</string>
<string name="settings.image_loader_concurrency_11">11</string>
<string name="settings.image_loader_concurrency_12">12</string>
<string name="albumArt">Caratula del Álbum</string>
<string name="common_multiple_years">Múltiples años</string>
<string name="settings.playback.resume_on_bluetooth_device">Reanudar al conectar un dispositivo Bluetooth</string>
@ -503,10 +490,6 @@
<!-- Subsonic feature flags -->
<string name="feature_flags_category_title">Funciones experimentales</string>
<string name="feature_flags_image_loader_title">Habilitar nuevo cargador de imágenes</string>
<string name="feature_flags_image_loader_description">Permite la implementación de un nuevo cargador de imágenes.
Actualmente no guarda la imagen en el almacenamiento del dispositivo y sólo utiliza caché en la memoria.
</string>
<string name="feature_flags_five_star_rating_title">Use cinco estrellas para las canciones</string>
<string name="feature_flags_five_star_rating_description">Utilice el sistema de calificación de cinco estrellas para canciones
en lugar de simplemente destacar / desestimar elementos.

View File

@ -381,19 +381,6 @@
<string name="settings.show_all_songs_by_artist">Voir tous les titres par artiste</string>
<string name="settings.show_all_songs_by_artist_summary">Ajouter une nouvelle entrée dans la vue artiste pour accéder à toutes les titres d\'un artiste</string>
<string name="download.menu_show_artist">Afficher l\'artiste</string>
<string name="settings.image_loader_concurrency">Chargements dimages simultanés</string>
<string name="settings.image_loader_concurrency_1">1</string>
<string name="settings.image_loader_concurrency_2">2</string>
<string name="settings.image_loader_concurrency_3">3</string>
<string name="settings.image_loader_concurrency_4">4</string>
<string name="settings.image_loader_concurrency_5">5</string>
<string name="settings.image_loader_concurrency_6">6</string>
<string name="settings.image_loader_concurrency_7">7</string>
<string name="settings.image_loader_concurrency_8">8</string>
<string name="settings.image_loader_concurrency_9">9</string>
<string name="settings.image_loader_concurrency_10">10</string>
<string name="settings.image_loader_concurrency_11">11</string>
<string name="settings.image_loader_concurrency_12">12</string>
<string name="albumArt">Pochette d\'album</string>
<string name="common_multiple_years">Années multiples</string>
<string name="settings.playback.resume_on_bluetooth_device">Reprendre lorsquun appareil Bluetooth se connecte</string>
@ -491,10 +478,6 @@
<!-- Subsonic feature flags -->
<string name="feature_flags_category_title">Drapeaux des fonctionnalités</string>
<string name="feature_flags_image_loader_title">Activer le nouveau chargeur d\'images</string>
<string name="feature_flags_image_loader_description">Permet l\'implémentation d\'un nouveau chargeur d\'images.
Actuellement, il n\'enregistre pas l\'image dans l\'appareil et n\'utilise que le cache en mémoire.
</string>
<string name="feature_flags_five_star_rating_title">Utiliser les étoiles pour noter les morceaux</string>
<string name="feature_flags_five_star_rating_description">Utiliser un système de notation à base d\'étoiles pour les morceaux
au lieu de simplement mettre en avant les morceaux.

View File

@ -393,19 +393,6 @@
<string name="settings.show_all_songs_by_artist">Az előadó összes dalának megjelenítése</string>
<string name="settings.show_all_songs_by_artist_summary">Új bejegyzés hozzáadása az előadóhoz, az előadó összes dalának eléréséhez.</string>
<string name="download.menu_show_artist">Ugrás az előadóhoz</string>
<string name="settings.image_loader_concurrency">Image Loader Concurrency</string>
<string name="settings.image_loader_concurrency_1">1</string>
<string name="settings.image_loader_concurrency_2">2</string>
<string name="settings.image_loader_concurrency_3">3</string>
<string name="settings.image_loader_concurrency_4">4</string>
<string name="settings.image_loader_concurrency_5">5</string>
<string name="settings.image_loader_concurrency_6">6</string>
<string name="settings.image_loader_concurrency_7">7</string>
<string name="settings.image_loader_concurrency_8">8</string>
<string name="settings.image_loader_concurrency_9">9</string>
<string name="settings.image_loader_concurrency_10">10</string>
<string name="settings.image_loader_concurrency_11">11</string>
<string name="settings.image_loader_concurrency_12">12</string>
<string name="albumArt">albumArt</string>
<string name="common_multiple_years">Több év</string>
<string name="settings.playback.resume_on_bluetooth_device">Folytatás Bluetooth eszköz csatlakozásakor</string>
@ -501,10 +488,6 @@
<!-- Subsonic feature flags -->
<string name="feature_flags_category_title">Jellemzők Zászlók</string>
<string name="feature_flags_image_loader_title">Engedélyezzen új képbetöltőt</string>
<string name="feature_flags_image_loader_description">Engedélyezi az új képbetöltő megvalósítását. Jelenleg nem
tárolja a képet az eszköz tárolójában, és csak a memóriában tárolja a gyorsítótárat.
</string>
<string name="feature_flags_five_star_rating_title">Öt csillagos értékelés használata a dalokhoz</string>
<string name="feature_flags_five_star_rating_description">Öt csillag használata az értékeléshez az egyszerű
csillaggal jelölés helyett.

View File

@ -338,18 +338,6 @@
<string name="share_via">Condividi canzoni via</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Predefinito</string>
<string name="settings.image_loader_concurrency_1">1</string>
<string name="settings.image_loader_concurrency_2">2</string>
<string name="settings.image_loader_concurrency_3">3</string>
<string name="settings.image_loader_concurrency_4">4</string>
<string name="settings.image_loader_concurrency_5">5</string>
<string name="settings.image_loader_concurrency_6">6</string>
<string name="settings.image_loader_concurrency_7">7</string>
<string name="settings.image_loader_concurrency_8">8</string>
<string name="settings.image_loader_concurrency_9">9</string>
<string name="settings.image_loader_concurrency_10">10</string>
<string name="settings.image_loader_concurrency_11">11</string>
<string name="settings.image_loader_concurrency_12">12</string>
<plurals name="select_album_n_songs">
<item quantity="one">1 canzone</item>
<item quantity="other">%d canzoni</item>

View File

@ -393,19 +393,6 @@
<string name="settings.show_all_songs_by_artist">Alle nummers van artiest tonen</string>
<string name="settings.show_all_songs_by_artist_summary">Item toevoegen in artiestweergave om alle nummers van een artiest te bekijken</string>
<string name="download.menu_show_artist">Artiest tonen</string>
<string name="settings.image_loader_concurrency">Aantal tegelijkertijd te laden afbeeldingen</string>
<string name="settings.image_loader_concurrency_1">1</string>
<string name="settings.image_loader_concurrency_2">2</string>
<string name="settings.image_loader_concurrency_3">3</string>
<string name="settings.image_loader_concurrency_4">4</string>
<string name="settings.image_loader_concurrency_5">5</string>
<string name="settings.image_loader_concurrency_6">6</string>
<string name="settings.image_loader_concurrency_7">7</string>
<string name="settings.image_loader_concurrency_8">8</string>
<string name="settings.image_loader_concurrency_9">9</string>
<string name="settings.image_loader_concurrency_10">10</string>
<string name="settings.image_loader_concurrency_11">11</string>
<string name="settings.image_loader_concurrency_12">12</string>
<string name="albumArt">Albumhoes</string>
<string name="common_multiple_years">Meerdere jaren</string>
<string name="settings.playback.resume_on_bluetooth_device">Hervatten bij verbinding met bluetoothapparaat</string>
@ -503,10 +490,6 @@
<!-- Subsonic feature flags -->
<string name="feature_flags_category_title">Experimentele functies</string>
<string name="feature_flags_image_loader_title">Nieuwe manier van afbeeldingen laden inschakelen</string>
<string name="feature_flags_image_loader_description">Schakelt de nieuwe methode voor het laden van afbeeldingen in.
Momenteel slaat het geen afbeeldingen op op de apparaatopslag en wordt alleen geheugencache gebruikt.
</string>
<string name="feature_flags_five_star_rating_title">Gebruik vijf sterren voor nummers</string>
<string name="feature_flags_five_star_rating_description">Gebruik vijf sterren ratingsysteem voor liedjes
in plaats van items simpelweg in de hoofdrol te zetten / niet te verwijderen.

View File

@ -376,19 +376,6 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników
<string name="settings.show_all_songs_by_artist">Wyświetlaj wszystkie utwory artysty</string>
<string name="settings.show_all_songs_by_artist_summary">Dodaje nową pozycję w widoku artysty z wszystkimi jego utworami</string>
<string name="download.menu_show_artist">Wyświetlaj artystę</string>
<string name="settings.image_loader_concurrency">Ilość jednocześnie ładowanych obrazów</string>
<string name="settings.image_loader_concurrency_1">1</string>
<string name="settings.image_loader_concurrency_2">2</string>
<string name="settings.image_loader_concurrency_3">3</string>
<string name="settings.image_loader_concurrency_4">4</string>
<string name="settings.image_loader_concurrency_5">5</string>
<string name="settings.image_loader_concurrency_6">6</string>
<string name="settings.image_loader_concurrency_7">7</string>
<string name="settings.image_loader_concurrency_8">8</string>
<string name="settings.image_loader_concurrency_9">9</string>
<string name="settings.image_loader_concurrency_10">10</string>
<string name="settings.image_loader_concurrency_11">11</string>
<string name="settings.image_loader_concurrency_12">12</string>
<string name="albumArt">Okładka</string>
<string name="common_multiple_years">Z różnych lat</string>
<string name="server_editor.new_label">Dodaj serwer</string>
@ -449,9 +436,6 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników
<!-- Subsonic feature flags -->
<string name="feature_flags_category_title">Flagi funkcji</string>
<string name="feature_flags_image_loader_title">Włącz program ładujący nowe obrazy</string>
<string name="feature_flags_image_loader_description">Włącza implementację modułu ładującego nowe obrazy.
Obecnie nie zapisuje obrazów w pamięci urządzenia, tylko wykorzystuje tylko pamięć podręczną.</string>
<string name="feature_flags_five_star_rating_title">Użyj pięciu gwiazdek dla utworów</string>
<string name="feature_flags_five_star_rating_description">W przypadku utworów użyj systemu pięciu gwiazdek
zamiast po prostu grać gwiazdkami / bez gwiazd.

View File

@ -381,19 +381,6 @@
<string name="settings.show_all_songs_by_artist">Mostrar Todas as Músicas por Artista</string>
<string name="settings.show_all_songs_by_artist_summary">Adicionar nova entrada em artista para acessar todas as músicas do artista</string>
<string name="download.menu_show_artist">Mostrar Artista</string>
<string name="settings.image_loader_concurrency">Concorrência ao Carregar Imagens</string>
<string name="settings.image_loader_concurrency_1">1</string>
<string name="settings.image_loader_concurrency_2">2</string>
<string name="settings.image_loader_concurrency_3">3</string>
<string name="settings.image_loader_concurrency_4">4</string>
<string name="settings.image_loader_concurrency_5">5</string>
<string name="settings.image_loader_concurrency_6">6</string>
<string name="settings.image_loader_concurrency_7">7</string>
<string name="settings.image_loader_concurrency_8">8</string>
<string name="settings.image_loader_concurrency_9">9</string>
<string name="settings.image_loader_concurrency_10">10</string>
<string name="settings.image_loader_concurrency_11">11</string>
<string name="settings.image_loader_concurrency_12">12</string>
<string name="albumArt">albumArt</string>
<string name="common_multiple_years">Anos Múltiplos</string>
<string name="settings.playback.resume_on_bluetooth_device">Retomar ao Conectar Dispositivo Bluetooth</string>
@ -489,10 +476,6 @@
<!-- Subsonic feature flags -->
<string name="feature_flags_category_title">Bandeiras de Recursos</string>
<string name="feature_flags_image_loader_title">Ativar Novo Carregador de Imagens</string>
<string name="feature_flags_image_loader_description">Permite nova implementação do carregador de imagens.
Atualmente, ele não salva a imagem no armazenamento do dispositivo e usa apenas o cache na memória.
</string>
<string name="feature_flags_five_star_rating_title">Usar Classif. de 5 estrelas Para Músicas</string>
<string name="feature_flags_five_star_rating_description">Use o sistema de classificação de 5 estrelas para músicas
em vez de simplesmente estrelar/não estrelar itens.

View File

@ -376,19 +376,6 @@
<string name="settings.show_all_songs_by_artist">Todas as Músicas do Artista</string>
<string name="settings.show_all_songs_by_artist_summary">Adicionar nova entrada em artista para ver todas as músicas do artista</string>
<string name="download.menu_show_artist">Mostrar Artista</string>
<string name="settings.image_loader_concurrency">Concorrência ao Carregar Imagens</string>
<string name="settings.image_loader_concurrency_1">1</string>
<string name="settings.image_loader_concurrency_2">2</string>
<string name="settings.image_loader_concurrency_3">3</string>
<string name="settings.image_loader_concurrency_4">4</string>
<string name="settings.image_loader_concurrency_5">5</string>
<string name="settings.image_loader_concurrency_6">6</string>
<string name="settings.image_loader_concurrency_7">7</string>
<string name="settings.image_loader_concurrency_8">8</string>
<string name="settings.image_loader_concurrency_9">9</string>
<string name="settings.image_loader_concurrency_10">10</string>
<string name="settings.image_loader_concurrency_11">11</string>
<string name="settings.image_loader_concurrency_12">12</string>
<string name="common_multiple_years">Múltiplos Anos</string>
<string name="server_editor.new_label">Adicionar Servidor</string>
<plurals name="select_album_n_songs">
@ -434,10 +421,6 @@
<!-- Subsonic feature flags -->
<string name="feature_flags_category_title">Bandeiras de recursos</string>
<string name="feature_flags_image_loader_title">Ativar novo carregador de imagens</string>
<string name="feature_flags_image_loader_description">Permite nova implementação do carregador de imagens.
Atualmente, ele não salva a imagem no armazenamento do dispositivo e usa apenas o cache na memória.
</string>
<string name="feature_flags_five_star_rating_title">Use classificação de cinco estrelas para músicas</string>
<string name="feature_flags_five_star_rating_description">Use o sistema de classificação de cinco estrelas para músicas
em vez de simplesmente estrelar / não estrelar itens.

View File

@ -368,19 +368,6 @@
<string name="settings.show_all_songs_by_artist">Показать все треки исполнителя</string>
<string name="settings.show_all_songs_by_artist_summary">Добавить новую запись в представлении исполнителя, чтобы получить доступ ко всем песням для исполнителя</string>
<string name="download.menu_show_artist">Показать исполнителей</string>
<string name="settings.image_loader_concurrency">Загрузчик совпадающих изображений</string>
<string name="settings.image_loader_concurrency_1">1</string>
<string name="settings.image_loader_concurrency_2">2</string>
<string name="settings.image_loader_concurrency_3">3</string>
<string name="settings.image_loader_concurrency_4">4</string>
<string name="settings.image_loader_concurrency_5">5</string>
<string name="settings.image_loader_concurrency_6">6</string>
<string name="settings.image_loader_concurrency_7">7</string>
<string name="settings.image_loader_concurrency_8">8</string>
<string name="settings.image_loader_concurrency_9">9</string>
<string name="settings.image_loader_concurrency_10">10</string>
<string name="settings.image_loader_concurrency_11">11</string>
<string name="settings.image_loader_concurrency_12">12</string>
<string name="albumArt">albumArt</string>
<string name="common_multiple_years">Несколько лет</string>
<!-- Subsonic api errors -->
@ -396,7 +383,4 @@
<!-- Subsonic feature flags -->
<string name="feature_flags_category_title">Флаги</string>
<string name="feature_flags_image_loader_title">Включить новый загрузчик изображений</string>
<string name="feature_flags_image_loader_description">Включает новую реализацию загрузчика изображений.
В настоящее время он не сохраняет изображение в памяти устройства и использует только кэш в памяти.</string>
</resources>

View File

@ -264,18 +264,6 @@
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">默认</string>
<string name="menu.share">分享</string>
<string name="settings.image_loader_concurrency_1">1</string>
<string name="settings.image_loader_concurrency_2">2</string>
<string name="settings.image_loader_concurrency_3">3</string>
<string name="settings.image_loader_concurrency_4">4</string>
<string name="settings.image_loader_concurrency_5">5</string>
<string name="settings.image_loader_concurrency_6">6</string>
<string name="settings.image_loader_concurrency_7">7</string>
<string name="settings.image_loader_concurrency_8">8</string>
<string name="settings.image_loader_concurrency_9">9</string>
<string name="settings.image_loader_concurrency_10">10</string>
<string name="settings.image_loader_concurrency_11">11</string>
<string name="settings.image_loader_concurrency_12">12</string>
<string name="settings.playback.bluetooth_disabled">已禁用</string>
<string name="settings.debug.log_delete">删除文件</string>
<string name="settings.debug.log_deleted">删除日志文件</string>
@ -307,5 +295,4 @@
<string name="api.subsonic.upgrade_client">版本不兼容,请升级 Ultrasonic 应用。</string>
<string name="api.subsonic.upgrade_server">不兼容的版本。请升级Subsonic 服务。</string>
<string name="feature_flags_image_loader_title">启用新的图像加载器</string>
</resources>

View File

@ -265,34 +265,6 @@
<item>@string/settings.share_hours</item>
<item>@string/settings.share_days</item>
</string-array>
<string-array name="imageConcurrencyNames" translatable="false">
<item>@string/settings.image_loader_concurrency_1</item>
<item>@string/settings.image_loader_concurrency_2</item>
<item>@string/settings.image_loader_concurrency_3</item>
<item>@string/settings.image_loader_concurrency_4</item>
<item>@string/settings.image_loader_concurrency_5</item>
<item>@string/settings.image_loader_concurrency_6</item>
<item>@string/settings.image_loader_concurrency_7</item>
<item>@string/settings.image_loader_concurrency_8</item>
<item>@string/settings.image_loader_concurrency_9</item>
<item>@string/settings.image_loader_concurrency_10</item>
<item>@string/settings.image_loader_concurrency_11</item>
<item>@string/settings.image_loader_concurrency_12</item>
</string-array>
<string-array name="imageConcurrencyValues" translatable="false">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
<item>10</item>
<item>11</item>
<item>12</item>
</string-array>
<string-array name="bluetoothDeviceSettingNames" translatable="false">
<item>@string/settings.playback.bluetooth_all</item>
<item>@string/settings.playback.bluetooth_a2dp</item>

View File

@ -395,19 +395,6 @@
<string name="settings.show_all_songs_by_artist">Show All Songs By Artist</string>
<string name="settings.show_all_songs_by_artist_summary">Add new entry in artist view to access all songs for an artist</string>
<string name="download.menu_show_artist">Show Artist</string>
<string name="settings.image_loader_concurrency">Image Loader Concurrency</string>
<string name="settings.image_loader_concurrency_1">1</string>
<string name="settings.image_loader_concurrency_2">2</string>
<string name="settings.image_loader_concurrency_3">3</string>
<string name="settings.image_loader_concurrency_4">4</string>
<string name="settings.image_loader_concurrency_5">5</string>
<string name="settings.image_loader_concurrency_6">6</string>
<string name="settings.image_loader_concurrency_7">7</string>
<string name="settings.image_loader_concurrency_8">8</string>
<string name="settings.image_loader_concurrency_9">9</string>
<string name="settings.image_loader_concurrency_10">10</string>
<string name="settings.image_loader_concurrency_11">11</string>
<string name="settings.image_loader_concurrency_12">12</string>
<string name="albumArt">albumArt</string>
<string name="common_multiple_years">Multiple Years</string>
<string name="settings.server_address_unset" translatable="false">http://example.com</string>
@ -506,10 +493,6 @@
<!-- Subsonic feature flags -->
<string name="feature_flags_category_title">Feature Flags</string>
<string name="feature_flags_image_loader_title">Enable new image loader</string>
<string name="feature_flags_image_loader_description">Enables new image loader implementation.
Currently it doesn\'t save image in device storage and uses only cache in memory.
</string>
<string name="feature_flags_five_star_rating_title">Use five star rating for songs</string>
<string name="feature_flags_five_star_rating_description">Use five star rating system for songs
instead of simply starring/unstarring items.

View File

@ -64,13 +64,6 @@
a:key="viewRefresh"
a:title="@string/settings.view_refresh"
app:iconSpaceReserved="false"/>
<ListPreference
a:defaultValue="5"
a:entries="@array/imageConcurrencyNames"
a:entryValues="@array/imageConcurrencyValues"
a:key="imageLoaderConcurrency"
a:title="@string/settings.image_loader_concurrency"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory
a:title="@string/settings.playback_control_title"
@ -371,12 +364,6 @@
<PreferenceCategory
a:title="@string/feature_flags_category_title"
app:iconSpaceReserved="false">
<CheckBoxPreference
a:key="ff_new_image_loader"
a:persistent="false"
a:title="@string/feature_flags_image_loader_title"
a:summary="@string/feature_flags_image_loader_description"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:key="use_five_star_rating"
a:persistent="false"

View File

@ -1,4 +1,4 @@
package org.moire.ultrasonic.subsonic.loader.image
package org.moire.ultrasonic.imageloader
import android.net.Uri
import com.nhaarman.mockito_kotlin.any
@ -21,7 +21,8 @@ import org.robolectric.annotation.Config
@Config(manifest = Config.NONE)
class AvatarRequestHandlerTest {
private val mockSubsonicApiClient = mock<SubsonicAPIClient>()
private val handler = AvatarRequestHandler(mockSubsonicApiClient)
private val handler =
AvatarRequestHandler(mockSubsonicApiClient)
@Test
fun `Should accept only cover art request`() {
@ -34,7 +35,6 @@ class AvatarRequestHandlerTest {
fun `Should not accept random request uri`() {
val requestUri = Uri.Builder()
.scheme(SCHEME)
.authority(AUTHORITY)
.appendPath("something")
.build()
@ -63,7 +63,9 @@ class AvatarRequestHandlerTest {
whenever(mockSubsonicApiClient.getAvatar(any()))
.thenReturn(streamResponse)
val response = handler.load(createLoadAvatarRequest("some-username").buildRequest(), 0)
val response = handler.load(
createLoadAvatarRequest("some-username").buildRequest(), 0
)
response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK
response.source `should not be` null

View File

@ -1,4 +1,4 @@
package org.moire.ultrasonic.subsonic.loader.image
package org.moire.ultrasonic.imageloader
import java.io.InputStream
import okio.Okio

View File

@ -1,4 +1,4 @@
package org.moire.ultrasonic.subsonic.loader.image
package org.moire.ultrasonic.imageloader
import android.net.Uri
import com.nhaarman.mockito_kotlin.any
@ -21,7 +21,8 @@ import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class CoverArtRequestHandlerTest {
private val mockSubsonicApiClientMock = mock<SubsonicAPIClient>()
private val handler = CoverArtRequestHandler(mockSubsonicApiClientMock)
private val handler =
CoverArtRequestHandler(mockSubsonicApiClientMock)
@Test
fun `Should accept only cover art request`() {
@ -34,7 +35,6 @@ class CoverArtRequestHandlerTest {
fun `Should not accept random request uri`() {
val requestUri = Uri.Builder()
.scheme(SCHEME)
.authority(AUTHORITY)
.appendPath("random")
.build()
@ -76,11 +76,13 @@ class CoverArtRequestHandlerTest {
whenever(mockSubsonicApiClientMock.getCoverArt(any(), anyOrNull()))
.thenReturn(streamResponse)
val response = handler.load(createLoadCoverArtRequest("some").buildRequest(), 0)
val response = handler.load(
createLoadCoverArtRequest("some").buildRequest(), 0
)
response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK
response.source `should not be` null
}
private fun Uri.buildRequest() = Request.Builder(this).build()
private fun Uri.buildRequest() = Request.Builder(this).stableKey("-1").build()
}

View File

@ -1,4 +1,4 @@
package org.moire.ultrasonic.subsonic.loader.image
package org.moire.ultrasonic.imageloader
import android.net.Uri
import org.amshove.kluent.shouldBeEqualTo
@ -11,15 +11,17 @@ class RequestCreatorTest {
@Test
fun `Should create valid load cover art request`() {
val entityId = "299"
val expectedUri = Uri.parse("$SCHEME://$AUTHORITY/$COVER_ART_PATH?$QUERY_ID=$entityId")
val size = 100L
val expectedUri =
Uri.parse("$SCHEME:/$COVER_ART_PATH?$QUERY_ID=$entityId&$SIZE=$size")
createLoadCoverArtRequest(entityId).compareTo(expectedUri).shouldBeEqualTo(0)
createLoadCoverArtRequest(entityId, size).compareTo(expectedUri).shouldBeEqualTo(0)
}
@Test
fun `Should create valid avatar request`() {
val username = "some-username"
val expectedUri = Uri.parse("$SCHEME://$AUTHORITY/$AVATAR_PATH?$QUERY_USERNAME=$username")
val expectedUri = Uri.parse("$SCHEME:/$AVATAR_PATH?$QUERY_USERNAME=$username")
createLoadAvatarRequest(username).compareTo(expectedUri).shouldBeEqualTo(0)
}

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB