Merge pull request #514 from tzugen/coverart
Fix bugs in new image loader and make it default.
This commit is contained in:
commit
580a170f4b
|
@ -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
|
||||
}
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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()) }
|
||||
}
|
|
@ -30,6 +30,8 @@ performance:
|
|||
|
||||
exceptions:
|
||||
active: true
|
||||
TooGenericExceptionCaught:
|
||||
allowedExceptionNameRegex: '_|(all|ignore|expected).*'
|
||||
|
||||
empty-blocks:
|
||||
active: true
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
include ':core:domain'
|
||||
include ':core:subsonic-api'
|
||||
include ':core:subsonic-api-image-loader'
|
||||
include ':core:cache'
|
||||
include ':ultrasonic'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()) }
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}")
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
)
|
|
@ -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()
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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í.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 d’images 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 lorsqu’un 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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
package org.moire.ultrasonic.subsonic.loader.image
|
||||
package org.moire.ultrasonic.imageloader
|
||||
|
||||
import java.io.InputStream
|
||||
import okio.Okio
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Loading…
Reference in New Issue