From 90ecbe4b784223949d4fd17c8f60d0b149cb8e2b Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 1 Jun 2021 14:43:02 +0200 Subject: [PATCH 01/15] Make getCoverArt require a non-null entry --- .../org/moire/ultrasonic/service/CachedMusicService.kt | 2 +- .../kotlin/org/moire/ultrasonic/service/MusicService.kt | 2 +- .../org/moire/ultrasonic/service/OfflineMusicService.kt | 2 +- .../kotlin/org/moire/ultrasonic/service/RESTMusicService.kt | 6 +----- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index 0f280258..7b70a191 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -257,7 +257,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, @Throws(Exception::class) override fun getCoverArt( - entry: MusicDirectory.Entry?, + entry: MusicDirectory.Entry, size: Int, saveToFile: Boolean, highQuality: Boolean diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index f4f88592..90f95f29 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -113,7 +113,7 @@ interface MusicService { @Throws(Exception::class) fun getCoverArt( - entry: MusicDirectory.Entry?, + entry: MusicDirectory.Entry, size: Int, saveToFile: Boolean, highQuality: Boolean diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index f1fc7f43..e6eb6142 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -133,7 +133,7 @@ class OfflineMusicService : MusicService, KoinComponent { } override fun getCoverArt( - entry: MusicDirectory.Entry?, + entry: MusicDirectory.Entry, size: Int, saveToFile: Boolean, highQuality: Boolean diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index fd9f2de6..f3968073 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -490,17 +490,13 @@ open class RESTMusicService( @Throws(Exception::class) override fun getCoverArt( - entry: MusicDirectory.Entry?, + 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) From 2eaa9a209175c8986b9f3ec97a46f4c5a3529fb0 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 2 Jun 2021 15:21:45 +0200 Subject: [PATCH 02/15] Fix two bugs in the new image loader: 1. Id was checked for nullability, but it is actually an empty string in most cases where there is no Cover art. This lead to queries without id set. 2. Size was not respected by the new image loader. --- .../loader/image/CoverArtRequestHandler.kt | 3 +- .../subsonic/loader/image/RequestCreator.kt | 14 ++++++-- .../loader/image/SubsonicImageLoader.kt | 12 +++++-- .../subsonic/SubsonicImageLoaderProxy.kt | 36 ++++++++++++++----- 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt index 7e242479..dfeb5286 100644 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt +++ b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt @@ -22,8 +22,9 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request 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() - val response = apiClient.getCoverArt(id) + val response = apiClient.getCoverArt(id, size) if (response.hasError() || response.stream == null) { throw IOException("${response.apiError}") } else { diff --git a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt index 9cc6799a..78ae0b95 100644 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt +++ b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt @@ -7,16 +7,24 @@ 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 SIZE = "size" internal const val QUERY_USERNAME = "username" -internal fun createLoadCoverArtRequest(entityId: String): Uri = Uri.Builder() +/** + * 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(config: ImageRequest.CoverArt): Uri = + Uri.Builder() .scheme(SCHEME) .authority(AUTHORITY) .appendPath(COVER_ART_PATH) - .appendQueryParameter(QUERY_ID, entityId) + .appendQueryParameter(QUERY_ID, config.entityId) + .appendQueryParameter(SIZE, config.size.toString()) .build() -internal fun createLoadAvatarRequest(username: String): Uri = Uri.Builder() +internal fun createLoadAvatarRequest(username: String): Uri = + Uri.Builder() .scheme(SCHEME) .authority(AUTHORITY) .appendPath(AVATAR_PATH) diff --git a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt index 630bbc4a..7fa27f84 100644 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt +++ b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt @@ -6,6 +6,9 @@ import com.squareup.picasso.Picasso import com.squareup.picasso.RequestCreator import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +// TODO: Caching doesn't work as expected because our query string varies. +// Need to use .stableKey() method + class SubsonicImageLoader( context: Context, apiClient: SubsonicAPIClient @@ -13,7 +16,7 @@ class SubsonicImageLoader( private val picasso = Picasso.Builder(context) .addRequestHandler(CoverArtRequestHandler(apiClient)) .addRequestHandler(AvatarRequestHandler(apiClient)) - .build().apply { setIndicatorsEnabled(BuildConfig.DEBUG) } + .build().apply { setIndicatorsEnabled(true) } fun load(request: ImageRequest) = when (request) { is ImageRequest.CoverArt -> loadCoverArt(request) @@ -21,9 +24,10 @@ class SubsonicImageLoader( } private fun loadCoverArt(request: ImageRequest.CoverArt) { - picasso.load(createLoadCoverArtRequest(request.entityId)) + picasso.load(createLoadCoverArtRequest(request)) .addPlaceholder(request) .addError(request) + .stableKey("${request.entityId}-${request.size}" ) .into(request.imageView) } @@ -31,6 +35,7 @@ class SubsonicImageLoader( picasso.load(createLoadAvatarRequest(request.username)) .addPlaceholder(request) .addError(request) + .stableKey(request.username) .into(request.imageView) } @@ -59,8 +64,9 @@ sealed class ImageRequest( class CoverArt( val entityId: String, imageView: ImageView, + val size: Int, placeHolderDrawableRes: Int? = null, - errorDrawableRes: Int? = null + errorDrawableRes: Int? = null, ) : ImageRequest( placeHolderDrawableRes, errorDrawableRes, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt index 6d0d2b99..2464dfcc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt @@ -2,12 +2,15 @@ package org.moire.ultrasonic.subsonic import android.view.View import android.widget.ImageView +import androidx.core.content.res.ResourcesCompat import org.moire.ultrasonic.R +import org.moire.ultrasonic.app.UApp 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 +import org.moire.ultrasonic.util.Util /** * Temporary proxy between new [SubsonicImageLoader] and [ImageLoader] interface and old @@ -19,6 +22,11 @@ class SubsonicImageLoaderProxy( legacyImageLoader: LegacyImageLoader, private val subsonicImageLoader: SubsonicImageLoader ) : ImageLoader by legacyImageLoader { + + private var imageSizeLarge = Util.getMaxDisplayMetric() + private var imageSizeDefault = 0 + + override fun loadImage( view: View?, entry: MusicDirectory.Entry?, @@ -40,16 +48,18 @@ class SubsonicImageLoaderProxy( defaultResourceId: Int ) { val id = entry?.coverArt + var requestedSize = size val unknownImageId = if (defaultResourceId == -1) R.drawable.unknown_album else defaultResourceId - if (id != null && - view != null && - view is ImageView - ) { + if (requestedSize <= 0) { + requestedSize = if (large) imageSizeLarge else imageSizeDefault + } + + if (id != null && id.isNotEmpty() && view is ImageView) { val request = ImageRequest.CoverArt( - id, view, + id, view, requestedSize, placeHolderDrawableRes = unknownImageId, errorDrawableRes = unknownImageId ) @@ -65,10 +75,7 @@ class SubsonicImageLoaderProxy( crossFade: Boolean, highQuality: Boolean ) { - if (username != null && - view != null && - view is ImageView - ) { + if (username != null && username.isNotEmpty() && view is ImageView) { val request = ImageRequest.Avatar( username, view, placeHolderDrawableRes = R.drawable.ic_contact_picture, @@ -77,4 +84,15 @@ class SubsonicImageLoaderProxy( subsonicImageLoader.load(request) } } + + init { + val default = 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 (default != null) { + imageSizeDefault = default.intrinsicHeight + } + } } From 0989ee20a6a53eb5c7c79e850947977715850656 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 2 Jun 2021 20:37:13 +0200 Subject: [PATCH 03/15] Remove LegacyImageLoader, use Picasso as default --- .../subsonic/loader/image/RequestCreator.kt | 24 +- .../loader/image/SubsonicImageLoader.kt | 13 +- .../api/subsonic/SubsonicAPIClient.kt | 3 + .../api/subsonic/di/SubsonicApiModule.kt | 8 - .../ultrasonic/fragment/SettingsFragment.java | 44 +- .../provider/UltrasonicAppWidgetProvider.java | 5 +- .../org/moire/ultrasonic/util/FileUtil.java | 62 +-- .../moire/ultrasonic/util/ImageLoader.java | 21 +- .../ultrasonic/util/LegacyImageLoader.java | 450 ------------------ .../ultrasonic/activity/NavigationActivity.kt | 1 - .../ultrasonic/fragment/AlbumRowAdapter.kt | 1 - .../ultrasonic/fragment/ArtistRowAdapter.kt | 1 - .../ultrasonic/fragment/GenericRowAdapter.kt | 9 - .../ultrasonic/service/MediaPlayerService.kt | 4 +- .../ultrasonic/service/OfflineMusicService.kt | 4 +- .../ultrasonic/service/RESTMusicService.kt | 13 +- .../subsonic/ImageLoaderProvider.kt | 28 +- .../subsonic/SubsonicImageLoaderProxy.kt | 16 +- ultrasonic/src/main/res/values-cs/strings.xml | 17 - ultrasonic/src/main/res/values-de/strings.xml | 15 - ultrasonic/src/main/res/values-es/strings.xml | 17 - ultrasonic/src/main/res/values-fr/strings.xml | 17 - ultrasonic/src/main/res/values-hu/strings.xml | 17 - ultrasonic/src/main/res/values-it/strings.xml | 12 - ultrasonic/src/main/res/values-nl/strings.xml | 17 - ultrasonic/src/main/res/values-pl/strings.xml | 16 - .../src/main/res/values-pt-rBR/strings.xml | 17 - ultrasonic/src/main/res/values-pt/strings.xml | 17 - ultrasonic/src/main/res/values-ru/strings.xml | 16 - .../src/main/res/values-zh-rCN/strings.xml | 13 - ultrasonic/src/main/res/values/strings.xml | 17 - 31 files changed, 74 insertions(+), 841 deletions(-) delete mode 100644 core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/di/SubsonicApiModule.kt delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java diff --git a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt index 78ae0b95..1e57fff9 100644 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt +++ b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt @@ -14,19 +14,19 @@ 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(config: ImageRequest.CoverArt): Uri = +internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri = Uri.Builder() - .scheme(SCHEME) - .authority(AUTHORITY) - .appendPath(COVER_ART_PATH) - .appendQueryParameter(QUERY_ID, config.entityId) - .appendQueryParameter(SIZE, config.size.toString()) - .build() + .scheme(SCHEME) + .authority(AUTHORITY) + .appendPath(COVER_ART_PATH) + .appendQueryParameter(QUERY_ID, entityId) + .appendQueryParameter(SIZE, size.toString()) + .build() internal fun createLoadAvatarRequest(username: String): Uri = Uri.Builder() - .scheme(SCHEME) - .authority(AUTHORITY) - .appendPath(AVATAR_PATH) - .appendQueryParameter(QUERY_USERNAME, username) - .build() + .scheme(SCHEME) + .authority(AUTHORITY) + .appendPath(AVATAR_PATH) + .appendQueryParameter(QUERY_USERNAME, username) + .build() diff --git a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt index 7fa27f84..fcb2eba7 100644 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt +++ b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt @@ -6,9 +6,7 @@ import com.squareup.picasso.Picasso import com.squareup.picasso.RequestCreator import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient -// TODO: Caching doesn't work as expected because our query string varies. -// Need to use .stableKey() method - +// TODO: Implement OkHTTP disk caching class SubsonicImageLoader( context: Context, apiClient: SubsonicAPIClient @@ -16,7 +14,10 @@ class SubsonicImageLoader( private val picasso = Picasso.Builder(context) .addRequestHandler(CoverArtRequestHandler(apiClient)) .addRequestHandler(AvatarRequestHandler(apiClient)) - .build().apply { setIndicatorsEnabled(true) } + .build().apply { + setIndicatorsEnabled(BuildConfig.DEBUG) + Picasso.setSingletonInstance(this) + } fun load(request: ImageRequest) = when (request) { is ImageRequest.CoverArt -> loadCoverArt(request) @@ -24,10 +25,10 @@ class SubsonicImageLoader( } private fun loadCoverArt(request: ImageRequest.CoverArt) { - picasso.load(createLoadCoverArtRequest(request)) + picasso.load(createLoadCoverArtRequest(request.entityId, request.size.toLong())) .addPlaceholder(request) .addError(request) - .stableKey("${request.entityId}-${request.size}" ) + .stableKey("${request.entityId}-${request.size}") .into(request.imageView) } diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt index fb6ce161..7b3a7600 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt @@ -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. diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/di/SubsonicApiModule.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/di/SubsonicApiModule.kt deleted file mode 100644 index b56dc097..00000000 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/di/SubsonicApiModule.kt +++ /dev/null @@ -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()) } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java index fcc81219..66da521d 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java @@ -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 mediaPlayerControllerLazy = inject(MediaPlayerController.class); - private final Lazy imageLoader = inject(ImageLoaderProvider.class); private final Lazy permissionUtil = inject(PermissionUtil.class); private final Lazy 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"); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java index acb1a193..4b2e33cc 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java @@ -9,7 +9,6 @@ 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; @@ -21,6 +20,8 @@ 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 : FileUtil.getAlbumArtBitmapFromDisk(currentSong, 240, true); if (bitmap == null) { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java index 95062dee..e4b7e2cc 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java @@ -23,14 +23,11 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Build; import android.os.Environment; +import android.os.StatFs; 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; @@ -60,7 +57,6 @@ public class FileUtil private static final List PLAYLIST_FILE_EXTENSIONS = Collections.singletonList("m3u"); private static final Pattern TITLE_WITH_TRACK = Pattern.compile("^\\d\\d-.*"); - private static final Lazy imageLoaderProvider = inject(ImageLoaderProvider.class); private static final Lazy permissionUtil = inject(PermissionUtil.class); public static File getSongFile(MusicDirectory.Entry song) @@ -118,6 +114,11 @@ 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); @@ -137,6 +138,11 @@ public class FileUtil return new File(albumArtDir, String.format("%s.jpeg", md5Hex)); } + /** + * 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(); @@ -150,24 +156,13 @@ public class FileUtil return new File(albumArtDir, String.format("%s.jpeg", md5Hex)); } - public static Bitmap getAvatarBitmap(String username, int size, boolean highQuality) + public static Bitmap getAvatarBitmapFromDisk(String username, int size, boolean highQuality) { if (username == null) return null; File avatarFile = getAvatarFile(username); Bitmap bitmap = null; - ImageLoader imageLoader = imageLoaderProvider.getValue().getImageLoader(); - - if (imageLoader != null) - { - bitmap = imageLoader.getImageBitmap(username, size); - } - - if (bitmap != null) - { - return bitmap.copy(bitmap.getConfig(), false); - } if (avatarFile != null && avatarFile.exists()) { @@ -198,15 +193,7 @@ public class FileUtil 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); - } - } + Timber.i("getAvatarBitmapFromDisk %s", String.valueOf(size)); return bitmap; } @@ -214,24 +201,13 @@ public class FileUtil return null; } - public static Bitmap getAlbumArtBitmap(MusicDirectory.Entry entry, int size, boolean highQuality) + public static Bitmap getAlbumArtBitmapFromDisk(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()) { @@ -262,15 +238,7 @@ public class FileUtil 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); - } - } + Timber.i("getAlbumArtBitmapFromDisk %s", String.valueOf(size)); return bitmap; } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java index d45c67b2..e1ae5ae3 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java @@ -1,18 +1,10 @@ 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); @@ -22,15 +14,4 @@ public interface ImageLoader { 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(); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java deleted file mode 100644 index 47654e98..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java +++ /dev/null @@ -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 . - - 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. - *

- * There should normally be only one instance of this class. - * - * @author Sindre Mehus - */ -public class LegacyImageLoader implements Runnable, ImageLoader { - private final LRUCache cache = new LRUCache<>(150); - private final BlockingQueue queue; - private int imageSizeDefault; - private final int imageSizeLarge; - private Bitmap largeUnknownImage; - private Bitmap unknownAvatarImage; - private final Context context; - private Collection 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(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."); - } - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 7f8abc29..9f029926 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -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() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt index e3974614..cae48837 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt @@ -29,7 +29,6 @@ class AlbumRowAdapter( ) : GenericRowAdapter( onItemClick, onContextMenuClick, - imageLoader, onMusicFolderUpdate ) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt index 2e312225..009b41e3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt @@ -30,7 +30,6 @@ class ArtistRowAdapter( ) : GenericRowAdapter( onItemClick, onContextMenuClick, - imageLoader, onMusicFolderUpdate ), SectionedAdapter { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt index 414c76d0..2a26463c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt @@ -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( val onItemClick: (T) -> Unit, val onContextMenuClick: (MenuItem, T) -> Boolean, - private val imageLoader: ImageLoader, private val onMusicFolderUpdate: (String?) -> Unit ) : RecyclerView.Adapter() { open var itemList: List = listOf() @@ -94,13 +92,6 @@ abstract class GenericRowAdapter( } } - 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 { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index fdc3b4cb..9a0b2938 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -478,7 +478,7 @@ class MediaPlayerService : Service() { if (currentPlaying != null) { try { val song = currentPlaying.song - val cover = FileUtil.getAlbumArtBitmap( + val cover = FileUtil.getAlbumArtBitmapFromDisk( song, Util.getMinDisplayMetric(), true ) @@ -648,7 +648,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 = FileUtil.getAlbumArtBitmapFromDisk(song, iconSize, true) notificationBuilder!!.setContentTitle(song.title) notificationBuilder!!.setContentText(song.artist) notificationBuilder!!.setLargeIcon(bitmap) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index e6eb6142..788ca2d4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -125,7 +125,7 @@ class OfflineMusicService : MusicService, KoinComponent { highQuality: Boolean ): Bitmap? { return try { - val bitmap = FileUtil.getAvatarBitmap(username, size, highQuality) + val bitmap = FileUtil.getAvatarBitmapFromDisk(username, size, highQuality) Util.scaleBitmap(bitmap, size) } catch (ignored: Exception) { null @@ -139,7 +139,7 @@ class OfflineMusicService : MusicService, KoinComponent { highQuality: Boolean ): Bitmap? { return try { - val bitmap = FileUtil.getAlbumArtBitmap(entry, size, highQuality) + val bitmap = FileUtil.getAlbumArtBitmapFromDisk(entry, size, highQuality) Util.scaleBitmap(bitmap, size) } catch (ignored: Exception) { null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index f3968073..b2200672 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -488,6 +488,9 @@ open class RESTMusicService( return response.body()!!.starred2.toDomainEntity() } + // TODO: Implement file caching in Picasso CoverArtRequestHandler, + // and then use Picasso to handle this cache + // This is only called by DownloadFile to cache the cover art for offline use @Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry, @@ -499,7 +502,7 @@ open class RESTMusicService( // the same song. synchronized(entry) { // Use cached file, if existing. - var bitmap = FileUtil.getAlbumArtBitmap(entry, size, highQuality) + var bitmap = FileUtil.getAlbumArtBitmapFromDisk(entry, size, highQuality) val serverScaling = isServerScalingEnabled() if (bitmap == null) { @@ -507,8 +510,9 @@ open class RESTMusicService( val id = entry.coverArt + // Can't load empty string ids if (TextUtils.isEmpty(id)) { - return null // Can't load + return null } val response = subsonicAPIClient.getCoverArt(id!!, size.toLong()) @@ -809,6 +813,9 @@ open class RESTMusicService( } } + // TODO: Implement file caching in Picasso AvatarRequestHandler, + // and then use Picasso to handle this cache + // This method is called from nowhere (all avatars are loaded directly using Picasso) @Throws(Exception::class) override fun getAvatar( username: String?, @@ -824,7 +831,7 @@ open class RESTMusicService( synchronized(username) { // Use cached file, if existing. - var bitmap = FileUtil.getAvatarBitmap(username, size, highQuality) + var bitmap = FileUtil.getAvatarBitmapFromDisk(username, size, highQuality) if (bitmap == null) { var inputStream: InputStream? = null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt index 3ee6bf53..e1907f66 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt @@ -3,11 +3,7 @@ package org.moire.ultrasonic.subsonic import android.content.Context 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.moire.ultrasonic.util.Util /** * Handles the lifetime of the Image Loader @@ -17,33 +13,13 @@ class ImageLoaderProvider(val context: Context) : KoinComponent { @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() + if (imageLoader == null) { + imageLoader = SubsonicImageLoaderProxy(get()) } return imageLoader!! } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt index 2464dfcc..27f6ec0a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt @@ -9,24 +9,21 @@ 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 import org.moire.ultrasonic.util.Util /** - * Temporary proxy between new [SubsonicImageLoader] and [ImageLoader] interface and old - * [LegacyImageLoader] implementation. - * - * Should be removed on [LegacyImageLoader] removal. + * Proxy between [SubsonicImageLoader] and the main App. + * Needed to calculate values like the maximum image size, + * which we can't outside the main package. */ + class SubsonicImageLoaderProxy( - legacyImageLoader: LegacyImageLoader, private val subsonicImageLoader: SubsonicImageLoader -) : ImageLoader by legacyImageLoader { +) : ImageLoader { private var imageSizeLarge = Util.getMaxDisplayMetric() private var imageSizeDefault = 0 - override fun loadImage( view: View?, entry: MusicDirectory.Entry?, @@ -87,7 +84,8 @@ class SubsonicImageLoaderProxy( init { val default = ResourcesCompat.getDrawable( - UApp.applicationContext().resources, R.drawable.unknown_album, null) + UApp.applicationContext().resources, R.drawable.unknown_album, null + ) // Determine the density-dependent image sizes by taking the fallback album // image and querying its size. diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index e1fc4b29..55ba92ac 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -379,19 +379,6 @@ Zobrazit všechny skladby umělce Přidat nový zápis v náhledu umělců pro přístup ke všem skladbám umělce Zobrazit umělce - Počet vláken stahování obrázků - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 albumArt Vícenásobné roky Pokračovat v přehrávání po připojení bluetooth přístroje @@ -501,10 +488,6 @@ Příznaky funkcí - Povolit nové načítání obrázků - 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. - Používat pět hvězdiček pro hodnocení skladeb Používat pět hvězdiček pro hodnocení skladeb namísto jednoduchého jednohvězdičkového hodnocení. diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index ee0c65bf..aab075fe 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -376,19 +376,6 @@ Alle Titel nach Künstler sortieren Einen neuen Eintrag in der Künstleransicht hinzufügen, um auf alle Lieder eines Künstlers zuzugreifen Künstler zeigen - Paralleles laden von Bildern - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 Mehrere Jahre Server hinzufügen @@ -434,8 +421,6 @@ Funktionseinstellungem - Neuen Bild-Lader aktivieren - Neuen Bild-Lader aktivieren. Bilder werden derzeit nur im Chache gespeichert. Verwenden Sie Fünf-Sterne-Bewertung für Songs Verwenden Sie Fünf-Sterne-Bewertungssystem für Songs          anstatt einfach Elemente zu markieren / zu entfernen. diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 1225d7eb..35192166 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -393,19 +393,6 @@ Mostrar todas las canciones por artista Añadir nueva entrada en la vista de artista para acceder a todas las canciones de un artista Mostrar artista - Concurrencia del cargador de imágenes - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 Caratula del Álbum Múltiples años Reanudar al conectar un dispositivo Bluetooth @@ -503,10 +490,6 @@ Funciones experimentales - Habilitar nuevo cargador de imágenes - 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. - Use cinco estrellas para las canciones Utilice el sistema de calificación de cinco estrellas para canciones en lugar de simplemente destacar / desestimar elementos. diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index 8e9ba787..5bc3b7b7 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -381,19 +381,6 @@ Voir tous les titres par artiste Ajouter une nouvelle entrée dans la vue artiste pour accéder à toutes les titres d\'un artiste Afficher l\'artiste - Chargements d’images simultanés - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 Pochette d\'album Années multiples Reprendre lorsqu’un appareil Bluetooth se connecte @@ -491,10 +478,6 @@ Drapeaux des fonctionnalités - Activer le nouveau chargeur d\'images - 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. - Utiliser les étoiles pour noter les morceaux Utiliser un système de notation à base d\'étoiles pour les morceaux au lieu de simplement mettre en avant les morceaux. diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index c913b846..5476f2c3 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -393,19 +393,6 @@ Az előadó összes dalának megjelenítése Új bejegyzés hozzáadása az előadóhoz, az előadó összes dalának eléréséhez. Ugrás az előadóhoz - Image Loader Concurrency - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 albumArt Több év Folytatás Bluetooth eszköz csatlakozásakor @@ -501,10 +488,6 @@ Jellemzők Zászlók - Engedélyezzen új képbetöltőt - 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. - Öt csillagos értékelés használata a dalokhoz Öt csillag használata az értékeléshez az egyszerű csillaggal jelölés helyett. diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index 043368a8..f7031d63 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -338,18 +338,6 @@ Condividi canzoni via MX Player Predefinito - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 1 canzone %d canzoni diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index 2b2da416..9cd9fd00 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -393,19 +393,6 @@ Alle nummers van artiest tonen Item toevoegen in artiestweergave om alle nummers van een artiest te bekijken Artiest tonen - Aantal tegelijkertijd te laden afbeeldingen - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 Albumhoes Meerdere jaren Hervatten bij verbinding met bluetoothapparaat @@ -503,10 +490,6 @@ Experimentele functies - Nieuwe manier van afbeeldingen laden inschakelen - Schakelt de nieuwe methode voor het laden van afbeeldingen in. - Momenteel slaat het geen afbeeldingen op op de apparaatopslag en wordt alleen geheugencache gebruikt. - Gebruik vijf sterren voor nummers Gebruik vijf sterren ratingsysteem voor liedjes in plaats van items simpelweg in de hoofdrol te zetten / niet te verwijderen. diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index c09b4479..4f7b15d1 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -376,19 +376,6 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników Wyświetlaj wszystkie utwory artysty Dodaje nową pozycję w widoku artysty z wszystkimi jego utworami Wyświetlaj artystę - Ilość jednocześnie ładowanych obrazów - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 Okładka Z różnych lat Dodaj serwer @@ -449,9 +436,6 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników Flagi funkcji - Włącz program ładujący nowe obrazy - 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ą. Użyj pięciu gwiazdek dla utworów W przypadku utworów użyj systemu pięciu gwiazdek zamiast po prostu grać gwiazdkami / bez gwiazd. diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index 826a45e4..903d7b6a 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -381,19 +381,6 @@ Mostrar Todas as Músicas por Artista Adicionar nova entrada em artista para acessar todas as músicas do artista Mostrar Artista - Concorrência ao Carregar Imagens - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 albumArt Anos Múltiplos Retomar ao Conectar Dispositivo Bluetooth @@ -489,10 +476,6 @@ Bandeiras de Recursos - Ativar Novo Carregador de Imagens - 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. - Usar Classif. de 5 estrelas Para Músicas Use o sistema de classificação de 5 estrelas para músicas em vez de simplesmente estrelar/não estrelar itens. diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index 74a7d3d4..8fb5bb12 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -376,19 +376,6 @@ Todas as Músicas do Artista Adicionar nova entrada em artista para ver todas as músicas do artista Mostrar Artista - Concorrência ao Carregar Imagens - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 Múltiplos Anos Adicionar Servidor @@ -434,10 +421,6 @@ Bandeiras de recursos - Ativar novo carregador de imagens - 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. - Use classificação de cinco estrelas para músicas Use o sistema de classificação de cinco estrelas para músicas em vez de simplesmente estrelar / não estrelar itens. diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 76d8f0c0..0897075f 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -368,19 +368,6 @@ Показать все треки исполнителя Добавить новую запись в представлении исполнителя, чтобы получить доступ ко всем песням для исполнителя Показать исполнителей - Загрузчик совпадающих изображений - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 albumArt Несколько лет @@ -396,7 +383,4 @@ Флаги - Включить новый загрузчик изображений - Включает новую реализацию загрузчика изображений. -В настоящее время он не сохраняет изображение в памяти устройства и использует только кэш в памяти. diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 716b65f4..60067c12 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -264,18 +264,6 @@ MX Player 默认 分享 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 已禁用 删除文件 删除日志文件 @@ -307,5 +295,4 @@ 版本不兼容,请升级 Ultrasonic 应用。 不兼容的版本。请升级Subsonic 服务。 - 启用新的图像加载器 diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index f0733e73..62556828 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -395,19 +395,6 @@ Show All Songs By Artist Add new entry in artist view to access all songs for an artist Show Artist - Image Loader Concurrency - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 albumArt Multiple Years http://example.com @@ -506,10 +493,6 @@ Feature Flags - Enable new image loader - Enables new image loader implementation. - Currently it doesn\'t save image in device storage and uses only cache in memory. - Use five star rating for songs Use five star rating system for songs instead of simply starring/unstarring items. From 41eb217d8fbf8390d42d62e4f369a1d197bd6ef6 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 2 Jun 2021 20:40:08 +0200 Subject: [PATCH 04/15] Finish removing unneeded preferences --- ultrasonic/src/main/res/values/arrays.xml | 28 ----------------------- ultrasonic/src/main/res/xml/settings.xml | 13 ----------- 2 files changed, 41 deletions(-) diff --git a/ultrasonic/src/main/res/values/arrays.xml b/ultrasonic/src/main/res/values/arrays.xml index edc5a09f..d18959e9 100644 --- a/ultrasonic/src/main/res/values/arrays.xml +++ b/ultrasonic/src/main/res/values/arrays.xml @@ -265,34 +265,6 @@ @string/settings.share_hours @string/settings.share_days - - @string/settings.image_loader_concurrency_1 - @string/settings.image_loader_concurrency_2 - @string/settings.image_loader_concurrency_3 - @string/settings.image_loader_concurrency_4 - @string/settings.image_loader_concurrency_5 - @string/settings.image_loader_concurrency_6 - @string/settings.image_loader_concurrency_7 - @string/settings.image_loader_concurrency_8 - @string/settings.image_loader_concurrency_9 - @string/settings.image_loader_concurrency_10 - @string/settings.image_loader_concurrency_11 - @string/settings.image_loader_concurrency_12 - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - @string/settings.playback.bluetooth_all @string/settings.playback.bluetooth_a2dp diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index 716eb2f2..ce724706 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -64,13 +64,6 @@ a:key="viewRefresh" a:title="@string/settings.view_refresh" app:iconSpaceReserved="false"/> - - Date: Wed, 2 Jun 2021 20:42:45 +0200 Subject: [PATCH 05/15] Finish removing unneeded preferences and fix import problem --- .../src/main/java/org/moire/ultrasonic/util/FileUtil.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java index e4b7e2cc..056f0df2 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java @@ -23,7 +23,6 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Build; import android.os.Environment; -import android.os.StatFs; import android.text.TextUtils; import org.moire.ultrasonic.app.UApp; @@ -43,6 +42,9 @@ 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; /** From f8efb6d59209898374e5c648ef1c094f28f6d822 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 2 Jun 2021 21:03:56 +0200 Subject: [PATCH 06/15] Update a test to reflect new arguments --- .../ultrasonic/subsonic/loader/image/RequestCreatorTest.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt b/core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt index b318d60d..89b39a4d 100644 --- a/core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt +++ b/core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt @@ -11,9 +11,11 @@ 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://$AUTHORITY/$COVER_ART_PATH?$QUERY_ID=$entityId&$SIZE=$size") - createLoadCoverArtRequest(entityId).compareTo(expectedUri).shouldBeEqualTo(0) + createLoadCoverArtRequest(entityId, size).compareTo(expectedUri).shouldBeEqualTo(0) } @Test From 9161f9dc9992ec854f3a3f81a473f64cd8e7001f Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 7 Jun 2021 00:22:29 +0200 Subject: [PATCH 07/15] Move ImageLoader module into main module. --- core/subsonic-api-image-loader/build.gradle | 29 ---- .../src/main/AndroidManifest.xml | 4 - settings.gradle | 1 - ultrasonic/build.gradle | 11 +- .../fragment/NowPlayingFragment.java | 2 +- .../ultrasonic/fragment/PlayerFragment.java | 4 +- .../org/moire/ultrasonic/view/AlbumView.java | 4 +- .../moire/ultrasonic/view/ChatAdapter.java | 11 +- .../moire/ultrasonic/view/EntryAdapter.java | 2 +- .../moire/ultrasonic/di/MusicServiceModule.kt | 4 +- .../ultrasonic/fragment/AlbumRowAdapter.kt | 4 +- .../ultrasonic/fragment/ArtistRowAdapter.kt | 4 +- .../fragment/TrackCollectionFragment.kt | 2 +- .../imageloader}/AvatarRequestHandler.kt | 6 +- .../imageloader}/CoverArtRequestHandler.kt | 3 +- .../ultrasonic/imageloader/ImageLoader.kt | 157 ++++++++++++++++++ .../ultrasonic/imageloader}/RequestCreator.kt | 5 +- .../imageloader}/SubsonicImageLoader.kt | 34 +--- .../subsonic/ImageLoaderProvider.kt | 30 +++- .../subsonic/SubsonicImageLoaderProxy.kt | 96 ----------- .../imageloader}/AvatarRequestHandlerTest.kt | 9 +- .../imageloader}/CommonFunctions.kt | 2 +- .../CoverArtRequestHandlerTest.kt | 9 +- .../imageloader}/RequestCreatorTest.kt | 6 +- .../src/test}/resources/Big_Buck_Bunny.jpeg | Bin 25 files changed, 232 insertions(+), 207 deletions(-) delete mode 100644 core/subsonic-api-image-loader/build.gradle delete mode 100644 core/subsonic-api-image-loader/src/main/AndroidManifest.xml rename {core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image => ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader}/AvatarRequestHandler.kt (85%) rename {core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image => ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader}/CoverArtRequestHandler.kt (92%) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt rename {core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image => ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader}/RequestCreator.kt (84%) rename {core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image => ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader}/SubsonicImageLoader.kt (68%) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt rename {core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image => ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader}/AvatarRequestHandlerTest.kt (89%) rename {core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image => ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader}/CommonFunctions.kt (81%) rename {core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image => ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader}/CoverArtRequestHandlerTest.kt (91%) rename {core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image => ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader}/RequestCreatorTest.kt (73%) rename {core/subsonic-api-image-loader/src/integrationTest => ultrasonic/src/test}/resources/Big_Buck_Bunny.jpeg (100%) diff --git a/core/subsonic-api-image-loader/build.gradle b/core/subsonic-api-image-loader/build.gradle deleted file mode 100644 index dc8324b7..00000000 --- a/core/subsonic-api-image-loader/build.gradle +++ /dev/null @@ -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 -} diff --git a/core/subsonic-api-image-loader/src/main/AndroidManifest.xml b/core/subsonic-api-image-loader/src/main/AndroidManifest.xml deleted file mode 100644 index b36252b8..00000000 --- a/core/subsonic-api-image-loader/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/settings.gradle b/settings.gradle index 140eafe6..a4a9a1e1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,4 @@ include ':core:domain' include ':core:subsonic-api' -include ':core:subsonic-api-image-loader' include ':core:cache' include ':ultrasonic' diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 3d6a9f78..997ea5fc 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -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 } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java index 05306643..fd6684d4 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java @@ -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); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlayerFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlayerFragment.java index 4f662d60..9b056315 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlayerFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlayerFragment.java @@ -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); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java index 21ef70b5..68abe346 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java @@ -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(); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java index 82c18a8e..6a6ccf37 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ChatAdapter.java @@ -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 private static final Pattern phoneMatcher = Pattern.compile(phoneRegex); private final Lazy activeServerProvider = inject(ActiveServerProvider.class); - private final Lazy imageLoader = inject(ImageLoaderProvider.class); + private final Lazy imageLoaderProvider = inject(ImageLoaderProvider.class); public ChatAdapter(Context context, List messages) { @@ -95,11 +96,11 @@ public class ChatAdapter extends ArrayAdapter 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); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/EntryAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/EntryAdapter.java index bd05bff8..65488e8f 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/EntryAdapter.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/EntryAdapter.java @@ -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; diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index 285e91be..9e0e746c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -22,7 +22,7 @@ import org.moire.ultrasonic.subsonic.DownloadHandler 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.imageloader.SubsonicImageLoader import org.moire.ultrasonic.util.Constants /** @@ -77,8 +77,6 @@ val musicServiceModule = module { OfflineMusicService() } - single { SubsonicImageLoader(androidContext(), get()) } - single { DownloadHandler(get(), get()) } single { NetworkAndStorageChecker(androidContext()) } single { VideoPlayer() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt index cae48837..22934478 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt @@ -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 @@ -57,7 +57,7 @@ class AlbumRowAdapter( imageLoader.loadImage( holder.coverArt, MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId }, - false, 0, false, true, R.drawable.unknown_album + false, 0, R.drawable.unknown_album ) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt index 009b41e3..3c3e3757 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt @@ -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 /** @@ -62,7 +62,7 @@ class ArtistRowAdapter( imageLoader.loadImage( holder.coverArt, MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId }, - false, 0, false, true, R.drawable.ic_contact_picture + false, 0, R.drawable.ic_contact_picture ) } else { holder.coverArt.visibility = View.GONE diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 2d544ef1..c9d7affb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -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) diff --git a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandler.kt similarity index 85% rename from core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandler.kt index ab8ac70e..e009f58c 100644 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandler.kt @@ -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" } } diff --git a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt similarity index 92% rename from core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt index dfeb5286..3c091088 100644 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt @@ -1,4 +1,4 @@ -package org.moire.ultrasonic.subsonic.loader.image +package org.moire.ultrasonic.imageloader import com.squareup.picasso.Picasso.LoadedFrom.NETWORK import com.squareup.picasso.Request @@ -14,7 +14,6 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request override fun canHandleRequest(data: Request): Boolean { return with(data.uri) { scheme == SCHEME && - authority == AUTHORITY && path == "/$COVER_ART_PATH" } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt new file mode 100644 index 00000000..63d1c2bb --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -0,0 +1,157 @@ +package org.moire.ultrasonic.imageloader + +import android.content.Context +import android.view.View +import android.widget.ImageView +import com.squareup.picasso.Picasso +import com.squareup.picasso.RequestCreator +import org.moire.ultrasonic.BuildConfig +import org.moire.ultrasonic.R +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.domain.MusicDirectory +import java.io.File + +/** + * Our new image loader which uses Picasso as a backend. + */ +class ImageLoader( + context: Context, + apiClient: SubsonicAPIClient, + private val config: ImageLoaderConfig +) { + + private val picasso = Picasso.Builder(context) + .addRequestHandler(CoverArtRequestHandler(apiClient)) + .addRequestHandler(AvatarRequestHandler(apiClient)) + .build().apply { + setIndicatorsEnabled(BuildConfig.DEBUG) + Picasso.setSingletonInstance(this) + } + + 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.entityId}-${request.size}") + .into(request.imageView) + } + + private fun loadAvatar(request: ImageRequest.Avatar) { + picasso.load(createLoadAvatarRequest(request.username)) + .addPlaceholder(request) + .addError(request) + .stableKey(request.username) + .into(request.imageView) + } + + private fun RequestCreator.addPlaceholder(request: ImageRequest): RequestCreator { + if (request.placeHolderDrawableRes != null) { + placeholder(request.placeHolderDrawableRes) + } + + return this + } + + private fun RequestCreator.addError(request: ImageRequest): RequestCreator { + if (request.errorDrawableRes != null) { + error(request.errorDrawableRes) + } + + return this + } + + /** + * 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 request = ImageRequest.CoverArt( + id, view, requestedSize, + placeHolderDrawableRes = defaultResourceId, + errorDrawableRes = defaultResourceId + ) + load(request) + } + } + + /** + * 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) + } + } + + 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, + 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? +) diff --git a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/RequestCreator.kt similarity index 84% rename from core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/RequestCreator.kt index 1e57fff9..37a11f5a 100644 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/RequestCreator.kt @@ -1,9 +1,8 @@ -package org.moire.ultrasonic.subsonic.loader.image +package org.moire.ultrasonic.imageloader 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" @@ -17,7 +16,6 @@ internal const val QUERY_USERNAME = "username" internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri = Uri.Builder() .scheme(SCHEME) - .authority(AUTHORITY) .appendPath(COVER_ART_PATH) .appendQueryParameter(QUERY_ID, entityId) .appendQueryParameter(SIZE, size.toString()) @@ -26,7 +24,6 @@ internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri = internal fun createLoadAvatarRequest(username: String): Uri = Uri.Builder() .scheme(SCHEME) - .authority(AUTHORITY) .appendPath(AVATAR_PATH) .appendQueryParameter(QUERY_USERNAME, username) .build() diff --git a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/SubsonicImageLoader.kt similarity index 68% rename from core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/SubsonicImageLoader.kt index fcb2eba7..712dd4df 100644 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/SubsonicImageLoader.kt @@ -1,12 +1,11 @@ -package org.moire.ultrasonic.subsonic.loader.image +package org.moire.ultrasonic.imageloader import android.content.Context -import android.widget.ImageView import com.squareup.picasso.Picasso import com.squareup.picasso.RequestCreator +import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient -// TODO: Implement OkHTTP disk caching class SubsonicImageLoader( context: Context, apiClient: SubsonicAPIClient @@ -56,32 +55,3 @@ class SubsonicImageLoader( return this } } - -sealed class ImageRequest( - val placeHolderDrawableRes: Int? = null, - val errorDrawableRes: Int? = null, - val imageView: ImageView -) { - class CoverArt( - val entityId: 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 - ) -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt index e1907f66..c674f1a6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt @@ -1,9 +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.util.ImageLoader +import org.moire.ultrasonic.R +import org.moire.ultrasonic.app.UApp +import org.moire.ultrasonic.util.FileUtil +import org.moire.ultrasonic.imageloader.ImageLoader +import org.moire.ultrasonic.imageloader.ImageLoaderConfig +import org.moire.ultrasonic.util.Util /** * Handles the lifetime of the Image Loader @@ -11,6 +17,26 @@ import org.moire.ultrasonic.util.ImageLoader class ImageLoaderProvider(val context: Context) : KoinComponent { private var imageLoader: ImageLoader? = null + private val config by lazy { + var defaultSize = 0 + val fallbackImage = ResourcesCompat.getDrawable( + UApp.applicationContext().resources, R.drawable.unknown_album, null + ) + + // Determine the density-dependent image sizes by taking the fallback album + // image and querying its size. + if (fallbackImage != null) { + defaultSize = fallbackImage.intrinsicHeight + } + + ImageLoaderConfig( + Util.getMaxDisplayMetric(), + defaultSize, + FileUtil.getAlbumArtDirectory() + ) + + } + @Synchronized fun clearImageLoader() { imageLoader = null @@ -19,7 +45,7 @@ class ImageLoaderProvider(val context: Context) : KoinComponent { @Synchronized fun getImageLoader(): ImageLoader { if (imageLoader == null) { - imageLoader = SubsonicImageLoaderProxy(get()) + imageLoader = ImageLoader(get(), get(), config) } return imageLoader!! } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt deleted file mode 100644 index 27f6ec0a..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt +++ /dev/null @@ -1,96 +0,0 @@ -package org.moire.ultrasonic.subsonic - -import android.view.View -import android.widget.ImageView -import androidx.core.content.res.ResourcesCompat -import org.moire.ultrasonic.R -import org.moire.ultrasonic.app.UApp -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.Util - -/** - * Proxy between [SubsonicImageLoader] and the main App. - * Needed to calculate values like the maximum image size, - * which we can't outside the main package. - */ - -class SubsonicImageLoaderProxy( - private val subsonicImageLoader: SubsonicImageLoader -) : ImageLoader { - - private var imageSizeLarge = Util.getMaxDisplayMetric() - private var imageSizeDefault = 0 - - 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 - var requestedSize = size - val unknownImageId = - if (defaultResourceId == -1) R.drawable.unknown_album - else defaultResourceId - - if (requestedSize <= 0) { - requestedSize = if (large) imageSizeLarge else imageSizeDefault - } - - if (id != null && id.isNotEmpty() && view is ImageView) { - val request = ImageRequest.CoverArt( - id, view, requestedSize, - 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 && username.isNotEmpty() && view is ImageView) { - val request = ImageRequest.Avatar( - username, view, - placeHolderDrawableRes = R.drawable.ic_contact_picture, - errorDrawableRes = R.drawable.ic_contact_picture - ) - subsonicImageLoader.load(request) - } - } - - init { - val default = 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 (default != null) { - imageSizeDefault = default.intrinsicHeight - } - } -} diff --git a/core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandlerTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt similarity index 89% rename from core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandlerTest.kt rename to ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt index 1a039d41..214cdad8 100644 --- a/core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandlerTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt @@ -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() - 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,8 @@ 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 diff --git a/core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CommonFunctions.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CommonFunctions.kt similarity index 81% rename from core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CommonFunctions.kt rename to ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CommonFunctions.kt index 30d38c60..3850290a 100644 --- a/core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CommonFunctions.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CommonFunctions.kt @@ -1,4 +1,4 @@ -package org.moire.ultrasonic.subsonic.loader.image +package org.moire.ultrasonic.imageloader import java.io.InputStream import okio.Okio diff --git a/core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandlerTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt similarity index 91% rename from core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandlerTest.kt rename to ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt index 4a43fbfe..5bd6c336 100644 --- a/core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandlerTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt @@ -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() - 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,7 +76,8 @@ 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 diff --git a/core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/RequestCreatorTest.kt similarity index 73% rename from core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt rename to ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/RequestCreatorTest.kt index 89b39a4d..4e4bcf5c 100644 --- a/core/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/RequestCreatorTest.kt @@ -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 @@ -13,7 +13,7 @@ class RequestCreatorTest { val entityId = "299" val size = 100L val expectedUri = - Uri.parse("$SCHEME://$AUTHORITY/$COVER_ART_PATH?$QUERY_ID=$entityId&$SIZE=$size") + Uri.parse("$SCHEME:/$COVER_ART_PATH?$QUERY_ID=$entityId&$SIZE=$size") createLoadCoverArtRequest(entityId, size).compareTo(expectedUri).shouldBeEqualTo(0) } @@ -21,7 +21,7 @@ class RequestCreatorTest { @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) } diff --git a/core/subsonic-api-image-loader/src/integrationTest/resources/Big_Buck_Bunny.jpeg b/ultrasonic/src/test/resources/Big_Buck_Bunny.jpeg similarity index 100% rename from core/subsonic-api-image-loader/src/integrationTest/resources/Big_Buck_Bunny.jpeg rename to ultrasonic/src/test/resources/Big_Buck_Bunny.jpeg From 566e429e4c7c398f91a2e11369a2fbdfed4670aa Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 7 Jun 2021 13:17:00 +0200 Subject: [PATCH 08/15] Use file cache in Picasso --- detekt-config.yml | 2 + .../provider/UltrasonicAppWidgetProvider.java | 4 +- .../org/moire/ultrasonic/util/FileUtil.java | 138 ++++-------------- .../moire/ultrasonic/util/ImageLoader.java | 17 --- .../moire/ultrasonic/di/MusicServiceModule.kt | 5 +- .../ultrasonic/fragment/AlbumRowAdapter.kt | 3 +- .../ultrasonic/fragment/ArtistRowAdapter.kt | 5 +- .../ultrasonic/imageloader/BitmapUtils.kt | 128 ++++++++++++++++ .../imageloader/CoverArtRequestHandler.kt | 7 + .../ultrasonic/imageloader/ImageLoader.kt | 14 +- .../imageloader/SubsonicImageLoader.kt | 57 -------- .../ultrasonic/service/CachedMusicService.kt | 10 +- .../moire/ultrasonic/service/DownloadFile.kt | 6 +- .../ultrasonic/service/MediaPlayerService.kt | 9 +- .../moire/ultrasonic/service/MusicService.kt | 9 +- .../ultrasonic/service/OfflineMusicService.kt | 11 +- .../ultrasonic/service/RESTMusicService.kt | 17 +-- .../subsonic/ImageLoaderProvider.kt | 52 ++++--- .../imageloader/AvatarRequestHandlerTest.kt | 3 +- .../imageloader/CoverArtRequestHandlerTest.kt | 3 +- 20 files changed, 249 insertions(+), 251 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/BitmapUtils.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/SubsonicImageLoader.kt diff --git a/detekt-config.yml b/detekt-config.yml index d095b0b5..47707e69 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -30,6 +30,8 @@ performance: exceptions: active: true + TooGenericExceptionCaught: + allowedExceptionNameRegex: '_|(all|ignore|expected).*' empty-blocks: active: true diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java index 4b2e33cc..619484e2 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java @@ -15,10 +15,10 @@ 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; @@ -160,7 +160,7 @@ public class UltrasonicAppWidgetProvider extends AppWidgetProvider // Set the cover art try { - Bitmap bitmap = currentSong == null ? null : FileUtil.getAlbumArtBitmapFromDisk(currentSong, 240, true); + Bitmap bitmap = currentSong == null ? null : BitmapUtils.Companion.getAlbumArtBitmapFromDisk(currentSong, 240); if (bitmap == null) { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java index 056f0df2..28b80a0c 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java @@ -19,8 +19,6 @@ 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; @@ -38,6 +36,7 @@ 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; @@ -127,6 +126,24 @@ public class FileUtil return getAlbumArtFile(albumDir); } + /** + * Get the cache key for a given album entry + * @param entry The album entry + * @return String The hash key + */ + public static String getAlbumArtKey(MusicDirectory.Entry entry) + { + File albumDir = getAlbumDirectory(entry); + File albumArtDir = getAlbumArtDirectory(); + + if (albumArtDir == null || albumDir == null) { + return null; + } + + return String.format(Locale.ROOT, "%s.jpeg", Util.md5Hex(albumDir.getPath())); + } + + public static File getAvatarFile(String username) { File albumArtDir = getAlbumArtDirectory(); @@ -158,119 +175,24 @@ public class FileUtil return new File(albumArtDir, String.format("%s.jpeg", md5Hex)); } - public static Bitmap getAvatarBitmapFromDisk(String username, int size, boolean highQuality) + + /** + * Get the album art file for a given cache key + * @param cacheKey + * @return File object. Not guaranteed that it exists + */ + public static File getAlbumArtFile(String cacheKey) { - if (username == null) return null; + File albumArtDir = getAlbumArtDirectory(); - File avatarFile = getAvatarFile(username); - - Bitmap bitmap = null; - - if (avatarFile != null && avatarFile.exists()) + if (albumArtDir == null || cacheKey == null) { - 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("getAvatarBitmapFromDisk %s", String.valueOf(size)); - - return bitmap; + return null; } - return null; + return new File(albumArtDir, cacheKey); } - public static Bitmap getAlbumArtBitmapFromDisk(MusicDirectory.Entry entry, int size, boolean highQuality) - { - if (entry == null) return null; - - File albumArtFile = getAlbumArtFile(entry); - - Bitmap bitmap = null; - - if (albumArtFile != null && albumArtFile.exists()) - { - final BitmapFactory.Options opt = new BitmapFactory.Options(); - - if (size > 0) - { - opt.inJustDecodeBounds = true; - BitmapFactory.decodeFile(albumArtFile.getPath(), opt); - - if (highQuality) - { - opt.inDither = true; - opt.inPreferQualityOverSpeed = true; - } - - opt.inPurgeable = true; - opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size)); - opt.inJustDecodeBounds = false; - } - - try - { - bitmap = BitmapFactory.decodeFile(albumArtFile.getPath(), opt); - } - catch (Exception ex) - { - Timber.e(ex, "Exception in BitmapFactory.decodeFile()"); - } - - Timber.i("getAlbumArtBitmapFromDisk %s", String.valueOf(size)); - - return bitmap; - } - - return null; - } - - public static Bitmap getSampledBitmap(byte[] bytes, int size, boolean highQuality) - { - final BitmapFactory.Options opt = new BitmapFactory.Options(); - - if (size > 0) - { - opt.inJustDecodeBounds = true; - BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt); - - if (highQuality) - { - opt.inDither = true; - opt.inPreferQualityOverSpeed = true; - } - - opt.inPurgeable = true; - opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size)); - opt.inJustDecodeBounds = false; - } - - Timber.i("getSampledBitmap %s", String.valueOf(size)); - return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt); - } public static File getAlbumArtDirectory() { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java deleted file mode 100644 index e1ae5ae3..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.moire.ultrasonic.util; - -import android.view.View; - -import org.moire.ultrasonic.domain.MusicDirectory; - -public interface ImageLoader { - void loadAvatarImage(View view, String username, boolean large, int size, boolean crossFade, - boolean highQuality); - - void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, - boolean crossFade, boolean highQuality); - - void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, - boolean crossFade, boolean highQuality, int defaultResourceId); - -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index 9e0e746c..c2c0b6d2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -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.imageloader.SubsonicImageLoader import org.moire.ultrasonic.util.Constants /** @@ -77,6 +78,8 @@ val musicServiceModule = module { OfflineMusicService() } + single { ImageLoader(androidContext(), get(), ImageLoaderProvider.config) } + single { DownloadHandler(get(), get()) } single { NetworkAndStorageChecker(androidContext()) } single { VideoPlayer() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt index 22934478..239fb237 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt @@ -55,8 +55,7 @@ class AlbumRowAdapter( holder.coverArtId = entry.coverArt imageLoader.loadImage( - holder.coverArt, - MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId }, + holder.coverArt, entry, false, 0, R.drawable.unknown_album ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt index 3c3e3757..69988607 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt @@ -61,7 +61,10 @@ class ArtistRowAdapter( holder.coverArt.visibility = View.VISIBLE imageLoader.loadImage( holder.coverArt, - MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId }, + MusicDirectory.Entry("-1").apply { + coverArt = holder.coverArtId + artist = itemList[listPosition].name + }, false, 0, R.drawable.ic_contact_picture ) } else { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/BitmapUtils.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/BitmapUtils.kt new file mode 100644 index 00000000..fddbec66 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/BitmapUtils.kt @@ -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 + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt index 3c091088..3152e565 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt @@ -1,5 +1,6 @@ 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 @@ -23,6 +24,12 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request ?: throw IllegalArgumentException("Nullable id") val size = request.uri.getQueryParameter(SIZE)?.toLong() + // Check if we have a hit in the disk cache + val cache = BitmapUtils.getAlbumArtBitmapFromDisk(request.stableKey!!, size?.toInt()) + if (cache != null) { + return Result(cache, DISK) + } + val response = apiClient.getCoverArt(id, size) if (response.hasError() || response.stream == null) { throw IOException("${response.apiError}") diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index 63d1c2bb..66b5664f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -5,11 +5,12 @@ import android.view.View import android.widget.ImageView import com.squareup.picasso.Picasso import com.squareup.picasso.RequestCreator +import java.io.File import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.domain.MusicDirectory -import java.io.File +import org.moire.ultrasonic.util.FileUtil /** * Our new image loader which uses Picasso as a backend. @@ -18,14 +19,13 @@ class ImageLoader( context: Context, apiClient: SubsonicAPIClient, private val config: ImageLoaderConfig -) { +) { private val picasso = Picasso.Builder(context) .addRequestHandler(CoverArtRequestHandler(apiClient)) .addRequestHandler(AvatarRequestHandler(apiClient)) .build().apply { setIndicatorsEnabled(BuildConfig.DEBUG) - Picasso.setSingletonInstance(this) } private fun load(request: ImageRequest) = when (request) { @@ -37,7 +37,7 @@ class ImageLoader( picasso.load(createLoadCoverArtRequest(request.entityId, request.size.toLong())) .addPlaceholder(request) .addError(request) - .stableKey("${request.entityId}-${request.size}") + .stableKey(request.cacheKey) .into(request.imageView) } @@ -80,8 +80,9 @@ class ImageLoader( val requestedSize = resolveSize(size, large) if (id != null && id.isNotEmpty() && view is ImageView) { + val key = FileUtil.getAlbumArtKey(entry) val request = ImageRequest.CoverArt( - id, view, requestedSize, + id, key, view, requestedSize, placeHolderDrawableRes = defaultResourceId, errorDrawableRes = defaultResourceId ) @@ -125,6 +126,7 @@ sealed class ImageRequest( ) { class CoverArt( val entityId: String, + val cacheKey: String, imageView: ImageView, val size: Int, placeHolderDrawableRes: Int? = null, @@ -150,7 +152,7 @@ sealed class ImageRequest( /** * Used to configure an instance of the ImageLoader */ -data class ImageLoaderConfig ( +data class ImageLoaderConfig( val largeSize: Int = 0, val defaultSize: Int = 0, val cacheFolder: File? diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/SubsonicImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/SubsonicImageLoader.kt deleted file mode 100644 index 712dd4df..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/SubsonicImageLoader.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.moire.ultrasonic.imageloader - -import android.content.Context -import com.squareup.picasso.Picasso -import com.squareup.picasso.RequestCreator -import org.moire.ultrasonic.BuildConfig -import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient - -class SubsonicImageLoader( - context: Context, - apiClient: SubsonicAPIClient -) { - private val picasso = Picasso.Builder(context) - .addRequestHandler(CoverArtRequestHandler(apiClient)) - .addRequestHandler(AvatarRequestHandler(apiClient)) - .build().apply { - setIndicatorsEnabled(BuildConfig.DEBUG) - Picasso.setSingletonInstance(this) - } - - fun load(request: ImageRequest) = when (request) { - is ImageRequest.CoverArt -> loadCoverArt(request) - is ImageRequest.Avatar -> loadAvatar(request) - } - - private fun loadCoverArt(request: ImageRequest.CoverArt) { - picasso.load(createLoadCoverArtRequest(request.entityId, request.size.toLong())) - .addPlaceholder(request) - .addError(request) - .stableKey("${request.entityId}-${request.size}") - .into(request.imageView) - } - - private fun loadAvatar(request: ImageRequest.Avatar) { - picasso.load(createLoadAvatarRequest(request.username)) - .addPlaceholder(request) - .addError(request) - .stableKey(request.username) - .into(request.imageView) - } - - private fun RequestCreator.addPlaceholder(request: ImageRequest): RequestCreator { - if (request.placeHolderDrawableRes != null) { - placeholder(request.placeHolderDrawableRes) - } - - return this - } - - private fun RequestCreator.addError(request: ImageRequest): RequestCreator { - if (request.errorDrawableRes != null) { - error(request.errorDrawableRes) - } - - return this - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index 7b70a191..020d95d0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -259,10 +259,9 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, override fun getCoverArt( entry: MusicDirectory.Entry, size: Int, - saveToFile: Boolean, - highQuality: Boolean + saveToFile: Boolean ): Bitmap? { - return musicService.getCoverArt(entry, size, saveToFile, highQuality) + return musicService.getCoverArt(entry, size, saveToFile) } @Throws(Exception::class) @@ -451,10 +450,9 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, override fun getAvatar( username: String?, size: Int, - saveToFile: Boolean, - highQuality: Boolean + saveToFile: Boolean ): Bitmap? { - return musicService.getAvatar(username, size, saveToFile, highQuality) + return musicService.getAvatar(username, size, saveToFile) } companion object { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index fd98da9b..5babce2b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -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 @@ -332,8 +333,9 @@ class DownloadFile( private fun downloadAndSaveCoverArt(musicService: MusicService) { 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 + val size = ImageLoaderProvider.config.largeSize + musicService.getCoverArt(song, size = size, saveToFile = true) } } catch (e: Exception) { Timber.e(e, "Failed to get cover art.") diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index 9a0b2938..3fd8f7e6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -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.getAlbumArtBitmapFromDisk( - 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.getAlbumArtBitmapFromDisk(song, iconSize, true) + val bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(song, iconSize) notificationBuilder!!.setContentTitle(song.title) notificationBuilder!!.setContentText(song.artist) notificationBuilder!!.setLargeIcon(bitmap) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index 90f95f29..dc3b8cee 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -115,12 +115,15 @@ interface MusicService { fun getCoverArt( entry: MusicDirectory.Entry, size: Int, - saveToFile: Boolean, - highQuality: Boolean + saveToFile: Boolean ): Bitmap? @Throws(Exception::class) - fun getAvatar(username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean): Bitmap? + fun getAvatar( + username: String?, + size: Int, + saveToFile: Boolean + ): Bitmap? /** * Return response [InputStream] and a [Boolean] that indicates if this response is diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index 788ca2d4..e6e0f425 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -40,6 +40,7 @@ import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.domain.UserInfo +import org.moire.ultrasonic.imageloader.BitmapUtils import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util @@ -121,11 +122,10 @@ class OfflineMusicService : MusicService, KoinComponent { override fun getAvatar( username: String?, size: Int, - saveToFile: Boolean, - highQuality: Boolean + saveToFile: Boolean ): Bitmap? { return try { - val bitmap = FileUtil.getAvatarBitmapFromDisk(username, size, highQuality) + val bitmap = BitmapUtils.getAvatarBitmapFromDisk(username, size) Util.scaleBitmap(bitmap, size) } catch (ignored: Exception) { null @@ -135,11 +135,10 @@ class OfflineMusicService : MusicService, KoinComponent { override fun getCoverArt( entry: MusicDirectory.Entry, size: Int, - saveToFile: Boolean, - highQuality: Boolean + saveToFile: Boolean ): Bitmap? { return try { - val bitmap = FileUtil.getAlbumArtBitmapFromDisk(entry, size, highQuality) + val bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(entry, size) Util.scaleBitmap(bitmap, size) } catch (ignored: Exception) { null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index b2200672..013fb7e5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -46,6 +46,7 @@ import org.moire.ultrasonic.domain.toDomainEntitiesList import org.moire.ultrasonic.domain.toDomainEntity import org.moire.ultrasonic.domain.toDomainEntityList import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity +import org.moire.ultrasonic.imageloader.BitmapUtils import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -488,21 +489,18 @@ open class RESTMusicService( return response.body()!!.starred2.toDomainEntity() } - // TODO: Implement file caching in Picasso CoverArtRequestHandler, - // and then use Picasso to handle this cache // This is only called by DownloadFile to cache the cover art for offline use @Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry, size: Int, - saveToFile: Boolean, - highQuality: Boolean + saveToFile: Boolean ): Bitmap? { // Synchronize on the entry so that we don't download concurrently for // the same song. synchronized(entry) { // Use cached file, if existing. - var bitmap = FileUtil.getAlbumArtBitmapFromDisk(entry, size, highQuality) + var bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(entry, size) val serverScaling = isServerScalingEnabled() if (bitmap == null) { @@ -541,7 +539,7 @@ open class RESTMusicService( } } - bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality) + bitmap = BitmapUtils.getSampledBitmap(bytes, size) } finally { Util.close(inputStream) } @@ -820,8 +818,7 @@ open class RESTMusicService( override fun getAvatar( username: String?, size: Int, - saveToFile: Boolean, - highQuality: Boolean + saveToFile: Boolean ): Bitmap? { // Synchronize on the username so that we don't download concurrently for // the same user. @@ -831,7 +828,7 @@ open class RESTMusicService( synchronized(username) { // Use cached file, if existing. - var bitmap = FileUtil.getAvatarBitmapFromDisk(username, size, highQuality) + var bitmap = BitmapUtils.getAvatarBitmapFromDisk(username, size) if (bitmap == null) { var inputStream: InputStream? = null @@ -858,7 +855,7 @@ open class RESTMusicService( } } - bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality) + bitmap = BitmapUtils.getSampledBitmap(bytes, size) } finally { Util.close(inputStream) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt index c674f1a6..6156b781 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt @@ -4,11 +4,12 @@ import android.content.Context import androidx.core.content.res.ResourcesCompat import org.koin.core.component.KoinComponent import org.koin.core.component.get +import org.koin.core.qualifier.named import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp -import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.imageloader.ImageLoaderConfig +import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util /** @@ -16,26 +17,7 @@ import org.moire.ultrasonic.util.Util */ class ImageLoaderProvider(val context: Context) : KoinComponent { private var imageLoader: ImageLoader? = null - - private val config by lazy { - var defaultSize = 0 - val fallbackImage = ResourcesCompat.getDrawable( - UApp.applicationContext().resources, R.drawable.unknown_album, null - ) - - // Determine the density-dependent image sizes by taking the fallback album - // image and querying its size. - if (fallbackImage != null) { - defaultSize = fallbackImage.intrinsicHeight - } - - ImageLoaderConfig( - Util.getMaxDisplayMetric(), - defaultSize, - FileUtil.getAlbumArtDirectory() - ) - - } + private var serverID: String = get(named("ServerID")) @Synchronized fun clearImageLoader() { @@ -44,9 +26,33 @@ class ImageLoaderProvider(val context: Context) : KoinComponent { @Synchronized fun getImageLoader(): ImageLoader { - if (imageLoader == null) { - imageLoader = ImageLoader(get(), get(), config) + // We need to generate a new ImageLoader if the server has changed... + val currentID = get(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() + ) + } + } } diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt index 214cdad8..8ea94f82 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarRequestHandlerTest.kt @@ -64,7 +64,8 @@ class AvatarRequestHandlerTest { .thenReturn(streamResponse) val response = handler.load( - createLoadAvatarRequest("some-username").buildRequest(), 0) + createLoadAvatarRequest("some-username").buildRequest(), 0 + ) response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK response.source `should not be` null diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt index 5bd6c336..121a08da 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt @@ -77,7 +77,8 @@ class CoverArtRequestHandlerTest { .thenReturn(streamResponse) val response = handler.load( - createLoadCoverArtRequest("some").buildRequest(), 0) + createLoadCoverArtRequest("some").buildRequest(), 0 + ) response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK response.source `should not be` null From 6dff5c5a23b37ab7404e5041a0cda322332f4db3 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 7 Jun 2021 14:37:01 +0200 Subject: [PATCH 09/15] Add mandatory stableKey to tests --- .../moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt index 121a08da..6245b16b 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandlerTest.kt @@ -84,5 +84,5 @@ class CoverArtRequestHandlerTest { response.source `should not be` null } - private fun Uri.buildRequest() = Request.Builder(this).build() + private fun Uri.buildRequest() = Request.Builder(this).stableKey("-1").build() } From 7182694c5defd5327f5c5126397f82a38af4a571 Mon Sep 17 00:00:00 2001 From: Nite Date: Mon, 7 Jun 2021 20:06:40 +0200 Subject: [PATCH 10/15] Fixed currentDownloading to be nullable (It can be null when nothing is downloading) --- .../org/moire/ultrasonic/service/MediaPlayerController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index e8f3c4f0..cd0f16c6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -489,7 +489,7 @@ class MediaPlayerController( val currentPlayingNumberOnPlaylist: Int get() = downloader.currentPlayingIndex - val currentDownloading: DownloadFile + val currentDownloading: DownloadFile? get() = downloader.currentDownloading val playList: List From fa6566e903282f63e47cdb71744bbfd7d5a26053 Mon Sep 17 00:00:00 2001 From: Nite Date: Mon, 7 Jun 2021 20:32:44 +0200 Subject: [PATCH 11/15] Fixed showing the unknown image when a source isn't specified --- .../kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index 66b5664f..15c7088a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -87,6 +87,8 @@ class ImageLoader( errorDrawableRes = defaultResourceId ) load(request) + } else if (view is ImageView) { + view.setImageResource(defaultResourceId) } } @@ -104,6 +106,8 @@ class ImageLoader( errorDrawableRes = R.drawable.ic_contact_picture ) load(request) + } else { + view.setImageResource(R.drawable.ic_contact_picture) } } From 78cb4d09cf06ce603dbe7f89753140b28524d669 Mon Sep 17 00:00:00 2001 From: Nite Date: Mon, 7 Jun 2021 21:38:39 +0200 Subject: [PATCH 12/15] Separated cache entries for large images --- .../src/main/java/org/moire/ultrasonic/util/FileUtil.java | 5 +++-- .../kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java index 28b80a0c..7c227bc8 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java @@ -129,9 +129,10 @@ public class FileUtil /** * 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) + public static String getAlbumArtKey(MusicDirectory.Entry entry, boolean large) { File albumDir = getAlbumDirectory(entry); File albumArtDir = getAlbumArtDirectory(); @@ -140,7 +141,7 @@ public class FileUtil return null; } - return String.format(Locale.ROOT, "%s.jpeg", Util.md5Hex(albumDir.getPath())); + return String.format(Locale.ROOT, "%s%b.jpeg", Util.md5Hex(albumDir.getPath()), large); } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index 15c7088a..f0a71f33 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -80,7 +80,7 @@ class ImageLoader( val requestedSize = resolveSize(size, large) if (id != null && id.isNotEmpty() && view is ImageView) { - val key = FileUtil.getAlbumArtKey(entry) + val key = FileUtil.getAlbumArtKey(entry, large) val request = ImageRequest.CoverArt( id, key, view, requestedSize, placeHolderDrawableRes = defaultResourceId, From 3c554caf2e7e379adb65114d021972f4ea6783b4 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 8 Jun 2021 13:47:47 +0200 Subject: [PATCH 13/15] On disk we are only caching the full-size images. So when 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. --- .../org/moire/ultrasonic/util/FileUtil.java | 6 +++++- .../imageloader/CoverArtRequestHandler.kt | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java index 7c227bc8..6bf557db 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java @@ -57,6 +57,8 @@ public class FileUtil private static final List VIDEO_FILE_EXTENSIONS = Arrays.asList("flv", "mp4", "m4v", "wmv", "avi", "mov", "mpg", "mkv"); private static final List 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 permissionUtil = inject(PermissionUtil.class); @@ -141,7 +143,9 @@ public class FileUtil return null; } - return String.format(Locale.ROOT, "%s%b.jpeg", Util.md5Hex(albumDir.getPath()), large); + String suffix = (large) ? SUFFIX_LARGE : SUFFIX_SMALL; + + return String.format(Locale.ROOT, "%s%s", Util.md5Hex(albumDir.getPath()), suffix); } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt index 3152e565..a6aeb048 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt @@ -7,6 +7,8 @@ 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. @@ -25,16 +27,23 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request val size = request.uri.getQueryParameter(SIZE)?.toLong() // Check if we have a hit in the disk cache - val cache = BitmapUtils.getAlbumArtBitmapFromDisk(request.stableKey!!, size?.toInt()) + // 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) { - throw IOException("${response.apiError}") - } else { + if (!response.hasError() && response.stream != null) { return Result(Okio.source(response.stream!!), NETWORK) } + + // Throw an error if still not successful + throw IOException("${response.apiError}") } } From be356d9c0a6ebf21c083e4be4efc3f92e8418ada Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 8 Jun 2021 17:12:55 +0200 Subject: [PATCH 14/15] Cleanup unused functions from RESTMusicService, put the caching functionality into the ImageLoader --- .../moire/ultrasonic/util/CacheCleaner.java | 3 +- .../org/moire/ultrasonic/util/FileUtil.java | 25 ++- .../ultrasonic/imageloader/ImageLoader.kt | 59 ++++++- .../ultrasonic/service/CachedMusicService.kt | 19 --- .../moire/ultrasonic/service/DownloadFile.kt | 8 +- .../moire/ultrasonic/service/MusicService.kt | 15 -- .../ultrasonic/service/OfflineMusicService.kt | 28 ---- .../ultrasonic/service/RESTMusicService.kt | 148 ++---------------- 8 files changed, 95 insertions(+), 210 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java index c09d6edc..0b80bae3 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java @@ -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(); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java index 6bf557db..3d3f8b27 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java @@ -137,9 +137,19 @@ public class FileUtil public static String getAlbumArtKey(MusicDirectory.Entry entry, boolean large) { File albumDir = getAlbumDirectory(entry); - File albumArtDir = getAlbumArtDirectory(); - if (albumArtDir == null || albumDir == null) { + 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; } @@ -149,6 +159,7 @@ public class FileUtil } + public static File getAvatarFile(String username) { File albumArtDir = getAlbumArtDirectory(); @@ -159,7 +170,7 @@ 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)); } /** @@ -170,20 +181,20 @@ public class FileUtil 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); } /** * Get the album art file for a given cache key - * @param cacheKey + * @param cacheKey The key (== the filename) * @return File object. Not guaranteed that it exists */ public static File getAlbumArtFile(String cacheKey) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index f0a71f33..52f7e5ef 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -1,23 +1,30 @@ 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, - apiClient: SubsonicAPIClient, + private val apiClient: SubsonicAPIClient, private val config: ImageLoaderConfig ) { @@ -111,6 +118,56 @@ class ImageLoader( } } + /** + * 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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index 020d95d0..30e5ff10 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -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,15 +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 - ): Bitmap? { - return musicService.getCoverArt(entry, size, saveToFile) - } - @Throws(Exception::class) override fun getDownloadInputStream( song: MusicDirectory.Entry, @@ -446,15 +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 - ): Bitmap? { - return musicService.getAvatar(username, size, saveToFile) - } - companion object { private const val MUSIC_DIR_CACHE_SIZE = 100 } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index 5babce2b..f8e88257 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -60,6 +60,7 @@ class DownloadFile( private var completeWhenDone = false private val downloader: Downloader by inject() + private val imageLoaderProvider: ImageLoaderProvider by inject() val progress: MutableLiveData = MutableLiveData(0) @@ -276,7 +277,7 @@ class DownloadFile( if (isCancelled) { throw Exception(String.format("Download of '%s' was cancelled", song)) } - downloadAndSaveCoverArt(musicService) + downloadAndSaveCoverArt() } if (isPlaying) { @@ -330,12 +331,11 @@ class DownloadFile( return String.format("DownloadTask (%s)", song) } - private fun downloadAndSaveCoverArt(musicService: MusicService) { + private fun downloadAndSaveCoverArt() { try { if (!TextUtils.isEmpty(song.coverArt)) { // Download the largest size that we can display in the UI - val size = ImageLoaderProvider.config.largeSize - musicService.getCoverArt(song, size = size, saveToFile = true) + imageLoaderProvider.getImageLoader().cacheCoverArt(song) } } catch (e: Exception) { Timber.e(e, "Failed to get cover art.") diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index dc3b8cee..b9e5f5f3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -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,20 +110,6 @@ interface MusicService { @Throws(Exception::class) fun getStarred2(): SearchResult - @Throws(Exception::class) - fun getCoverArt( - entry: MusicDirectory.Entry, - size: Int, - saveToFile: Boolean - ): Bitmap? - - @Throws(Exception::class) - fun getAvatar( - username: String?, - size: Int, - saveToFile: Boolean - ): Bitmap? - /** * Return response [InputStream] and a [Boolean] that indicates if this response is * partial. diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index e6e0f425..a4ad2ca9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -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 @@ -40,7 +39,6 @@ import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.domain.UserInfo -import org.moire.ultrasonic.imageloader.BitmapUtils import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util @@ -119,32 +117,6 @@ class OfflineMusicService : MusicService, KoinComponent { return result } - override fun getAvatar( - username: String?, - size: Int, - saveToFile: Boolean - ): Bitmap? { - return try { - val bitmap = BitmapUtils.getAvatarBitmapFromDisk(username, size) - Util.scaleBitmap(bitmap, size) - } catch (ignored: Exception) { - null - } - } - - override fun getCoverArt( - entry: MusicDirectory.Entry, - size: Int, - saveToFile: Boolean - ): Bitmap? { - return try { - val bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(entry, size) - Util.scaleBitmap(bitmap, size) - } catch (ignored: Exception) { - null - } - } - override fun search(criteria: SearchCriteria): SearchResult { val artists: MutableList = ArrayList() val albums: MutableList = ArrayList() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 013fb7e5..625a8af0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -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 @@ -46,7 +41,6 @@ import org.moire.ultrasonic.domain.toDomainEntitiesList import org.moire.ultrasonic.domain.toDomainEntity import org.moire.ultrasonic.domain.toDomainEntityList import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity -import org.moire.ultrasonic.imageloader.BitmapUtils import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -489,80 +483,6 @@ open class RESTMusicService( return response.body()!!.starred2.toDomainEntity() } - // This is only called by DownloadFile to cache the cover art for offline use - @Throws(Exception::class) - override fun getCoverArt( - entry: MusicDirectory.Entry, - size: Int, - saveToFile: Boolean - ): Bitmap? { - // Synchronize on the entry so that we don't download concurrently for - // the same song. - synchronized(entry) { - // Use cached file, if existing. - var bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(entry, size) - val serverScaling = isServerScalingEnabled() - - if (bitmap == null) { - Timber.d("Loading cover art for: %s", entry) - - val id = entry.coverArt - - // Can't load empty string ids - if (TextUtils.isEmpty(id)) { - return null - } - - 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 = BitmapUtils.getSampledBitmap(bytes, size) - } 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, @@ -811,64 +731,22 @@ open class RESTMusicService( } } - // TODO: Implement file caching in Picasso AvatarRequestHandler, - // and then use Picasso to handle this cache - // This method is called from nowhere (all avatars are loaded directly using Picasso) - @Throws(Exception::class) - override fun getAvatar( - username: String?, - size: Int, - saveToFile: 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 = BitmapUtils.getAvatarBitmapFromDisk(username, size) - - 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 = BitmapUtils.getSampledBitmap(bytes, size) - } 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" + + @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 + ) + } + } + } } } From 4eeff191aa6aeb746398101ea7073d80b9a195b7 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 8 Jun 2021 17:20:45 +0200 Subject: [PATCH 15/15] Add two ToDos --- .../org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt | 1 + .../main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt index 7b3a7600..6484208e 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt @@ -141,6 +141,7 @@ class SubsonicAPIClient( api.getAvatar(username).execute() } + // TODO: Move this to response checker private inline fun handleStreamResponse(apiCall: () -> Response): StreamResponse { val response = apiCall() return if (response.isSuccessful) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 625a8af0..d3e3e708 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -736,6 +736,7 @@ open class RESTMusicService( 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) {