Merge pull request #206 from ultrasonic/add-image-loader

Add new image loader implementation
This commit is contained in:
Yahor Berdnikau 2018-07-15 09:55:22 +02:00 committed by GitHub
commit 7f36c74060
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1299 additions and 640 deletions

View File

@ -21,12 +21,15 @@ ext.versions = [
semver : "1.0.0", semver : "1.0.0",
twitterSerial : "0.1.6", twitterSerial : "0.1.6",
koin : "0.9.3", koin : "0.9.3",
picasso : "2.71828",
junit : "4.12", junit : "4.12",
mockito : "2.16.0", mockito : "2.16.0",
mockitoKotlin : "1.5.0", mockitoKotlin : "1.5.0",
kluent : "1.35", kluent : "1.35",
apacheCodecs : "1.10", apacheCodecs : "1.10",
testRunner : "1.0.1",
robolectric : "3.8",
] ]
ext.gradlePlugins = [ ext.gradlePlugins = [
@ -40,6 +43,7 @@ ext.gradlePlugins = [
ext.androidSupport = [ ext.androidSupport = [
support : "com.android.support:support-v4:$versions.androidSupport", support : "com.android.support:support-v4:$versions.androidSupport",
design : "com.android.support:design:$versions.androidSupport", design : "com.android.support:design:$versions.androidSupport",
annotations : "com.android.support:support-annotations:$versions.androidSupport"
] ]
ext.other = [ ext.other = [
@ -53,7 +57,8 @@ ext.other = [
semver : "net.swiftzer.semver:semver:$versions.semver", semver : "net.swiftzer.semver:semver:$versions.semver",
twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial", twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial",
koinCore : "org.koin:koin-core:$versions.koin", koinCore : "org.koin:koin-core:$versions.koin",
koinAndroid : "org.koin:koin-android:$versions.koin" koinAndroid : "org.koin:koin-android:$versions.koin",
picasso : "com.squareup.picasso:picasso:$versions.picasso",
] ]
ext.testing = [ ext.testing = [
@ -63,6 +68,9 @@ ext.testing = [
mockito : "org.mockito:mockito-core:$versions.mockito", mockito : "org.mockito:mockito-core:$versions.mockito",
mockitoInline : "org.mockito:mockito-inline:$versions.mockito", mockitoInline : "org.mockito:mockito-inline:$versions.mockito",
kluent : "org.amshove.kluent:kluent:$versions.kluent", kluent : "org.amshove.kluent:kluent:$versions.kluent",
kluentAndroid : "org.amshove.kluent:kluent-android:$versions.kluent",
mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp", mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp",
apacheCodecs : "commons-codec:commons-codec:$versions.apacheCodecs", apacheCodecs : "commons-codec:commons-codec:$versions.apacheCodecs",
testRunner : "com.android.support.test:runner:$versions.testRunner",
robolectric : "org.robolectric:robolectric:$versions.robolectric",
] ]

View File

@ -21,6 +21,7 @@ def createJacocoFullReportTask() {
description = "Generate full Jacoco coverage report including all modules." description = "Generate full Jacoco coverage report including all modules."
def subsonicApi = project.findProject("subsonic-api") def subsonicApi = project.findProject("subsonic-api")
def subsonicApiImageLoader = project.findProject("subsonic-api-image-loader")
def ultrasonicApp = project.findProject("ultrasonic") def ultrasonicApp = project.findProject("ultrasonic")
def cache = project.findProject("cache") def cache = project.findProject("cache")
@ -29,6 +30,10 @@ def createJacocoFullReportTask() {
dir: "${subsonicApi.buildDir}/classes/main", dir: "${subsonicApi.buildDir}/classes/main",
excludes: subsonicApi.jacocoExclude excludes: subsonicApi.jacocoExclude
), ),
fileTree(
dir: "${subsonicApiImageLoader.buildDir}/intermediates/classes/debug/org",
excludes: subsonicApiImageLoader.jacocoExclude
),
fileTree( fileTree(
dir: "${ultrasonicApp.buildDir}/intermediates/classes/debug/org", dir: "${ultrasonicApp.buildDir}/intermediates/classes/debug/org",
excludes: ultrasonicApp.jacocoExclude excludes: ultrasonicApp.jacocoExclude
@ -38,8 +43,12 @@ def createJacocoFullReportTask() {
excludes: cache.jacocoExclude excludes: cache.jacocoExclude
) )
) )
sourceDirectories = files(subsonicApi.sourceSets.main.getAllSource(), sourceDirectories = files(
ultrasonicApp.extensions.getByName('android').sourceSets.main.java.sourceFiles) subsonicApi.sourceSets.main.getAllSource(),
subsonicApiImageLoader.extensions.getByName('android').sourceSets.main.java.sourceFiles,
ultrasonicApp.extensions.getByName('android').sourceSets.main.java.sourceFiles,
cache.sourceSets.main.getAllSource(),
)
executionData = files("${buildDir}/jacoco/jacoco.exec") executionData = files("${buildDir}/jacoco/jacoco.exec")
reports { reports {

View File

@ -1,6 +1,7 @@
include ':library' include ':library'
include ':domain' include ':domain'
include ':subsonic-api' include ':subsonic-api'
include ':subsonic-api-image-loader'
include ':cache' include ':cache'
include ':menudrawer' include ':menudrawer'
include ':pulltorefresh' include ':pulltorefresh'

View File

@ -0,0 +1,55 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'jacoco-android'
apply from: '../gradle_scripts/code_quality.gradle'
android {
compileSdkVersion(versions.compileSdk)
defaultConfig {
minSdkVersion(versions.minSdk)
targetSdkVersion(versions.targetSdk)
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test.java.srcDirs += 'src/test/kotlin'
test.java.srcDirs += "${projectDir}/src/integrationTest/kotlin"
test.resources.srcDirs += "${projectDir}/src/integrationTest/resources"
}
}
dependencies {
api project(':domain')
api project(':subsonic-api')
api other.kotlinStdlib
api(other.picasso) {
exclude group: "com.android.support"
}
testImplementation testing.junit
testImplementation testing.kotlinJunit
testImplementation testing.mockito
testImplementation testing.mockitoInline
testImplementation testing.mockitoKotlin
testImplementation testing.kluent
testImplementation testing.robolectric
}
jacoco {
toolVersion(versions.jacoco)
}
ext {
jacocoExclude = []
}
jacocoAndroidUnitTestReport {
excludes += jacocoExclude
}
afterEvaluate {
testDebugUnitTest.finalizedBy jacocoTestDebugUnitTestReport
}

View File

@ -0,0 +1,73 @@
package org.moire.ultrasonic.subsonic.loader.image
import android.net.Uri
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever
import com.squareup.picasso.Picasso
import com.squareup.picasso.Request
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.amshove.kluent.`should throw`
import org.amshove.kluent.shouldEqualTo
import org.junit.Test
import org.junit.runner.RunWith
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class AvatarRequestHandlerTest {
private val mockSubsonicApiClient = mock<SubsonicAPIClient>()
private val handler = AvatarRequestHandler(mockSubsonicApiClient)
@Test
fun `Should accept only cover art request`() {
val requestUri = createLoadAvatarRequest("some-username")
handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo true
}
@Test
fun `Should not accept random request uri`() {
val requestUri = Uri.Builder()
.scheme(SCHEME)
.authority(AUTHORITY)
.appendPath("something")
.build()
handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo false
}
@Test
fun `Should fail loading if uri doesn't contain username`() {
var requestUri = createLoadAvatarRequest("some-username")
requestUri = requestUri.buildUpon().clearQuery().build()
val fail = {
handler.load(requestUri.buildRequest(), 0)
}
fail `should throw` IllegalStateException::class
}
@Test
fun `Should load avatar from network`() {
val streamResponse = StreamResponse(
loadResourceStream("Big_Buck_Bunny.jpeg"),
apiError = null,
responseHttpCode = 200
)
whenever(mockSubsonicApiClient.getAvatar(any()))
.thenReturn(streamResponse)
val response = handler.load(createLoadAvatarRequest("some-username").buildRequest(), 0)
response.loadedFrom `should equal` Picasso.LoadedFrom.NETWORK
response.source `should not be` null
}
private fun Uri.buildRequest() = Request.Builder(this).build()
}

View File

@ -0,0 +1,9 @@
package org.moire.ultrasonic.subsonic.loader.image
import okio.Okio
import java.io.InputStream
fun Any.loadResourceStream(name: String): InputStream {
val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
return source.inputStream()
}

View File

@ -0,0 +1,86 @@
package org.moire.ultrasonic.subsonic.loader.image
import android.net.Uri
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.anyOrNull
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever
import com.squareup.picasso.Picasso
import com.squareup.picasso.Request
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.amshove.kluent.`should throw`
import org.amshove.kluent.shouldEqualTo
import org.junit.Test
import org.junit.runner.RunWith
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
import org.robolectric.RobolectricTestRunner
import java.io.IOException
@RunWith(RobolectricTestRunner::class)
class CoverArtRequestHandlerTest {
private val mockSubsonicApiClientMock = mock<SubsonicAPIClient>()
private val handler = CoverArtRequestHandler(mockSubsonicApiClientMock)
@Test
fun `Should accept only cover art request`() {
val requestUri = createLoadCoverArtRequest("some-id")
handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo true
}
@Test
fun `Should not accept random request uri`() {
val requestUri = Uri.Builder()
.scheme(SCHEME)
.authority(AUTHORITY)
.appendPath("random")
.build()
handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo false
}
@Test
fun `Should fail loading if uri doesn't contain id`() {
var requestUri = createLoadCoverArtRequest("some-id")
requestUri = requestUri.buildUpon().clearQuery().build()
val fail = {
handler.load(requestUri.buildRequest(), 0)
}
fail `should throw` IllegalStateException::class
}
@Test
fun `Should throw IOException when request to api failed`() {
val streamResponse = StreamResponse(null, null, 500)
whenever(mockSubsonicApiClientMock.getCoverArt(any(), anyOrNull()))
.thenReturn(streamResponse)
val fail = {
handler.load(createLoadCoverArtRequest("some").buildRequest(), 0)
}
fail `should throw` IOException::class
}
@Test
fun `Should load bitmap from network`() {
val streamResponse = StreamResponse(
loadResourceStream("Big_Buck_Bunny.jpeg"),
apiError = null,
responseHttpCode = 200
)
whenever(mockSubsonicApiClientMock.getCoverArt(any(), anyOrNull()))
.thenReturn(streamResponse)
val response = handler.load(createLoadCoverArtRequest("some").buildRequest(), 0)
response.loadedFrom `should equal` Picasso.LoadedFrom.NETWORK
response.source `should not be` null
}
private fun Uri.buildRequest() = Request.Builder(this).build()
}

View File

@ -0,0 +1,26 @@
package org.moire.ultrasonic.subsonic.loader.image
import android.net.Uri
import org.amshove.kluent.shouldEqualTo
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
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")
createLoadCoverArtRequest(entityId).compareTo(expectedUri).shouldEqualTo(0)
}
@Test
fun `Should create valid avatar request`() {
val username = "some-username"
val expectedUri = Uri.parse("$SCHEME://$AUTHORITY/$AVATAR_PATH?$QUERY_USERNAME=$username")
createLoadAvatarRequest(username).compareTo(expectedUri).shouldEqualTo(0)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

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

View File

@ -0,0 +1,34 @@
package org.moire.ultrasonic.subsonic.loader.image
import com.squareup.picasso.Picasso
import com.squareup.picasso.Request
import com.squareup.picasso.RequestHandler
import okio.Okio
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import java.io.IOException
/**
* Loads avatars from subsonic api.
*/
class AvatarRequestHandler(
private val apiClient: SubsonicAPIClient
) : RequestHandler() {
override fun canHandleRequest(data: Request): Boolean {
return with(data.uri) {
scheme == SCHEME &&
authority == AUTHORITY &&
path == "/$AVATAR_PATH"
}
}
override fun load(request: Request, networkPolicy: Int): Result {
val username = request.uri.getQueryParameter(QUERY_USERNAME)
val response = apiClient.getAvatar(username)
if (response.hasError()) {
throw IOException("${response.apiError}")
} else {
return Result(Okio.source(response.stream), Picasso.LoadedFrom.NETWORK)
}
}
}

View File

@ -0,0 +1,32 @@
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 okio.Okio
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import java.io.IOException
/**
* 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)
val response = apiClient.getCoverArt(id)
if (response.hasError()) {
throw IOException("${response.apiError}")
} else {
return Result(Okio.source(response.stream), NETWORK)
}
}
}

View File

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

View File

@ -0,0 +1,80 @@
package org.moire.ultrasonic.subsonic.loader.image
import android.content.Context
import android.widget.ImageView
import com.squareup.picasso.Picasso
import com.squareup.picasso.RequestCreator
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
class SubsonicImageLoader(
context: Context,
apiClient: SubsonicAPIClient
) {
private val picasso = Picasso.Builder(context)
.addRequestHandler(CoverArtRequestHandler(apiClient))
.addRequestHandler(AvatarRequestHandler(apiClient))
.build().apply { setIndicatorsEnabled(BuildConfig.DEBUG) }
fun load(request: ImageRequest) = when (request) {
is ImageRequest.CoverArt -> loadCoverArt(request)
is ImageRequest.Avatar -> loadAvatar(request)
}
private fun loadCoverArt(request: ImageRequest.CoverArt) {
picasso.load(createLoadCoverArtRequest(request.entityId))
.addPlaceholder(request)
.addError(request)
.into(request.imageView)
}
private fun loadAvatar(request: ImageRequest.Avatar) {
picasso.load(createLoadAvatarRequest(request.username))
.addPlaceholder(request)
.addError(request)
.into(request.imageView)
}
private fun RequestCreator.addPlaceholder(request: ImageRequest): RequestCreator {
if (request.placeHolderDrawableRes != null) {
placeholder(request.placeHolderDrawableRes)
}
return this
}
private fun RequestCreator.addError(request: ImageRequest): RequestCreator {
if (request.errorDrawableRes != null) {
error(request.errorDrawableRes)
}
return this
}
}
sealed class ImageRequest(
val placeHolderDrawableRes: Int? = null,
val errorDrawableRes: Int? = null,
val imageView: ImageView
) {
class CoverArt(
val entityId: String,
imageView: ImageView,
placeHolderDrawableRes: Int? = null,
errorDrawableRes: Int? = null
) : ImageRequest(
placeHolderDrawableRes,
errorDrawableRes,
imageView
)
class Avatar(
val username: String,
imageView: ImageView,
placeHolderDrawableRes: Int? = null,
errorDrawableRes: Int? = null
) : ImageRequest(
placeHolderDrawableRes,
errorDrawableRes,
imageView
)
}

View File

@ -55,6 +55,7 @@ dependencies {
implementation project(':library') implementation project(':library')
implementation project(':domain') implementation project(':domain')
implementation project(':subsonic-api') implementation project(':subsonic-api')
implementation project(':subsonic-api-image-loader')
implementation project(':cache') implementation project(':cache')
implementation androidSupport.support implementation androidSupport.support

View File

@ -1,205 +1,205 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:a="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.moire.ultrasonic" package="org.moire.ultrasonic"
a:installLocation="auto"> android:installLocation="auto">
<uses-permission a:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission a:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission a:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission a:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission a:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission a:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission a:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH"/>
<supports-screens <supports-screens
a:anyDensity="true" android:anyDensity="true"
a:largeScreens="true" android:largeScreens="true"
a:normalScreens="true" android:normalScreens="true"
a:smallScreens="true" android:smallScreens="true"
a:xlargeScreens="true"/> android:xlargeScreens="true"/>
<application <application
a:allowBackup="false" android:allowBackup="false"
a:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
a:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
a:theme="@style/Theme.AppCompat" android:theme="@style/Theme.AppCompat"
a:name=".app.UApp" android:name=".app.UApp"
a:label="@string/common.appname"> android:label="@string/common.appname">
<activity <activity
a:name=".activity.MainActivity" android:name=".activity.MainActivity"
a:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
a:label="UltraSonic" android:label="UltraSonic"
a:launchMode="standard"> android:launchMode="standard">
<intent-filter> <intent-filter>
<action a:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category a:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
a:name=".activity.SelectArtistActivity" android:name=".activity.SelectArtistActivity"
a:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
a:launchMode="standard"/> android:launchMode="standard"/>
<activity <activity
a:name=".activity.SelectAlbumActivity" android:name=".activity.SelectAlbumActivity"
a:configChanges="orientation|keyboardHidden"/> android:configChanges="orientation|keyboardHidden"/>
<activity <activity
a:name=".activity.SearchActivity" android:name=".activity.SearchActivity"
a:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
a:label="@string/search.label" android:label="@string/search.label"
a:launchMode="singleTask"/> android:launchMode="singleTask"/>
<activity <activity
a:name=".activity.SelectPlaylistActivity" android:name=".activity.SelectPlaylistActivity"
a:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
a:label="@string/playlist.label" android:label="@string/playlist.label"
a:launchMode="standard"/> android:launchMode="standard"/>
<activity <activity
a:name=".activity.PodcastsActivity" android:name=".activity.PodcastsActivity"
a:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
a:label="@string/podcasts.label" android:label="@string/podcasts.label"
a:launchMode="standard"/> android:launchMode="standard"/>
<activity <activity
a:name=".activity.BookmarkActivity" android:name=".activity.BookmarkActivity"
a:configChanges="orientation|keyboardHidden"/> android:configChanges="orientation|keyboardHidden"/>
<activity <activity
a:name=".activity.ShareActivity" android:name=".activity.ShareActivity"
a:configChanges="orientation|keyboardHidden"/> android:configChanges="orientation|keyboardHidden"/>
<activity <activity
a:name=".activity.ChatActivity" android:name=".activity.ChatActivity"
a:configChanges="orientation|keyboardHidden"/> android:configChanges="orientation|keyboardHidden"/>
<activity <activity
a:name=".activity.DownloadActivity" android:name=".activity.DownloadActivity"
a:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
a:launchMode="singleTask" android:launchMode="singleTask"
a:exported="true" /> android:exported="true" />
<activity <activity
a:name=".activity.SettingsActivity" android:name=".activity.SettingsActivity"
a:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
a:launchMode="singleTask"/> android:launchMode="singleTask"/>
<activity <activity
a:name=".activity.HelpActivity" android:name=".activity.HelpActivity"
a:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
a:launchMode="singleTask"/> android:launchMode="singleTask"/>
<activity <activity
a:name=".activity.LyricsActivity" android:name=".activity.LyricsActivity"
a:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
a:launchMode="singleTask"/> android:launchMode="singleTask"/>
<activity <activity
a:name=".activity.EqualizerActivity" android:name=".activity.EqualizerActivity"
a:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
a:label="@string/equalizer.label" android:label="@string/equalizer.label"
a:launchMode="singleTask"/> android:launchMode="singleTask"/>
<activity <activity
a:name=".activity.SelectGenreActivity" android:name=".activity.SelectGenreActivity"
a:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
a:launchMode="standard"/> android:launchMode="standard"/>
<activity <activity
a:name=".activity.VoiceQueryReceiverActivity" android:name=".activity.VoiceQueryReceiverActivity"
a:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action a:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/> <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<category a:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
a:name=".activity.QueryReceiverActivity" android:name=".activity.QueryReceiverActivity"
a:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action a:name="android.intent.action.SEARCH"/> <action android:name="android.intent.action.SEARCH"/>
<category a:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
</intent-filter> </intent-filter>
<meta-data <meta-data
a:name="android.app.searchable" android:name="android.app.searchable"
a:resource="@xml/searchable"/> android:resource="@xml/searchable"/>
</activity> </activity>
<activity a:name=".activity.ServerSettingsActivity" /> <activity android:name=".activity.ServerSettingsActivity" />
<service <service
a:name=".service.DownloadServiceImpl" android:name=".service.DownloadServiceImpl"
a:label="UltraSonic Download Service" android:label="UltraSonic Download Service"
a:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action a:name="org.moire.ultrasonic.CMD_TOGGLEPAUSE"/> <action android:name="org.moire.ultrasonic.CMD_TOGGLEPAUSE"/>
<action a:name="org.moire.ultrasonic.CMD_PLAY"/> <action android:name="org.moire.ultrasonic.CMD_PLAY"/>
<action a:name="org.moire.ultrasonic.CMD_PAUSE"/> <action android:name="org.moire.ultrasonic.CMD_PAUSE"/>
<action a:name="org.moire.ultrasonic.CMD_NEXT"/> <action android:name="org.moire.ultrasonic.CMD_NEXT"/>
<action a:name="org.moire.ultrasonic.CMD_PREVIOUS"/> <action android:name="org.moire.ultrasonic.CMD_PREVIOUS"/>
<action a:name="org.moire.ultrasonic.CMD_STOP"/> <action android:name="org.moire.ultrasonic.CMD_STOP"/>
</intent-filter> </intent-filter>
</service> </service>
<receiver a:name=".receiver.MediaButtonIntentReceiver"> <receiver android:name=".receiver.MediaButtonIntentReceiver">
<intent-filter a:priority="2147483647"> <intent-filter android:priority="2147483647">
<action a:name="android.intent.action.MEDIA_BUTTON"/> <action android:name="android.intent.action.MEDIA_BUTTON"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver a:name=".receiver.BluetoothIntentReceiver"> <receiver android:name=".receiver.BluetoothIntentReceiver">
<intent-filter> <intent-filter>
<action a:name="android.bluetooth.device.action.ACL_CONNECTED"/> <action android:name="android.bluetooth.device.action.ACL_CONNECTED"/>
<action a:name="android.bluetooth.device.action.ACL_DISCONNECTED"/> <action android:name="android.bluetooth.device.action.ACL_DISCONNECTED"/>
<action a:name="android.bluetooth.device.action.ACL_DISCONNECT_REQUESTED"/> <action android:name="android.bluetooth.device.action.ACL_DISCONNECT_REQUESTED"/>
<action a:name="android.bluetooth.a2dp.action.SINK_STATE_CHANGED"/> <action android:name="android.bluetooth.a2dp.action.SINK_STATE_CHANGED"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver <receiver
a:name=".provider.UltraSonicAppWidgetProvider4x1" android:name=".provider.UltraSonicAppWidgetProvider4x1"
a:label="UltraSonic (4x1)"> android:label="UltraSonic (4x1)">
<intent-filter> <intent-filter>
<action a:name="android.appwidget.action.APPWIDGET_UPDATE"/> <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter> </intent-filter>
<meta-data <meta-data
a:name="android.appwidget.provider" android:name="android.appwidget.provider"
a:resource="@xml/appwidget_info_4x1"/> android:resource="@xml/appwidget_info_4x1"/>
</receiver> </receiver>
<receiver <receiver
a:name=".provider.UltraSonicAppWidgetProvider4x2" android:name=".provider.UltraSonicAppWidgetProvider4x2"
a:label="UltraSonic (4x2)"> android:label="UltraSonic (4x2)">
<intent-filter> <intent-filter>
<action a:name="android.appwidget.action.APPWIDGET_UPDATE"/> <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter> </intent-filter>
<meta-data <meta-data
a:name="android.appwidget.provider" android:name="android.appwidget.provider"
a:resource="@xml/appwidget_info_4x2"/> android:resource="@xml/appwidget_info_4x2"/>
</receiver> </receiver>
<receiver <receiver
a:name=".provider.UltraSonicAppWidgetProvider4x3" android:name=".provider.UltraSonicAppWidgetProvider4x3"
a:label="UltraSonic (4x3)"> android:label="UltraSonic (4x3)">
<intent-filter> <intent-filter>
<action a:name="android.appwidget.action.APPWIDGET_UPDATE"/> <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter> </intent-filter>
<meta-data <meta-data
a:name="android.appwidget.provider" android:name="android.appwidget.provider"
a:resource="@xml/appwidget_info_4x3"/> android:resource="@xml/appwidget_info_4x3"/>
</receiver> </receiver>
<receiver <receiver
a:name=".provider.UltraSonicAppWidgetProvider4x4" android:name=".provider.UltraSonicAppWidgetProvider4x4"
a:label="UltraSonic (4x4)"> android:label="UltraSonic (4x4)">
<intent-filter> <intent-filter>
<action a:name="android.appwidget.action.APPWIDGET_UPDATE"/> <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter> </intent-filter>
<meta-data <meta-data
a:name="android.appwidget.provider" android:name="android.appwidget.provider"
a:resource="@xml/appwidget_info_4x4"/> android:resource="@xml/appwidget_info_4x4"/>
</receiver> </receiver>
<provider <provider
a:name=".provider.SearchSuggestionProvider" android:name=".provider.SearchSuggestionProvider"
a:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"/> android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"/>
<meta-data <meta-data
a:name="android.app.default_searchable" android:name="android.app.default_searchable"
a:value="org.moire.ultrasonic.activity.QueryReceiverActivity"/> android:value="org.moire.ultrasonic.activity.QueryReceiverActivity"/>
<receiver <receiver
a:name=".receiver.A2dpIntentReceiver" android:name=".receiver.A2dpIntentReceiver"
a:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action a:name="com.android.music.playstatusrequest"/> <action android:name="com.android.music.playstatusrequest"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
</application> </application>

View File

@ -20,7 +20,6 @@ package org.moire.ultrasonic.activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
@ -33,54 +32,26 @@ import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent; import android.view.*;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.View.OnTouchListener; import android.view.View.OnTouchListener;
import android.view.ViewGroup; import android.widget.*;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RemoteViews;
import android.widget.TextView;
import net.simonvt.menudrawer.MenuDrawer; import net.simonvt.menudrawer.MenuDrawer;
import net.simonvt.menudrawer.Position; import net.simonvt.menudrawer.Position;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.app.UApp;
import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.domain.Share; import org.moire.ultrasonic.domain.Share;
import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.featureflags.Feature;
import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.*;
import org.moire.ultrasonic.service.DownloadServiceImpl; import org.moire.ultrasonic.subsonic.SubsonicImageLoaderProxy;
import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.util.*;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator;
import org.moire.ultrasonic.util.ImageLoader;
import org.moire.ultrasonic.util.ModalBackgroundTask;
import org.moire.ultrasonic.util.ShareDetails;
import org.moire.ultrasonic.util.SilentBackgroundTask;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.TimeSpan;
import org.moire.ultrasonic.util.TimeSpanPicker;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.util.VideoPlayerType;
import java.io.File; import java.io.File;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.ArrayList; import java.util.*;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
@ -820,21 +791,39 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
} }
} }
public synchronized void clearImageLoader() public synchronized void clearImageLoader() {
{ if (IMAGE_LOADER != null &&
if (IMAGE_LOADER != null && IMAGE_LOADER.isRunning()) IMAGE_LOADER.clear(); IMAGE_LOADER.isRunning()) {
} IMAGE_LOADER.clear();
}
public synchronized ImageLoader getImageLoader() IMAGE_LOADER = null;
{ }
if (IMAGE_LOADER == null || !IMAGE_LOADER.isRunning())
{
IMAGE_LOADER = new ImageLoader(this, Util.getImageLoaderConcurrency(this));
IMAGE_LOADER.startImageLoader();
}
return IMAGE_LOADER; public synchronized ImageLoader getImageLoader() {
} if (IMAGE_LOADER == null ||
!IMAGE_LOADER.isRunning()) {
LegacyImageLoader legacyImageLoader = new LegacyImageLoader(
this,
Util.getImageLoaderConcurrency(this)
);
boolean isNewImageLoaderEnabled = ((UApp) getApplication()).getFeaturesStorage()
.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER);
if (isNewImageLoaderEnabled) {
IMAGE_LOADER = new SubsonicImageLoaderProxy(
legacyImageLoader,
((UApp) getApplication()).getSubsonicImageLoader()
);
} else {
IMAGE_LOADER = legacyImageLoader;
}
IMAGE_LOADER.startImageLoader();
}
return IMAGE_LOADER;
}
void download(final boolean append, final boolean save, final boolean autoPlay, final boolean playNext, final boolean shuffle, final List<Entry> songs) void download(final boolean append, final boolean save, final boolean autoPlay, final boolean playNext, final boolean shuffle, final List<Entry> songs)
{ {

View File

@ -4,29 +4,21 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.CheckBoxPreference; import android.preference.*;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.provider.SearchRecentSuggestions; import android.provider.SearchRecentSuggestions;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.ServerSettingsActivity; import org.moire.ultrasonic.activity.ServerSettingsActivity;
import org.moire.ultrasonic.activity.SubsonicTabActivity; import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.app.UApp;
import org.moire.ultrasonic.featureflags.Feature;
import org.moire.ultrasonic.featureflags.FeatureStorage;
import org.moire.ultrasonic.provider.SearchSuggestionProvider; import org.moire.ultrasonic.provider.SearchSuggestionProvider;
import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.DownloadService;
import org.moire.ultrasonic.service.DownloadServiceImpl; import org.moire.ultrasonic.service.DownloadServiceImpl;
import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.*;
import org.moire.ultrasonic.util.FileUtil;
import org.moire.ultrasonic.util.ImageLoader;
import org.moire.ultrasonic.util.TimeSpanPreference;
import org.moire.ultrasonic.util.Util;
import java.io.File; import java.io.File;
@ -115,6 +107,7 @@ public class SettingsFragment extends PreferenceFragment
sharingDefaultGreeting.setText(Util.getShareGreeting(getActivity())); sharingDefaultGreeting.setText(Util.getShareGreeting(getActivity()));
setupClearSearchPreference(); setupClearSearchPreference();
setupGaplessControlSettingsV14(); setupGaplessControlSettingsV14();
setupFeatureFlagsPreferences();
} }
@Override @Override
@ -178,6 +171,24 @@ public class SettingsFragment extends PreferenceFragment
} }
} }
private void setupFeatureFlagsPreferences() {
CheckBoxPreference ffImageLoader = (CheckBoxPreference) findPreference(
Constants.PREFERENCES_KEY_FF_IMAGE_LOADER);
final FeatureStorage featureStorage = ((UApp) getActivity().getApplication()).getFeaturesStorage();
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);
((SubsonicTabActivity) getActivity()).clearImageLoader();
return true;
}
});
}
}
private void setupGaplessControlSettingsV14() { private void setupGaplessControlSettingsV14() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
PreferenceCategory playbackControlSettings = PreferenceCategory playbackControlSettings =

View File

@ -130,6 +130,7 @@ public final class Constants
public static final String PREFERENCES_KEY_SHOW_ALL_SONGS_BY_ARTIST = "showAllSongsByArtist"; public static final String PREFERENCES_KEY_SHOW_ALL_SONGS_BY_ARTIST = "showAllSongsByArtist";
public static final String PREFERENCES_KEY_SCAN_MEDIA = "scanMedia"; public static final String PREFERENCES_KEY_SCAN_MEDIA = "scanMedia";
public static final String PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY = "imageLoaderConcurrency"; public static final String PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY = "imageLoaderConcurrency";
public static final String PREFERENCES_KEY_FF_IMAGE_LOADER = "ff_new_image_loader";
// Number of free trial days for non-licensed servers. // Number of free trial days for non-licensed servers.
public static final int FREE_TRIAL_DAYS = 30; public static final int FREE_TRIAL_DAYS = 30;

View File

@ -1,462 +1,43 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.util; package org.moire.ultrasonic.util;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap; 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 android.util.Log;
import android.view.View; import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import java.util.ArrayList; public interface ImageLoader {
import java.util.Collection; boolean isRunning();
import java.util.Collections;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
/** void setConcurrency(int concurrency);
* Asynchronous loading of images, with caching.
* <p/>
* There should normally be only one instance of this class.
*
* @author Sindre Mehus
*/
public class ImageLoader implements Runnable
{
private static final String TAG = ImageLoader.class.getSimpleName();
private final LRUCache<String, Bitmap> cache = new LRUCache<String, Bitmap>(150); void startImageLoader();
private final BlockingQueue<Task> queue;
private int imageSizeDefault;
private final int imageSizeLarge;
private Bitmap largeUnknownImage;
private Bitmap unknownAvatarImage;
private Context context;
private Collection<Thread> threads;
private AtomicBoolean running = new AtomicBoolean();
private int concurrency;
public ImageLoader(Context context, int concurrency) void stopImageLoader();
{
this.context = context;
this.concurrency = concurrency;
queue = new LinkedBlockingQueue<Task>(1000);
Resources resources = context.getResources(); void loadAvatarImage(
Drawable drawable = resources.getDrawable(R.drawable.unknown_album); View view,
String username,
boolean large,
int size,
boolean crossFade,
boolean highQuality
);
// Determine the density-dependent image sizes. void loadImage(
if (drawable != null) View view,
{ MusicDirectory.Entry entry,
imageSizeDefault = drawable.getIntrinsicHeight(); boolean large,
} int size,
boolean crossFade,
boolean highQuality
);
imageSizeLarge = Util.getMaxDisplayMetric(context); Bitmap getImageBitmap(String username, int size);
createLargeUnknownImage(context);
createUnknownAvatarImage(context);
}
public synchronized boolean isRunning() Bitmap getImageBitmap(MusicDirectory.Entry entry, boolean large, int size);
{
return running.get() && !threads.isEmpty();
}
public void setConcurrency(int concurrency) void addImageToCache(Bitmap bitmap, MusicDirectory.Entry entry, int size);
{
this.concurrency = concurrency;
}
public void startImageLoader() void addImageToCache(Bitmap bitmap, String username, int size);
{
running.set(true);
threads = Collections.synchronizedCollection(new ArrayList<Thread>(this.concurrency)); void clear();
for (int i = 0; i < this.concurrency; i++)
{
Thread thread = new Thread(this, String.format("ImageLoader_%d", i));
threads.add(thread);
thread.start();
}
}
public synchronized void stopImageLoader()
{
clear();
for (Thread thread : threads)
{
thread.interrupt();
}
running.set(false);
threads.clear();
}
private void createLargeUnknownImage(Context context)
{
BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album_large);
Log.i(TAG, "createLargeUnknownImage");
if (drawable != null)
{
largeUnknownImage = Util.scaleBitmap(drawable.getBitmap(), imageSizeLarge);
}
}
private void createUnknownAvatarImage(Context context)
{
Resources res = context.getResources();
Drawable contact = res.getDrawable(R.drawable.ic_contact_picture);
unknownAvatarImage = Util.createBitmapFromDrawable(contact);
}
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));
}
public void loadImage(View view, MusicDirectory.Entry entry, boolean large, int size, boolean crossFade, boolean highQuality)
{
view.invalidate();
if (entry == null)
{
setUnknownImage(view, large);
return;
}
String coverArt = entry.getCoverArt();
if (TextUtils.isEmpty(coverArt)) {
setUnknownImage(view, large);
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);
queue.offer(new Task(view, entry, size, large, crossFade, highQuality));
}
private static String getKey(String coverArtId, int size)
{
return String.format("%s:%d", coverArtId, size);
}
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;
}
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))
{
Log.i(TAG, "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))
{
Log.i(TAG, "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);
}
}
}
public void setUnknownAvatarImage(View view)
{
setAvatarImageBitmap(view, null, unknownAvatarImage, false);
}
public void setUnknownImage(View view, boolean large)
{
if (large)
{
setImageBitmap(view, null, largeUnknownImage, false);
}
else
{
if (view instanceof TextView)
{
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0);
}
else if (view instanceof ImageView)
{
((ImageView) view).setImageResource(R.drawable.unknown_album);
}
}
}
public void addImageToCache(Bitmap bitmap, MusicDirectory.Entry entry, int size)
{
cache.put(getKey(entry.getCoverArt(), size), bitmap);
}
public void addImageToCache(Bitmap bitmap, String username, int size)
{
cache.put(getKey(username, size), bitmap);
}
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)
{
Log.e(TAG, "Unexpected exception in ImageLoader.", x);
}
}
}
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;
public 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();
}
public 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(view.getContext());
final boolean isAvatar = this.username != null && this.entry == null;
final Bitmap bitmap = this.entry != null ? musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) : musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null);
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)
{
Log.e(TAG, "Failed to download album art.", x);
}
}
}
} }

View File

@ -0,0 +1,450 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.util;
import android.content.Context;
import android.content.res.Resources;
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 android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
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.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Asynchronous loading of images, with caching.
* <p/>
* There should normally be only one instance of this class.
*
* @author Sindre Mehus
*/
public class LegacyImageLoader implements Runnable, ImageLoader {
private static final String TAG = LegacyImageLoader.class.getSimpleName();
private final LRUCache<String, Bitmap> cache = new LRUCache<>(150);
private final BlockingQueue<Task> queue;
private int imageSizeDefault;
private final int imageSizeLarge;
private Bitmap largeUnknownImage;
private Bitmap unknownAvatarImage;
private Context context;
private Collection<Thread> threads;
private AtomicBoolean running = new AtomicBoolean();
private int concurrency;
public LegacyImageLoader(
Context context,
int concurrency
) {
this.context = context;
this.concurrency = concurrency;
queue = new LinkedBlockingQueue<>(1000);
Resources resources = context.getResources();
Drawable drawable = resources.getDrawable(R.drawable.unknown_album);
// Determine the density-dependent image sizes.
if (drawable != null) {
imageSizeDefault = drawable.getIntrinsicHeight();
}
imageSizeLarge = Util.getMaxDisplayMetric(context);
createLargeUnknownImage(context);
createUnknownAvatarImage(context);
}
@Override
public synchronized boolean isRunning() {
return running.get() && !threads.isEmpty();
}
@Override
public void setConcurrency(int concurrency) {
this.concurrency = concurrency;
}
@Override
public void startImageLoader() {
running.set(true);
threads = Collections.synchronizedCollection(new ArrayList<Thread>(this.concurrency));
for (int i = 0; i < this.concurrency; i++) {
Thread thread = new Thread(this, String.format("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) {
BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album_large);
Log.i(TAG, "createLargeUnknownImage");
if (drawable != null) {
largeUnknownImage = Util.scaleBitmap(drawable.getBitmap(), imageSizeLarge);
}
}
private void createUnknownAvatarImage(Context context) {
Resources res = context.getResources();
Drawable contact = res.getDrawable(R.drawable.ic_contact_picture);
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
) {
view.invalidate();
if (entry == null) {
setUnknownImage(view, large);
return;
}
String coverArt = entry.getCoverArt();
if (TextUtils.isEmpty(coverArt)) {
setUnknownImage(view, large);
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);
queue.offer(new Task(view, entry, size, large, crossFade, highQuality));
}
private static String getKey(String coverArtId, int size) {
return String.format("%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)) {
Log.i(TAG, "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)) {
Log.i(TAG, "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) {
if (large) {
setImageBitmap(view, null, largeUnknownImage, false);
} else {
if (view instanceof TextView) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0);
} else if (view instanceof ImageView) {
((ImageView) view).setImageResource(R.drawable.unknown_album);
}
}
}
@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) {
Log.e(TAG, "Unexpected exception in ImageLoader.", x);
}
}
}
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(view.getContext());
final boolean isAvatar = this.username != null && this.entry == null;
final Bitmap bitmap = this.entry != null
? musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null)
: musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null);
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) {
Log.e(TAG, "Failed to download album art.", x);
}
}
}
}

View File

@ -25,7 +25,6 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicService;

View File

@ -8,7 +8,6 @@ import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.SubsonicTabActivity; import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.ChatMessage; import org.moire.ultrasonic.domain.ChatMessage;

View File

@ -24,7 +24,6 @@ import android.widget.ArrayAdapter;
import android.widget.CheckedTextView; import android.widget.CheckedTextView;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.moire.ultrasonic.activity.SubsonicTabActivity; import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.util.ImageLoader; import org.moire.ultrasonic.util.ImageLoader;

View File

@ -1,10 +1,14 @@
package org.moire.ultrasonic.app package org.moire.ultrasonic.app
import android.app.Application import android.app.Application
import org.koin.android.ext.android.get
import org.koin.android.ext.android.startKoin import org.koin.android.ext.android.startKoin
import org.moire.ultrasonic.di.baseNetworkModule import org.moire.ultrasonic.di.baseNetworkModule
import org.moire.ultrasonic.di.directoriesModule import org.moire.ultrasonic.di.directoriesModule
import org.moire.ultrasonic.di.featureFlagsModule
import org.moire.ultrasonic.di.musicServiceModule import org.moire.ultrasonic.di.musicServiceModule
import org.moire.ultrasonic.featureflags.FeatureStorage
import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
class UApp : Application() { class UApp : Application() {
@ -15,7 +19,22 @@ class UApp : Application() {
startKoin(this, listOf( startKoin(this, listOf(
directoriesModule, directoriesModule,
baseNetworkModule, baseNetworkModule,
musicServiceModule(sharedPreferences) featureFlagsModule(this),
musicServiceModule(sharedPreferences, this)
)) ))
} }
/**
* Temporary method to get subsonic image loader from java code.
*/
fun getSubsonicImageLoader(): SubsonicImageLoader {
return get()
}
/**
* Temporary method to get features storage.
*/
fun getFeaturesStorage(): FeatureStorage {
return get()
}
} }

View File

@ -0,0 +1,11 @@
package org.moire.ultrasonic.di
import android.content.Context
import org.koin.dsl.module.applicationContext
import org.moire.ultrasonic.featureflags.FeatureStorage
fun featureFlagsModule(
context: Context
) = applicationContext {
factory { FeatureStorage(context) }
}

View File

@ -1,6 +1,7 @@
@file:JvmName("MusicServiceModule") @file:JvmName("MusicServiceModule")
package org.moire.ultrasonic.di package org.moire.ultrasonic.di
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log import android.util.Log
import org.koin.dsl.module.applicationContext import org.koin.dsl.module.applicationContext
@ -14,6 +15,7 @@ import org.moire.ultrasonic.service.CachedMusicService
import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.OfflineMusicService import org.moire.ultrasonic.service.OfflineMusicService
import org.moire.ultrasonic.service.RESTMusicService import org.moire.ultrasonic.service.RESTMusicService
import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import kotlin.math.abs import kotlin.math.abs
@ -24,7 +26,10 @@ private const val DEFAULT_SERVER_INSTANCE = 1
private const val UNKNOWN_SERVER_URL = "not-exists" private const val UNKNOWN_SERVER_URL = "not-exists"
private const val LOG_TAG = "MusicServiceModule" private const val LOG_TAG = "MusicServiceModule"
fun musicServiceModule(sp: SharedPreferences) = applicationContext { fun musicServiceModule(
sp: SharedPreferences,
context: Context
) = applicationContext {
context(MUSIC_SERVICE_CONTEXT) { context(MUSIC_SERVICE_CONTEXT) {
subsonicApiModule() subsonicApiModule()
@ -109,5 +114,7 @@ fun musicServiceModule(sp: SharedPreferences) = applicationContext {
bean<MusicService>(name = OFFLINE_MUSIC_SERVICE) { bean<MusicService>(name = OFFLINE_MUSIC_SERVICE) {
return@bean OfflineMusicService(get(), get()) return@bean OfflineMusicService(get(), get())
} }
bean { return@bean SubsonicImageLoader(context, get()) }
} }
} }

View File

@ -0,0 +1,14 @@
package org.moire.ultrasonic.featureflags
/**
* Contains a list of new features/implementations that are not yet finished,
* but possible to try it out.
*/
enum class Feature(
val defaultValue: Boolean
) {
/**
* Enables new image downloader implementation.
*/
NEW_IMAGE_DOWNLOADER(false)
}

View File

@ -0,0 +1,31 @@
package org.moire.ultrasonic.featureflags
import android.content.Context
private const val SP_NAME = "feature_flags"
/**
* Provides storage for current feature flag state.
*/
class FeatureStorage(
context: Context
) {
private val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
/**
* Get [feature] current enabled state.
*/
fun isFeatureEnabled(feature: Feature): Boolean {
return sp.getBoolean(feature.name, feature.defaultValue)
}
/**
* Update [feature] enabled state to [isEnabled].
*/
fun changeFeatureFlag(
feature: Feature,
isEnabled: Boolean
) {
sp.edit().putBoolean(feature.name, isEnabled).apply()
}
}

View File

@ -0,0 +1,65 @@
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
) {
val id = entry?.coverArt
if (id != null &&
view != null &&
view is ImageView) {
val request = ImageRequest.CoverArt(
id,
view,
placeHolderDrawableRes = R.drawable.unknown_album,
errorDrawableRes = R.drawable.unknown_album
)
subsonicImageLoader.load(request)
}
}
override fun loadAvatarImage(
view: View?,
username: String?,
large: Boolean,
size: Int,
crossFade: Boolean,
highQuality: Boolean
) {
if (username != null &&
view != null &&
view is ImageView) {
val request = ImageRequest.Avatar(
username,
view,
placeHolderDrawableRes = R.drawable.ic_contact_picture,
errorDrawableRes = R.drawable.ic_contact_picture
)
subsonicImageLoader.load(request)
}
}
}

View File

@ -436,5 +436,10 @@
<string name="api.subsonic.trial_period_is_over">El período de prueba ha terminado.</string> <string name="api.subsonic.trial_period_is_over">El período de prueba ha terminado.</string>
<string name="api.subsonic.upgrade_client">Versiones incompatibles. Por favor actualiza la aplicación de Android UltraSonic.</string> <string name="api.subsonic.upgrade_client">Versiones incompatibles. Por favor actualiza la aplicación de Android UltraSonic.</string>
<string name="api.subsonic.upgrade_server">Versiones incompatibles. Por favor actualiza el servidor de Subsonic.</string> <string name="api.subsonic.upgrade_server">Versiones incompatibles. Por favor actualiza el servidor de Subsonic.</string>
<string name="feature_flags_category_title">Banderas de características</string>
<string name="feature_flags_image_loader_description">Permite la implementación de un nuevo cargador de imágenes.
Actualmente no guarda la imagen en el almacenamiento del dispositivo y sólo utiliza caché en la memoria.
</string>
<string name="feature_flags_image_loader_title">Habilitar nuevo cargador de imágenes</string>
</resources> </resources>

View File

@ -436,5 +436,11 @@
<string name="api.subsonic.trial_period_is_over">La période d\'essai est terminée.</string> <string name="api.subsonic.trial_period_is_over">La période d\'essai est terminée.</string>
<string name="api.subsonic.upgrade_client">Versions incompatible. Veuillez mette à jour l\'application Android UltraSonic.</string> <string name="api.subsonic.upgrade_client">Versions incompatible. Veuillez mette à jour l\'application Android UltraSonic.</string>
<string name="api.subsonic.upgrade_server">Versions incompatible. Veuillez mette à jour le serveur Subsonic.</string> <string name="api.subsonic.upgrade_server">Versions incompatible. Veuillez mette à jour le serveur Subsonic.</string>
<string name="feature_flags_category_title">Drapeaux des fonctionnalités</string>
<string name="feature_flags_image_loader_description">Permet l\'implémentation d\'un nouveau chargeur d\'images.
Actuellement, il n\'enregistre pas l\'image dans le stockage de l\'appareil et n\'utilise que le cache en
mémoire.
</string>
<string name="feature_flags_image_loader_title">Activer le nouveau chargeur d\'images</string>
</resources> </resources>

View File

@ -436,5 +436,10 @@
<string name="api.subsonic.trial_period_is_over">A próbaidő vége.</string> <string name="api.subsonic.trial_period_is_over">A próbaidő vége.</string>
<string name="api.subsonic.upgrade_client">Nem kompatibilis verzió. Kérjük, frissítse az UltraSonic Android alkalmazást!</string> <string name="api.subsonic.upgrade_client">Nem kompatibilis verzió. Kérjük, frissítse az UltraSonic Android alkalmazást!</string>
<string name="api.subsonic.upgrade_server">Nem kompatibilis verzió. Kérjük, frissítse a Subsonic kiszolgálót!</string> <string name="api.subsonic.upgrade_server">Nem kompatibilis verzió. Kérjük, frissítse a Subsonic kiszolgálót!</string>
<string name="feature_flags_image_loader_description">Engedélyezi az új képbetöltő megvalósítását. Jelenleg nem
tárolja a képet az eszköz tárolójában, és csak a memóriában tárolja a gyorsítótárat.
</string>
<string name="feature_flags_category_title">Jellemzők Zászlók</string>
<string name="feature_flags_image_loader_title">Engedélyezzen új képbetöltőt</string>
</resources> </resources>

View File

@ -436,5 +436,10 @@
<string name="api.subsonic.trial_period_is_over">O período de avaliação acabou.</string> <string name="api.subsonic.trial_period_is_over">O período de avaliação acabou.</string>
<string name="api.subsonic.upgrade_client">Versões incompativeis. Atualize o aplicativo UltraSonic para Android.</string> <string name="api.subsonic.upgrade_client">Versões incompativeis. Atualize o aplicativo UltraSonic para Android.</string>
<string name="api.subsonic.upgrade_server">Versões incompativeis. Atualize o servidor UltraSonic.</string> <string name="api.subsonic.upgrade_server">Versões incompativeis. Atualize o servidor UltraSonic.</string>
<string name="feature_flags_image_loader_description">Permite nova implementação do carregador de imagens.
Atualmente, ele não salva a imagem no armazenamento do dispositivo e usa apenas o cache na memória.
</string>
<string name="feature_flags_category_title">Bandeiras de recursos</string>
<string name="feature_flags_image_loader_title">Ativar novo carregador de imagens</string>
</resources> </resources>

View File

@ -436,5 +436,10 @@
<string name="api.subsonic.trial_period_is_over">O período de avaliação acabou.</string> <string name="api.subsonic.trial_period_is_over">O período de avaliação acabou.</string>
<string name="api.subsonic.upgrade_client">Versões incompativeis. Atualize o aplicativo UltraSonic para Android.</string> <string name="api.subsonic.upgrade_client">Versões incompativeis. Atualize o aplicativo UltraSonic para Android.</string>
<string name="api.subsonic.upgrade_server">Versões incompativeis. Atualize o servidor UltraSonic.</string> <string name="api.subsonic.upgrade_server">Versões incompativeis. Atualize o servidor UltraSonic.</string>
<string name="feature_flags_image_loader_description">Permite nova implementação do carregador de imagens.
Atualmente, ele não salva a imagem no armazenamento do dispositivo e usa apenas o cache na memória.
</string>
<string name="feature_flags_category_title">Bandeiras de recursos</string>
<string name="feature_flags_image_loader_title">Ativar novo carregador de imagens</string>
</resources> </resources>

View File

@ -441,4 +441,10 @@
<string name="api.subsonic.upgrade_client">Incompatible versions. Please upgrade UltraSonic Android app.</string> <string name="api.subsonic.upgrade_client">Incompatible versions. Please upgrade UltraSonic Android app.</string>
<string name="api.subsonic.upgrade_server">Incompatible versions. Please upgrade Subsonic server.</string> <string name="api.subsonic.upgrade_server">Incompatible versions. Please upgrade Subsonic server.</string>
<string name="feature_flags_image_loader_title">Enable new image loader</string>
<string name="feature_flags_image_loader_description">Enables new image loader implementation.
Currently it doesn\'t save image in device storage and uses only cache in memory.
</string>
<string name="feature_flags_category_title">Feature Flags</string>
</resources> </resources>

View File

@ -284,5 +284,14 @@
a:summary="@string/settings.screen_lit_summary" a:summary="@string/settings.screen_lit_summary"
a:title="@string/settings.screen_lit_title"/> a:title="@string/settings.screen_lit_title"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory a:title="@string/feature_flags_category_title">
<CheckBoxPreference
a:key="ff_new_image_loader"
a:persistent="false"
a:title="@string/feature_flags_image_loader_title"
a:summary="@string/feature_flags_image_loader_description"
/>
</PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>