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/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 deleted file mode 100644 index 7e242479..00000000 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.moire.ultrasonic.subsonic.loader.image - -import com.squareup.picasso.Picasso.LoadedFrom.NETWORK -import com.squareup.picasso.Request -import com.squareup.picasso.RequestHandler -import java.io.IOException -import okio.Okio -import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient - -/** - * Loads cover arts from subsonic api. - */ -class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : RequestHandler() { - override fun canHandleRequest(data: Request): Boolean { - return with(data.uri) { - scheme == SCHEME && - authority == AUTHORITY && - path == "/$COVER_ART_PATH" - } - } - - override fun load(request: Request, networkPolicy: Int): Result { - val id = request.uri.getQueryParameter(QUERY_ID) - ?: throw IllegalArgumentException("Nullable id") - - val response = apiClient.getCoverArt(id) - if (response.hasError() || response.stream == null) { - throw IOException("${response.apiError}") - } else { - return Result(Okio.source(response.stream!!), NETWORK) - } - } -} 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 deleted file mode 100644 index 9cc6799a..00000000 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.moire.ultrasonic.subsonic.loader.image - -import android.net.Uri - -internal const val SCHEME = "subsonic_api" -internal const val AUTHORITY = BuildConfig.LIBRARY_PACKAGE_NAME -internal const val COVER_ART_PATH = "cover_art" -internal const val AVATAR_PATH = "avatar" -internal const val QUERY_ID = "id" -internal const val QUERY_USERNAME = "username" - -internal fun createLoadCoverArtRequest(entityId: String): Uri = Uri.Builder() - .scheme(SCHEME) - .authority(AUTHORITY) - .appendPath(COVER_ART_PATH) - .appendQueryParameter(QUERY_ID, entityId) - .build() - -internal fun createLoadAvatarRequest(username: String): Uri = Uri.Builder() - .scheme(SCHEME) - .authority(AUTHORITY) - .appendPath(AVATAR_PATH) - .appendQueryParameter(QUERY_USERNAME, username) - .build() 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 deleted file mode 100644 index 630bbc4a..00000000 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt +++ /dev/null @@ -1,80 +0,0 @@ -package org.moire.ultrasonic.subsonic.loader.image - -import android.content.Context -import android.widget.ImageView -import com.squareup.picasso.Picasso -import com.squareup.picasso.RequestCreator -import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient - -class SubsonicImageLoader( - context: Context, - apiClient: SubsonicAPIClient -) { - private val picasso = Picasso.Builder(context) - .addRequestHandler(CoverArtRequestHandler(apiClient)) - .addRequestHandler(AvatarRequestHandler(apiClient)) - .build().apply { setIndicatorsEnabled(BuildConfig.DEBUG) } - - fun load(request: ImageRequest) = when (request) { - is ImageRequest.CoverArt -> loadCoverArt(request) - is ImageRequest.Avatar -> loadAvatar(request) - } - - private fun loadCoverArt(request: ImageRequest.CoverArt) { - picasso.load(createLoadCoverArtRequest(request.entityId)) - .addPlaceholder(request) - .addError(request) - .into(request.imageView) - } - - private fun loadAvatar(request: ImageRequest.Avatar) { - picasso.load(createLoadAvatarRequest(request.username)) - .addPlaceholder(request) - .addError(request) - .into(request.imageView) - } - - private fun RequestCreator.addPlaceholder(request: ImageRequest): RequestCreator { - if (request.placeHolderDrawableRes != null) { - placeholder(request.placeHolderDrawableRes) - } - - return this - } - - private fun RequestCreator.addError(request: ImageRequest): RequestCreator { - if (request.errorDrawableRes != null) { - error(request.errorDrawableRes) - } - - return this - } -} - -sealed class ImageRequest( - val placeHolderDrawableRes: Int? = null, - val errorDrawableRes: Int? = null, - val imageView: ImageView -) { - class CoverArt( - val entityId: String, - imageView: ImageView, - placeHolderDrawableRes: Int? = null, - errorDrawableRes: Int? = null - ) : ImageRequest( - placeHolderDrawableRes, - errorDrawableRes, - imageView - ) - - class Avatar( - val username: String, - imageView: ImageView, - placeHolderDrawableRes: Int? = null, - errorDrawableRes: Int? = null - ) : ImageRequest( - placeHolderDrawableRes, - errorDrawableRes, - imageView - ) -} 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..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 @@ -103,6 +103,7 @@ class SubsonicAPIClient( val api: SubsonicAPIDefinition get() = wrappedApi /** + * TODO: Remove this in favour of handling the stream response inside RESTService * Convenient method to get cover art from api using item [id] and optional maximum [size]. * * It detects the response `Content-Type` and tries to parse subsonic error if there is one. @@ -114,6 +115,7 @@ class SubsonicAPIClient( } /** + * TODO: Remove this in favour of handling the stream response inside RESTService * Convenient method to get media stream from api using item [id] and optional [maxBitrate]. * * Optionally also you can provide [offset] that stream should start from. @@ -128,6 +130,7 @@ class SubsonicAPIClient( } /** + * TODO: Remove this in favour of handling the stream response inside RESTService * Convenient method to get user avatar using [username]. * * It detects the response `Content-Type` and tries to parse subsonic error if there is one. @@ -138,6 +141,7 @@ class SubsonicAPIClient( api.getAvatar(username).execute() } + // TODO: Move this to response checker private inline fun handleStreamResponse(apiCall: () -> Response): StreamResponse { val response = apiCall() return if (response.isSuccessful) { 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/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/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/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..619484e2 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java @@ -9,17 +9,18 @@ import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.os.Environment; -import timber.log.Timber; import android.view.KeyEvent; import android.widget.RemoteViews; import org.moire.ultrasonic.R; import org.moire.ultrasonic.activity.NavigationActivity; import org.moire.ultrasonic.domain.MusicDirectory; +import org.moire.ultrasonic.imageloader.BitmapUtils; import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; import org.moire.ultrasonic.service.MediaPlayerController; import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.FileUtil; + +import timber.log.Timber; /** * Widget Provider for the Ultrasonic Widgets @@ -159,7 +160,7 @@ public class UltrasonicAppWidgetProvider extends AppWidgetProvider // Set the cover art try { - Bitmap bitmap = currentSong == null ? null : FileUtil.getAlbumArtBitmap(currentSong, 240, true); + Bitmap bitmap = currentSong == null ? null : BitmapUtils.Companion.getAlbumArtBitmapFromDisk(currentSong, 240); if (bitmap == null) { 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 95062dee..3d3f8b27 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java @@ -19,18 +19,12 @@ package org.moire.ultrasonic.util; import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.os.Build; import android.os.Environment; import android.text.TextUtils; -import kotlin.Lazy; -import timber.log.Timber; - import org.moire.ultrasonic.app.UApp; import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.subsonic.ImageLoaderProvider; import java.io.File; import java.io.FileInputStream; @@ -42,10 +36,14 @@ import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.SortedSet; import java.util.TreeSet; import java.util.regex.Pattern; +import kotlin.Lazy; +import timber.log.Timber; + import static org.koin.java.KoinJavaComponent.inject; /** @@ -59,8 +57,9 @@ public class FileUtil private static final List 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 imageLoaderProvider = inject(ImageLoaderProvider.class); private static final Lazy permissionUtil = inject(PermissionUtil.class); public static File getSongFile(MusicDirectory.Entry song) @@ -118,12 +117,49 @@ public class FileUtil return playlistDir; } + /** + * Get the album art file for a given album entry + * @param entry The album entry + * @return File object. Not guaranteed that it exists + */ public static File getAlbumArtFile(MusicDirectory.Entry entry) { File albumDir = getAlbumDirectory(entry); return getAlbumArtFile(albumDir); } + /** + * Get the cache key for a given album entry + * @param entry The album entry + * @param large Whether to get the key for the large or the default image + * @return String The hash key + */ + public static String getAlbumArtKey(MusicDirectory.Entry entry, boolean large) + { + File albumDir = getAlbumDirectory(entry); + + return getAlbumArtKey(albumDir, large); + } + + /** + * Get the cache key for a given album entry + * @param albumDir The album directory + * @param large Whether to get the key for the large or the default image + * @return String The hash key + */ + public static String getAlbumArtKey(File albumDir, boolean large) + { + if (albumDir == null) { + return null; + } + + String suffix = (large) ? SUFFIX_LARGE : SUFFIX_SMALL; + + return String.format(Locale.ROOT, "%s%s", Util.md5Hex(albumDir.getPath()), suffix); + } + + + public static File getAvatarFile(String username) { File albumArtDir = getAlbumArtDirectory(); @@ -134,173 +170,45 @@ public class FileUtil } String md5Hex = Util.md5Hex(username); - return new File(albumArtDir, String.format("%s.jpeg", md5Hex)); + return new File(albumArtDir, String.format("%s%s", md5Hex, SUFFIX_LARGE)); } + /** + * Get the album art file for a given album directory + * @param albumDir The album directory + * @return File object. Not guaranteed that it exists + */ public static File getAlbumArtFile(File albumDir) { File albumArtDir = getAlbumArtDirectory(); + String key = getAlbumArtKey(albumDir, true); - if (albumArtDir == null || albumDir == null) + if (key == null || albumArtDir == null) { return null; } - String md5Hex = Util.md5Hex(albumDir.getPath()); - return new File(albumArtDir, String.format("%s.jpeg", md5Hex)); + return new File(albumArtDir, key); } - public static Bitmap getAvatarBitmap(String username, int size, boolean highQuality) + + /** + * Get the album art file for a given cache key + * @param cacheKey The key (== the filename) + * @return File object. Not guaranteed that it exists + */ + public static File getAlbumArtFile(String cacheKey) { - if (username == null) return null; + File albumArtDir = getAlbumArtDirectory(); - File avatarFile = getAvatarFile(username); - - Bitmap bitmap = null; - ImageLoader imageLoader = imageLoaderProvider.getValue().getImageLoader(); - - if (imageLoader != null) + if (albumArtDir == null || cacheKey == null) { - bitmap = imageLoader.getImageBitmap(username, size); + return null; } - if (bitmap != null) - { - return bitmap.copy(bitmap.getConfig(), false); - } - - if (avatarFile != null && avatarFile.exists()) - { - final BitmapFactory.Options opt = new BitmapFactory.Options(); - - if (size > 0) - { - opt.inJustDecodeBounds = true; - BitmapFactory.decodeFile(avatarFile.getPath(), opt); - - if (highQuality) - { - opt.inDither = true; - opt.inPreferQualityOverSpeed = true; - } - - opt.inPurgeable = true; - opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size)); - opt.inJustDecodeBounds = false; - } - - try - { - bitmap = BitmapFactory.decodeFile(avatarFile.getPath(), opt); - } - catch (Exception ex) - { - Timber.e(ex, "Exception in BitmapFactory.decodeFile()"); - } - - Timber.i("getAvatarBitmap %s", String.valueOf(size)); - - if (bitmap != null) - { - if (imageLoader != null) - { - imageLoader.addImageToCache(bitmap, username, size); - } - } - - return bitmap; - } - - return null; + return new File(albumArtDir, cacheKey); } - public static Bitmap getAlbumArtBitmap(MusicDirectory.Entry entry, int size, boolean highQuality) - { - if (entry == null) return null; - - File albumArtFile = getAlbumArtFile(entry); - - Bitmap bitmap = null; - ImageLoader imageLoader = imageLoaderProvider.getValue().getImageLoader(); - - if (imageLoader != null) - { - bitmap = imageLoader.getImageBitmap(entry, true, size); - } - - if (bitmap != null) - { - return bitmap.copy(bitmap.getConfig(), false); - } - - if (albumArtFile != null && albumArtFile.exists()) - { - final BitmapFactory.Options opt = new BitmapFactory.Options(); - - if (size > 0) - { - opt.inJustDecodeBounds = true; - BitmapFactory.decodeFile(albumArtFile.getPath(), opt); - - if (highQuality) - { - opt.inDither = true; - opt.inPreferQualityOverSpeed = true; - } - - opt.inPurgeable = true; - opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size)); - opt.inJustDecodeBounds = false; - } - - try - { - bitmap = BitmapFactory.decodeFile(albumArtFile.getPath(), opt); - } - catch (Exception ex) - { - Timber.e(ex, "Exception in BitmapFactory.decodeFile()"); - } - - Timber.i("getAlbumArtBitmap %s", String.valueOf(size)); - - if (bitmap != null) - { - if (imageLoader != null) - { - imageLoader.addImageToCache(bitmap, entry, size); - } - } - - return bitmap; - } - - return null; - } - - public static Bitmap getSampledBitmap(byte[] bytes, int size, boolean highQuality) - { - final BitmapFactory.Options opt = new BitmapFactory.Options(); - - if (size > 0) - { - opt.inJustDecodeBounds = true; - BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt); - - if (highQuality) - { - opt.inDither = true; - opt.inPreferQualityOverSpeed = true; - } - - opt.inPurgeable = true; - opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size)); - opt.inJustDecodeBounds = false; - } - - Timber.i("getSampledBitmap %s", String.valueOf(size)); - return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt); - } public static File getAlbumArtDirectory() { 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 d45c67b2..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ImageLoader.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.moire.ultrasonic.util; - -import android.graphics.Bitmap; -import android.view.View; -import org.moire.ultrasonic.domain.MusicDirectory; - -public interface ImageLoader { - boolean isRunning(); - - void setConcurrency(int concurrency); - - void startImageLoader(); - - void stopImageLoader(); - - void loadAvatarImage(View view, String username, boolean large, int size, boolean crossFade, - boolean highQuality); - - void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, - boolean crossFade, boolean highQuality); - - void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, - boolean crossFade, boolean highQuality, int defaultResourceId); - - void cancel(String coverArt); - - Bitmap getImageBitmap(String username, int size); - - Bitmap getImageBitmap(MusicDirectory.Entry entry, boolean large, int size); - - void addImageToCache(Bitmap bitmap, MusicDirectory.Entry entry, int size); - - void addImageToCache(Bitmap bitmap, String username, int size); - - void clear(); -} 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/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/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/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index 285e91be..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.subsonic.loader.image.SubsonicImageLoader import org.moire.ultrasonic.util.Constants /** @@ -77,7 +78,7 @@ val musicServiceModule = module { OfflineMusicService() } - single { SubsonicImageLoader(androidContext(), get()) } + single { ImageLoader(androidContext(), get(), ImageLoaderProvider.config) } single { DownloadHandler(get(), get()) } single { NetworkAndStorageChecker(androidContext()) } 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..239fb237 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 @@ -29,7 +29,6 @@ class AlbumRowAdapter( ) : GenericRowAdapter( onItemClick, onContextMenuClick, - imageLoader, onMusicFolderUpdate ) { @@ -56,9 +55,8 @@ class AlbumRowAdapter( holder.coverArtId = entry.coverArt imageLoader.loadImage( - holder.coverArt, - MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId }, - false, 0, false, true, R.drawable.unknown_album + holder.coverArt, entry, + false, 0, R.drawable.unknown_album ) } } 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..69988607 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 /** @@ -30,7 +30,6 @@ class ArtistRowAdapter( ) : GenericRowAdapter( onItemClick, onContextMenuClick, - imageLoader, onMusicFolderUpdate ), SectionedAdapter { @@ -62,8 +61,11 @@ class ArtistRowAdapter( holder.coverArt.visibility = View.VISIBLE imageLoader.loadImage( holder.coverArt, - MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId }, - false, 0, false, true, R.drawable.ic_contact_picture + MusicDirectory.Entry("-1").apply { + coverArt = holder.coverArtId + artist = itemList[listPosition].name + }, + false, 0, R.drawable.ic_contact_picture ) } else { holder.coverArt.visibility = View.GONE 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/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/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 new file mode 100644 index 00000000..a6aeb048 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtRequestHandler.kt @@ -0,0 +1,49 @@ +package org.moire.ultrasonic.imageloader + +import com.squareup.picasso.Picasso.LoadedFrom.DISK +import com.squareup.picasso.Picasso.LoadedFrom.NETWORK +import com.squareup.picasso.Request +import com.squareup.picasso.RequestHandler +import java.io.IOException +import okio.Okio +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.util.FileUtil.SUFFIX_LARGE +import org.moire.ultrasonic.util.FileUtil.SUFFIX_SMALL + +/** + * Loads cover arts from subsonic api. + */ +class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : RequestHandler() { + override fun canHandleRequest(data: Request): Boolean { + return with(data.uri) { + scheme == SCHEME && + path == "/$COVER_ART_PATH" + } + } + + override fun load(request: Request, networkPolicy: Int): Result { + val id = request.uri.getQueryParameter(QUERY_ID) + ?: throw IllegalArgumentException("Nullable id") + val size = request.uri.getQueryParameter(SIZE)?.toLong() + + // Check if we have a hit in the disk cache + // Note: Currently we are only caching full size images on disk + // So we modify the key to query for the full size image, + // because scaling down a larger size image on the device is quicker than + // requesting the down-sized image from the network. + val key = request.stableKey!!.replace(SUFFIX_SMALL, SUFFIX_LARGE) + val cache = BitmapUtils.getAlbumArtBitmapFromDisk(key, size?.toInt()) + if (cache != null) { + return Result(cache, DISK) + } + + // Try to fetch the image from the API + val response = apiClient.getCoverArt(id, size) + if (!response.hasError() && response.stream != null) { + return Result(Okio.source(response.stream!!), NETWORK) + } + + // Throw an error if still not successful + throw IOException("${response.apiError}") + } +} 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..52f7e5ef --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -0,0 +1,220 @@ +package org.moire.ultrasonic.imageloader + +import android.content.Context +import android.text.TextUtils +import android.view.View +import android.widget.ImageView +import com.squareup.picasso.Picasso +import com.squareup.picasso.RequestCreator +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.io.OutputStream +import org.moire.ultrasonic.BuildConfig +import org.moire.ultrasonic.R +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.service.RESTMusicService +import org.moire.ultrasonic.util.FileUtil +import org.moire.ultrasonic.util.Util +import timber.log.Timber + +/** + * Our new image loader which uses Picasso as a backend. + */ +class ImageLoader( + context: Context, + private val apiClient: SubsonicAPIClient, + private val config: ImageLoaderConfig +) { + + private val picasso = Picasso.Builder(context) + .addRequestHandler(CoverArtRequestHandler(apiClient)) + .addRequestHandler(AvatarRequestHandler(apiClient)) + .build().apply { + setIndicatorsEnabled(BuildConfig.DEBUG) + } + + private fun load(request: ImageRequest) = when (request) { + is ImageRequest.CoverArt -> loadCoverArt(request) + is ImageRequest.Avatar -> loadAvatar(request) + } + + private fun loadCoverArt(request: ImageRequest.CoverArt) { + picasso.load(createLoadCoverArtRequest(request.entityId, request.size.toLong())) + .addPlaceholder(request) + .addError(request) + .stableKey(request.cacheKey) + .into(request.imageView) + } + + private fun loadAvatar(request: ImageRequest.Avatar) { + picasso.load(createLoadAvatarRequest(request.username)) + .addPlaceholder(request) + .addError(request) + .stableKey(request.username) + .into(request.imageView) + } + + private fun RequestCreator.addPlaceholder(request: ImageRequest): RequestCreator { + if (request.placeHolderDrawableRes != null) { + placeholder(request.placeHolderDrawableRes) + } + + return this + } + + private fun RequestCreator.addError(request: ImageRequest): RequestCreator { + if (request.errorDrawableRes != null) { + error(request.errorDrawableRes) + } + + return this + } + + /** + * Load the cover of a given entry into an ImageView + */ + @JvmOverloads + fun loadImage( + view: View?, + entry: MusicDirectory.Entry?, + large: Boolean, + size: Int, + defaultResourceId: Int = R.drawable.unknown_album + ) { + val id = entry?.coverArt + val requestedSize = resolveSize(size, large) + + if (id != null && id.isNotEmpty() && view is ImageView) { + val key = FileUtil.getAlbumArtKey(entry, large) + val request = ImageRequest.CoverArt( + id, key, view, requestedSize, + placeHolderDrawableRes = defaultResourceId, + errorDrawableRes = defaultResourceId + ) + load(request) + } else if (view is ImageView) { + view.setImageResource(defaultResourceId) + } + } + + /** + * Load the avatar of a given user into an ImageView + */ + fun loadAvatarImage( + view: ImageView, + username: String + ) { + if (username.isNotEmpty()) { + val request = ImageRequest.Avatar( + username, view, + placeHolderDrawableRes = R.drawable.ic_contact_picture, + errorDrawableRes = R.drawable.ic_contact_picture + ) + load(request) + } else { + view.setImageResource(R.drawable.ic_contact_picture) + } + } + + /** + * Download a cover art file and cache it on disk + */ + fun cacheCoverArt( + entry: MusicDirectory.Entry + ) { + + // Synchronize on the entry so that we don't download concurrently for + // the same song. + synchronized(entry) { + // Always download the large size.. + val size = config.largeSize + + // Check cache to avoid downloading existing files + val file = FileUtil.getAlbumArtFile(entry) + + // Return if have a cache hit + if (file.exists()) return + + // Can't load empty string ids + val id = entry.coverArt + if (TextUtils.isEmpty(id)) return + + // Query the API + Timber.d("Loading cover art for: %s", entry) + val response = apiClient.getCoverArt(id!!, size.toLong()) + RESTMusicService.checkStreamResponseError(response) + + // Check for failure + if (response.stream == null) return + + // Write Response stream to file + var inputStream: InputStream? = null + try { + inputStream = response.stream + val bytes = Util.toByteArray(inputStream) + + var outputStream: OutputStream? = null + try { + outputStream = FileOutputStream(file) + outputStream.write(bytes) + } finally { + Util.close(outputStream) + } + } finally { + Util.close(inputStream) + } + } + } + + private fun resolveSize(requested: Int, large: Boolean): Int { + if (requested <= 0) { + return if (large) config.largeSize else config.defaultSize + } else { + return requested + } + } +} + +/** + * Data classes to hold all the info we need later on to process the request + */ +sealed class ImageRequest( + val placeHolderDrawableRes: Int? = null, + val errorDrawableRes: Int? = null, + val imageView: ImageView +) { + class CoverArt( + val entityId: String, + val cacheKey: String, + imageView: ImageView, + val size: Int, + placeHolderDrawableRes: Int? = null, + errorDrawableRes: Int? = null, + ) : ImageRequest( + placeHolderDrawableRes, + errorDrawableRes, + imageView + ) + + class Avatar( + val username: String, + imageView: ImageView, + placeHolderDrawableRes: Int? = null, + errorDrawableRes: Int? = null + ) : ImageRequest( + placeHolderDrawableRes, + errorDrawableRes, + imageView + ) +} + +/** + * Used to configure an instance of the ImageLoader + */ +data class ImageLoaderConfig( + val largeSize: Int = 0, + val defaultSize: Int = 0, + val cacheFolder: File? +) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/RequestCreator.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/RequestCreator.kt new file mode 100644 index 00000000..37a11f5a --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/RequestCreator.kt @@ -0,0 +1,29 @@ +package org.moire.ultrasonic.imageloader + +import android.net.Uri + +internal const val SCHEME = "subsonic_api" +internal const val COVER_ART_PATH = "cover_art" +internal const val AVATAR_PATH = "avatar" +internal const val QUERY_ID = "id" +internal const val SIZE = "size" +internal const val QUERY_USERNAME = "username" + +/** + * Picasso.load() only accepts an URI as parameter. Therefore we create a bogus URI, in which + * we encode the data that we need in the RequestHandler. + */ +internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri = + Uri.Builder() + .scheme(SCHEME) + .appendPath(COVER_ART_PATH) + .appendQueryParameter(QUERY_ID, entityId) + .appendQueryParameter(SIZE, size.toString()) + .build() + +internal fun createLoadAvatarRequest(username: String): Uri = + Uri.Builder() + .scheme(SCHEME) + .appendPath(AVATAR_PATH) + .appendQueryParameter(QUERY_USERNAME, username) + .build() 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..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,16 +254,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, @Throws(Exception::class) override fun getStarred2(): SearchResult = musicService.getStarred2() - @Throws(Exception::class) - override fun getCoverArt( - entry: MusicDirectory.Entry?, - size: Int, - saveToFile: Boolean, - highQuality: Boolean - ): Bitmap? { - return musicService.getCoverArt(entry, size, saveToFile, highQuality) - } - @Throws(Exception::class) override fun getDownloadInputStream( song: MusicDirectory.Entry, @@ -447,16 +436,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, musicService.updateShare(id, description, expires) } - @Throws(Exception::class) - override fun getAvatar( - username: String?, - size: Int, - saveToFile: Boolean, - highQuality: Boolean - ): Bitmap? { - return musicService.getAvatar(username, size, saveToFile, highQuality) - } - companion object { private const val MUSIC_DIR_CACHE_SIZE = 100 } 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..f8e88257 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 @@ -59,6 +60,7 @@ class DownloadFile( private var completeWhenDone = false private val downloader: Downloader by inject() + private val imageLoaderProvider: ImageLoaderProvider by inject() val progress: MutableLiveData = MutableLiveData(0) @@ -275,7 +277,7 @@ class DownloadFile( if (isCancelled) { throw Exception(String.format("Download of '%s' was cancelled", song)) } - downloadAndSaveCoverArt(musicService) + downloadAndSaveCoverArt() } if (isPlaying) { @@ -329,11 +331,11 @@ class DownloadFile( return String.format("DownloadTask (%s)", song) } - private fun downloadAndSaveCoverArt(musicService: MusicService) { + private fun downloadAndSaveCoverArt() { try { if (!TextUtils.isEmpty(song.coverArt)) { - val size = Util.getMinDisplayMetric() - musicService.getCoverArt(song, size, true, true) + // Download the largest size that we can display in the UI + imageLoaderProvider.getImageLoader().cacheCoverArt(song) } } catch (e: Exception) { Timber.e(e, "Failed to get cover art.") 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 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..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.getAlbumArtBitmap( - song, Util.getMinDisplayMetric(), - true + val cover = BitmapUtils.getAlbumArtBitmapFromDisk( + song, Util.getMinDisplayMetric() ) metadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, -1L) metadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.artist) @@ -648,7 +647,7 @@ class MediaPlayerService : Service() { // Set song title, artist and cover if possible if (song != null) { val iconSize = (256 * context.resources.displayMetrics.density).toInt() - val bitmap = FileUtil.getAlbumArtBitmap(song, iconSize, true) + val bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(song, iconSize) notificationBuilder!!.setContentTitle(song.title) notificationBuilder!!.setContentText(song.artist) notificationBuilder!!.setLargeIcon(bitmap) 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..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,17 +110,6 @@ interface MusicService { @Throws(Exception::class) fun getStarred2(): SearchResult - @Throws(Exception::class) - fun getCoverArt( - entry: MusicDirectory.Entry?, - size: Int, - saveToFile: Boolean, - highQuality: Boolean - ): Bitmap? - - @Throws(Exception::class) - fun getAvatar(username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean): Bitmap? - /** * Return response [InputStream] and a [Boolean] that indicates if this response is * partial. 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..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 @@ -118,34 +117,6 @@ class OfflineMusicService : MusicService, KoinComponent { return result } - override fun getAvatar( - username: String?, - size: Int, - saveToFile: Boolean, - highQuality: Boolean - ): Bitmap? { - return try { - val bitmap = FileUtil.getAvatarBitmap(username, size, highQuality) - Util.scaleBitmap(bitmap, size) - } catch (ignored: Exception) { - null - } - } - - override fun getCoverArt( - entry: MusicDirectory.Entry?, - size: Int, - saveToFile: Boolean, - highQuality: Boolean - ): Bitmap? { - return try { - val bitmap = FileUtil.getAlbumArtBitmap(entry, size, highQuality) - Util.scaleBitmap(bitmap, size) - } catch (ignored: Exception) { - null - } - } - override fun search(criteria: SearchCriteria): SearchResult { val artists: MutableList = 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 fd9f2de6..d3e3e708 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 @@ -488,83 +483,6 @@ open class RESTMusicService( return response.body()!!.starred2.toDomainEntity() } - @Throws(Exception::class) - override fun getCoverArt( - entry: MusicDirectory.Entry?, - size: Int, - saveToFile: Boolean, - highQuality: Boolean - ): Bitmap? { - // Synchronize on the entry so that we don't download concurrently for - // the same song. - if (entry == null) { - return null - } - - synchronized(entry) { - // Use cached file, if existing. - var bitmap = FileUtil.getAlbumArtBitmap(entry, size, highQuality) - val serverScaling = isServerScalingEnabled() - - if (bitmap == null) { - Timber.d("Loading cover art for: %s", entry) - - val id = entry.coverArt - - if (TextUtils.isEmpty(id)) { - return null // Can't load - } - - val response = subsonicAPIClient.getCoverArt(id!!, size.toLong()) - checkStreamResponseError(response) - - if (response.stream == null) { - return null // Failed to load - } - - var inputStream: InputStream? = null - try { - inputStream = response.stream - val bytes = Util.toByteArray(inputStream) - - // If we aren't allowing server-side scaling, always save the file to disk - // because it will be unmodified - if (!serverScaling || saveToFile) { - var outputStream: OutputStream? = null - try { - outputStream = FileOutputStream( - FileUtil.getAlbumArtFile(entry) - ) - outputStream.write(bytes) - } finally { - Util.close(outputStream) - } - } - - bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality) - } finally { - Util.close(inputStream) - } - } - - // Return scaled bitmap - return Util.scaleBitmap(bitmap, size) - } - } - - @Throws(SubsonicRESTException::class, IOException::class) - private fun checkStreamResponseError(response: StreamResponse) { - if (response.hasError() || response.stream == null) { - if (response.apiError != null) { - throw SubsonicRESTException(response.apiError!!) - } else { - throw IOException( - "Failed to make endpoint request, code: " + response.responseHttpCode - ) - } - } - } - @Throws(Exception::class) override fun getDownloadInputStream( song: MusicDirectory.Entry, @@ -813,62 +731,23 @@ open class RESTMusicService( } } - @Throws(Exception::class) - override fun getAvatar( - username: String?, - size: Int, - saveToFile: Boolean, - highQuality: Boolean - ): Bitmap? { - // Synchronize on the username so that we don't download concurrently for - // the same user. - if (username == null) { - return null - } - - synchronized(username) { - // Use cached file, if existing. - var bitmap = FileUtil.getAvatarBitmap(username, size, highQuality) - - if (bitmap == null) { - var inputStream: InputStream? = null - try { - val response = subsonicAPIClient.getAvatar(username) - - if (response.hasError()) return null - - inputStream = response.stream - val bytes = Util.toByteArray(inputStream) - - // If we aren't allowing server-side scaling, always save the file to disk - // because it will be unmodified - if (saveToFile) { - var outputStream: OutputStream? = null - - try { - outputStream = FileOutputStream( - FileUtil.getAvatarFile(username) - ) - outputStream.write(bytes) - } finally { - Util.close(outputStream) - } - } - - bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality) - } finally { - Util.close(inputStream) - } - } - - // Return scaled bitmap - return Util.scaleBitmap(bitmap, size) - } - } - companion object { private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder" private const val INDEXES_STORAGE_NAME = "indexes" private const val ARTISTS_STORAGE_NAME = "artists" + + // TODO: Move to response checker + @Throws(SubsonicRESTException::class, IOException::class) + fun checkStreamResponseError(response: StreamResponse) { + if (response.hasError() || response.stream == null) { + if (response.apiError != null) { + throw SubsonicRESTException(response.apiError!!) + } else { + throw IOException( + "Failed to make endpoint request, code: " + response.responseHttpCode + ) + } + } + } } } 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..6156b781 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt @@ -1,12 +1,15 @@ package org.moire.ultrasonic.subsonic import android.content.Context +import androidx.core.content.res.ResourcesCompat import org.koin.core.component.KoinComponent import org.koin.core.component.get -import org.moire.ultrasonic.featureflags.Feature -import org.moire.ultrasonic.featureflags.FeatureStorage -import org.moire.ultrasonic.util.ImageLoader -import org.moire.ultrasonic.util.LegacyImageLoader +import org.koin.core.qualifier.named +import org.moire.ultrasonic.R +import org.moire.ultrasonic.app.UApp +import org.moire.ultrasonic.imageloader.ImageLoader +import org.moire.ultrasonic.imageloader.ImageLoaderConfig +import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util /** @@ -14,37 +17,42 @@ import org.moire.ultrasonic.util.Util */ class ImageLoaderProvider(val context: Context) : KoinComponent { private var imageLoader: ImageLoader? = null + private var serverID: String = get(named("ServerID")) @Synchronized fun clearImageLoader() { - if ( - imageLoader != null && - imageLoader!!.isRunning - ) { - imageLoader!!.clear() - } imageLoader = null } @Synchronized fun getImageLoader(): ImageLoader { - if (imageLoader == null || !imageLoader!!.isRunning) { - val legacyImageLoader = LegacyImageLoader( - context, - Util.getImageLoaderConcurrency() - ) - val features: FeatureStorage = get() - val isNewImageLoaderEnabled = features.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER) - imageLoader = if (isNewImageLoaderEnabled) { - SubsonicImageLoaderProxy( - legacyImageLoader, - get() - ) - } else { - legacyImageLoader - } - imageLoader!!.startImageLoader() + // We need to generate a new ImageLoader if the server has changed... + val currentID = get(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/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt deleted file mode 100644 index 6d0d2b99..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt +++ /dev/null @@ -1,80 +0,0 @@ -package org.moire.ultrasonic.subsonic - -import android.view.View -import android.widget.ImageView -import org.moire.ultrasonic.R -import org.moire.ultrasonic.domain.MusicDirectory -import org.moire.ultrasonic.subsonic.loader.image.ImageRequest -import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader -import org.moire.ultrasonic.util.ImageLoader -import org.moire.ultrasonic.util.LegacyImageLoader - -/** - * Temporary proxy between new [SubsonicImageLoader] and [ImageLoader] interface and old - * [LegacyImageLoader] implementation. - * - * Should be removed on [LegacyImageLoader] removal. - */ -class SubsonicImageLoaderProxy( - legacyImageLoader: LegacyImageLoader, - private val subsonicImageLoader: SubsonicImageLoader -) : ImageLoader by legacyImageLoader { - override fun loadImage( - view: View?, - entry: MusicDirectory.Entry?, - large: Boolean, - size: Int, - crossFade: Boolean, - highQuality: Boolean - ) { - return loadImage(view, entry, large, size, crossFade, highQuality, -1) - } - - override fun loadImage( - view: View?, - entry: MusicDirectory.Entry?, - large: Boolean, - size: Int, - crossFade: Boolean, - highQuality: Boolean, - defaultResourceId: Int - ) { - val id = entry?.coverArt - val unknownImageId = - if (defaultResourceId == -1) R.drawable.unknown_album - else defaultResourceId - - if (id != null && - view != null && - view is ImageView - ) { - val request = ImageRequest.CoverArt( - id, view, - placeHolderDrawableRes = unknownImageId, - errorDrawableRes = unknownImageId - ) - subsonicImageLoader.load(request) - } - } - - override fun loadAvatarImage( - view: View?, - username: String?, - large: Boolean, - size: Int, - crossFade: Boolean, - highQuality: Boolean - ) { - if (username != null && - view != null && - view is ImageView - ) { - val request = ImageRequest.Avatar( - username, view, - placeHolderDrawableRes = R.drawable.ic_contact_picture, - errorDrawableRes = R.drawable.ic_contact_picture - ) - subsonicImageLoader.load(request) - } - } -} 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/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/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. 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"/> - - () - private val handler = AvatarRequestHandler(mockSubsonicApiClient) + private val handler = + AvatarRequestHandler(mockSubsonicApiClient) @Test fun `Should accept only cover art request`() { @@ -34,7 +35,6 @@ class AvatarRequestHandlerTest { fun `Should not accept random request uri`() { val requestUri = Uri.Builder() .scheme(SCHEME) - .authority(AUTHORITY) .appendPath("something") .build() @@ -63,7 +63,9 @@ class AvatarRequestHandlerTest { whenever(mockSubsonicApiClient.getAvatar(any())) .thenReturn(streamResponse) - val response = handler.load(createLoadAvatarRequest("some-username").buildRequest(), 0) + val response = handler.load( + createLoadAvatarRequest("some-username").buildRequest(), 0 + ) response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK response.source `should not be` null 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 88% 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..6245b16b 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,11 +76,13 @@ class CoverArtRequestHandlerTest { whenever(mockSubsonicApiClientMock.getCoverArt(any(), anyOrNull())) .thenReturn(streamResponse) - val response = handler.load(createLoadCoverArtRequest("some").buildRequest(), 0) + val response = handler.load( + createLoadCoverArtRequest("some").buildRequest(), 0 + ) response.loadedFrom `should be equal to` Picasso.LoadedFrom.NETWORK response.source `should not be` null } - private fun Uri.buildRequest() = Request.Builder(this).build() + private fun Uri.buildRequest() = Request.Builder(this).stableKey("-1").build() } 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 59% 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 b318d60d..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 @@ -11,15 +11,17 @@ class RequestCreatorTest { @Test fun `Should create valid load cover art request`() { val entityId = "299" - val expectedUri = Uri.parse("$SCHEME://$AUTHORITY/$COVER_ART_PATH?$QUERY_ID=$entityId") + val size = 100L + val expectedUri = + Uri.parse("$SCHEME:/$COVER_ART_PATH?$QUERY_ID=$entityId&$SIZE=$size") - createLoadCoverArtRequest(entityId).compareTo(expectedUri).shouldBeEqualTo(0) + createLoadCoverArtRequest(entityId, size).compareTo(expectedUri).shouldBeEqualTo(0) } @Test fun `Should create valid avatar request`() { val username = "some-username" - val expectedUri = Uri.parse("$SCHEME://$AUTHORITY/$AVATAR_PATH?$QUERY_USERNAME=$username") + val expectedUri = Uri.parse("$SCHEME:/$AVATAR_PATH?$QUERY_USERNAME=$username") createLoadAvatarRequest(username).compareTo(expectedUri).shouldBeEqualTo(0) } 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