commit
ea9289b5db
|
@ -4,28 +4,32 @@ ext.versions = [
|
||||||
compileSdk : 27,
|
compileSdk : 27,
|
||||||
gradle : '4.5.1',
|
gradle : '4.5.1',
|
||||||
|
|
||||||
androidTools : "3.1.0",
|
androidTools : "3.1.3",
|
||||||
ktlint : "0.20.0",
|
ktlint : "0.24.0",
|
||||||
ktlintGradle : "3.2.0",
|
ktlintGradle : "4.1.0",
|
||||||
detekt : "1.0.0.RC6-4",
|
detekt : "1.0.0.RC6-4",
|
||||||
jacoco : "0.7.9",
|
jacoco : "0.7.9",
|
||||||
jacocoAndroid : "0.1.2",
|
jacocoAndroid : "0.1.2",
|
||||||
|
|
||||||
androidSupport : "22.2.1",
|
androidSupport : "22.2.1",
|
||||||
|
|
||||||
kotlin : "1.2.31",
|
kotlin : "1.2.51",
|
||||||
|
|
||||||
retrofit : "2.4.0",
|
retrofit : "2.4.0",
|
||||||
jackson : "2.9.5",
|
jackson : "2.9.5",
|
||||||
okhttp : "3.10.0",
|
okhttp : "3.10.0",
|
||||||
semver : "1.0.0",
|
semver : "1.0.0",
|
||||||
twitterSerial : "0.1.6",
|
twitterSerial : "0.1.6",
|
||||||
|
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 = [
|
||||||
|
@ -39,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 = [
|
||||||
|
@ -51,6 +56,9 @@ ext.other = [
|
||||||
okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp",
|
okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp",
|
||||||
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",
|
||||||
|
koinAndroid : "org.koin:koin-android:$versions.koin",
|
||||||
|
picasso : "com.squareup.picasso:picasso:$versions.picasso",
|
||||||
]
|
]
|
||||||
|
|
||||||
ext.testing = [
|
ext.testing = [
|
||||||
|
@ -60,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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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 |
|
@ -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>
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
|
@ -13,6 +13,8 @@ dependencies {
|
||||||
api other.kotlinStdlib
|
api other.kotlinStdlib
|
||||||
api other.retrofit
|
api other.retrofit
|
||||||
api other.jacksonConverter
|
api other.jacksonConverter
|
||||||
|
api other.koinCore
|
||||||
|
|
||||||
implementation(other.jacksonKotlin) {
|
implementation(other.jacksonKotlin) {
|
||||||
exclude module: 'kotlin-reflect'
|
exclude module: 'kotlin-reflect'
|
||||||
}
|
}
|
||||||
|
@ -36,7 +38,8 @@ jacoco {
|
||||||
ext {
|
ext {
|
||||||
// Excluding data classes
|
// Excluding data classes
|
||||||
jacocoExclude = [
|
jacocoExclude = [
|
||||||
'**/models/**'
|
'**/models/**',
|
||||||
|
'**/di/**'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,14 @@ class GetStreamUrlTest {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
client = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(),
|
val config = SubsonicClientConfiguration(
|
||||||
USERNAME, PASSWORD, V1_6_0, CLIENT_ID)
|
mockWebServerRule.mockWebServer.url("/").toString(),
|
||||||
|
USERNAME,
|
||||||
|
PASSWORD,
|
||||||
|
V1_6_0,
|
||||||
|
CLIENT_ID
|
||||||
|
)
|
||||||
|
client = SubsonicAPIClient(config)
|
||||||
val baseExpectedUrl = mockWebServerRule.mockWebServer.url("").toString()
|
val baseExpectedUrl = mockWebServerRule.mockWebServer.url("").toString()
|
||||||
expectedUrl = "$baseExpectedUrl/rest/stream.view?id=$id&u=$USERNAME" +
|
expectedUrl = "$baseExpectedUrl/rest/stream.view?id=$id&u=$USERNAME" +
|
||||||
"&c=$CLIENT_ID&f=json&v=${V1_6_0.restApiVersion}&p=enc:${PASSWORD.toHexBytes()}"
|
"&c=$CLIENT_ID&f=json&v=${V1_6_0.restApiVersion}&p=enc:${PASSWORD.toHexBytes()}"
|
||||||
|
|
|
@ -10,11 +10,18 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
||||||
abstract class SubsonicAPIClientTest {
|
abstract class SubsonicAPIClientTest {
|
||||||
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
|
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
|
||||||
|
|
||||||
|
protected lateinit var config: SubsonicClientConfiguration
|
||||||
protected lateinit var client: SubsonicAPIClient
|
protected lateinit var client: SubsonicAPIClient
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
client = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(),
|
config = SubsonicClientConfiguration(
|
||||||
USERNAME, PASSWORD, CLIENT_VERSION, CLIENT_ID)
|
mockWebServerRule.mockWebServer.url("/").toString(),
|
||||||
|
USERNAME,
|
||||||
|
PASSWORD,
|
||||||
|
CLIENT_VERSION,
|
||||||
|
CLIENT_ID
|
||||||
|
)
|
||||||
|
client = SubsonicAPIClient(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,9 @@ import org.junit.Test
|
||||||
class SubsonicApiPasswordTest : SubsonicAPIClientTest() {
|
class SubsonicApiPasswordTest : SubsonicAPIClientTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `Should pass PasswordMD5Interceptor in query params for api version 1 13 0`() {
|
fun `Should pass PasswordMD5Interceptor in query params for api version 1 13 0`() {
|
||||||
val clientV12 = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(),
|
val clientV12 = SubsonicAPIClient(
|
||||||
USERNAME, PASSWORD, SubsonicAPIVersions.V1_14_0, CLIENT_ID)
|
config.copy(minimalProtocolVersion = SubsonicAPIVersions.V1_14_0)
|
||||||
|
)
|
||||||
mockWebServerRule.enqueueResponse("ping_ok.json")
|
mockWebServerRule.enqueueResponse("ping_ok.json")
|
||||||
|
|
||||||
clientV12.api.ping().execute()
|
clientV12.api.ping().execute()
|
||||||
|
@ -25,8 +26,9 @@ class SubsonicApiPasswordTest : SubsonicAPIClientTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should pass PasswordHexInterceptor in query params for api version 1 12 0`() {
|
fun `Should pass PasswordHexInterceptor in query params for api version 1 12 0`() {
|
||||||
val clientV11 = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(),
|
val clientV11 = SubsonicAPIClient(
|
||||||
USERNAME, PASSWORD, SubsonicAPIVersions.V1_12_0, CLIENT_ID)
|
config.copy(minimalProtocolVersion = SubsonicAPIVersions.V1_12_0)
|
||||||
|
)
|
||||||
mockWebServerRule.enqueueResponse("ping_ok.json")
|
mockWebServerRule.enqueueResponse("ping_ok.json")
|
||||||
|
|
||||||
clientV11.api.ping().execute()
|
clientV11.api.ping().execute()
|
||||||
|
|
|
@ -90,7 +90,15 @@ class SubsonicApiSSLTest {
|
||||||
assertResponseSuccessful(response)
|
assertResponseSuccessful(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSubsonicClient(allowSelfSignedCertificate: Boolean) = SubsonicAPIClient(
|
private fun createSubsonicClient(allowSelfSignedCertificate: Boolean): SubsonicAPIClient {
|
||||||
"https://$HOST:$PORT/", USERNAME, PASSWORD, CLIENT_VERSION, CLIENT_ID,
|
val config = SubsonicClientConfiguration(
|
||||||
allowSelfSignedCertificate = allowSelfSignedCertificate)
|
"https://$HOST:$PORT/",
|
||||||
|
USERNAME,
|
||||||
|
PASSWORD,
|
||||||
|
CLIENT_VERSION,
|
||||||
|
CLIENT_ID,
|
||||||
|
allowSelfSignedCertificate = allowSelfSignedCertificate
|
||||||
|
)
|
||||||
|
return SubsonicAPIClient(config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,44 +36,39 @@ private const val READ_TIMEOUT = 60_000L
|
||||||
* @author Yahor Berdnikau
|
* @author Yahor Berdnikau
|
||||||
*/
|
*/
|
||||||
class SubsonicAPIClient(
|
class SubsonicAPIClient(
|
||||||
baseUrl: String,
|
config: SubsonicClientConfiguration,
|
||||||
username: String,
|
baseOkClient: OkHttpClient = OkHttpClient.Builder().build()
|
||||||
password: String,
|
|
||||||
minimalProtocolVersion: SubsonicAPIVersions,
|
|
||||||
clientID: String,
|
|
||||||
allowSelfSignedCertificate: Boolean = false,
|
|
||||||
enableLdapUserSupport: Boolean = false,
|
|
||||||
debug: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
private val versionInterceptor = VersionInterceptor(minimalProtocolVersion) {
|
private val versionInterceptor = VersionInterceptor(config.minimalProtocolVersion) {
|
||||||
protocolVersion = it
|
protocolVersion = it
|
||||||
}
|
}
|
||||||
|
|
||||||
private val proxyPasswordInterceptor = ProxyPasswordInterceptor(
|
private val proxyPasswordInterceptor = ProxyPasswordInterceptor(
|
||||||
minimalProtocolVersion,
|
config.minimalProtocolVersion,
|
||||||
PasswordHexInterceptor(password),
|
PasswordHexInterceptor(config.password),
|
||||||
PasswordMD5Interceptor(password),
|
PasswordMD5Interceptor(config.password),
|
||||||
enableLdapUserSupport)
|
config.enableLdapUserSupport
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get currently used protocol version.
|
* Get currently used protocol version.
|
||||||
*/
|
*/
|
||||||
var protocolVersion = minimalProtocolVersion
|
var protocolVersion = config.minimalProtocolVersion
|
||||||
private set(value) {
|
private set(value) {
|
||||||
field = value
|
field = value
|
||||||
proxyPasswordInterceptor.apiVersion = field
|
proxyPasswordInterceptor.apiVersion = field
|
||||||
wrappedApi.currentApiVersion = field
|
wrappedApi.currentApiVersion = field
|
||||||
}
|
}
|
||||||
|
|
||||||
private val okHttpClient = OkHttpClient.Builder()
|
private val okHttpClient = baseOkClient.newBuilder()
|
||||||
.readTimeout(READ_TIMEOUT, MILLISECONDS)
|
.readTimeout(READ_TIMEOUT, MILLISECONDS)
|
||||||
.apply { if (allowSelfSignedCertificate) allowSelfSignedCertificates() }
|
.apply { if (config.allowSelfSignedCertificate) allowSelfSignedCertificates() }
|
||||||
.addInterceptor { chain ->
|
.addInterceptor { chain ->
|
||||||
// Adds default request params
|
// Adds default request params
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
val newUrl = originalRequest.url().newBuilder()
|
val newUrl = originalRequest.url().newBuilder()
|
||||||
.addQueryParameter("u", username)
|
.addQueryParameter("u", config.username)
|
||||||
.addQueryParameter("c", clientID)
|
.addQueryParameter("c", config.clientID)
|
||||||
.addQueryParameter("f", "json")
|
.addQueryParameter("f", "json")
|
||||||
.build()
|
.build()
|
||||||
chain.proceed(originalRequest.newBuilder().url(newUrl).build())
|
chain.proceed(originalRequest.newBuilder().url(newUrl).build())
|
||||||
|
@ -81,7 +76,7 @@ class SubsonicAPIClient(
|
||||||
.addInterceptor(versionInterceptor)
|
.addInterceptor(versionInterceptor)
|
||||||
.addInterceptor(proxyPasswordInterceptor)
|
.addInterceptor(proxyPasswordInterceptor)
|
||||||
.addInterceptor(RangeHeaderInterceptor())
|
.addInterceptor(RangeHeaderInterceptor())
|
||||||
.apply { if (debug) addLogging() }
|
.apply { if (config.debug) addLogging() }
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val jacksonMapper = ObjectMapper()
|
private val jacksonMapper = ObjectMapper()
|
||||||
|
@ -90,14 +85,15 @@ class SubsonicAPIClient(
|
||||||
.registerModule(KotlinModule())
|
.registerModule(KotlinModule())
|
||||||
|
|
||||||
private val retrofit = Retrofit.Builder()
|
private val retrofit = Retrofit.Builder()
|
||||||
.baseUrl("$baseUrl/rest/")
|
.baseUrl("${config.baseUrl}/rest/")
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
.addConverterFactory(JacksonConverterFactory.create(jacksonMapper))
|
.addConverterFactory(JacksonConverterFactory.create(jacksonMapper))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val wrappedApi = ApiVersionCheckWrapper(
|
private val wrappedApi = ApiVersionCheckWrapper(
|
||||||
retrofit.create(SubsonicAPIDefinition::class.java),
|
retrofit.create(SubsonicAPIDefinition::class.java),
|
||||||
minimalProtocolVersion)
|
config.minimalProtocolVersion
|
||||||
|
)
|
||||||
|
|
||||||
val api: SubsonicAPIDefinition get() = wrappedApi
|
val api: SubsonicAPIDefinition get() = wrappedApi
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package org.moire.ultrasonic.api.subsonic
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides configuration for [SubsonicAPIClient].
|
||||||
|
*/
|
||||||
|
data class SubsonicClientConfiguration(
|
||||||
|
val baseUrl: String,
|
||||||
|
val username: String,
|
||||||
|
val password: String,
|
||||||
|
val minimalProtocolVersion: SubsonicAPIVersions,
|
||||||
|
val clientID: String,
|
||||||
|
val allowSelfSignedCertificate: Boolean = false,
|
||||||
|
val enableLdapUserSupport: Boolean = false,
|
||||||
|
val debug: Boolean = false
|
||||||
|
)
|
|
@ -0,0 +1,10 @@
|
||||||
|
package org.moire.ultrasonic.api.subsonic.di
|
||||||
|
|
||||||
|
import org.koin.dsl.context.Context
|
||||||
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||||
|
|
||||||
|
const val SUBSONIC_API_CLIENT_CONTEXT = "SubsonicApiClientContext"
|
||||||
|
|
||||||
|
fun Context.subsonicApiModule() = context(SUBSONIC_API_CLIENT_CONTEXT) {
|
||||||
|
bean { return@bean SubsonicAPIClient(get(), get()) }
|
||||||
|
}
|
|
@ -8,8 +8,8 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.moire.ultrasonic"
|
applicationId "org.moire.ultrasonic"
|
||||||
versionCode 68
|
versionCode 69
|
||||||
versionName "2.5.0"
|
versionName "2.6.0"
|
||||||
|
|
||||||
minSdkVersion versions.minSdk
|
minSdkVersion versions.minSdk
|
||||||
targetSdkVersion versions.targetSdk
|
targetSdkVersion versions.targetSdk
|
||||||
|
@ -55,12 +55,14 @@ 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
|
||||||
implementation androidSupport.design
|
implementation androidSupport.design
|
||||||
|
|
||||||
implementation other.kotlinStdlib
|
implementation other.kotlinStdlib
|
||||||
|
implementation other.koinAndroid
|
||||||
|
|
||||||
testImplementation other.kotlinReflect
|
testImplementation other.kotlinReflect
|
||||||
testImplementation testing.junit
|
testImplementation testing.junit
|
||||||
|
@ -69,7 +71,7 @@ dependencies {
|
||||||
testImplementation testing.kluent
|
testImplementation testing.kluent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Excluding all non-kotlin classes
|
// Excluding all java classes and stuff that should not be covered
|
||||||
ext {
|
ext {
|
||||||
jacocoExclude = [
|
jacocoExclude = [
|
||||||
'**/activity/**',
|
'**/activity/**',
|
||||||
|
@ -83,7 +85,8 @@ ext {
|
||||||
'**/view/**',
|
'**/view/**',
|
||||||
'**/R$*.class',
|
'**/R$*.class',
|
||||||
'**/R.class',
|
'**/R.class',
|
||||||
'**/BuildConfig.class'
|
'**/BuildConfig.class',
|
||||||
|
'**/di/**'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
jacocoAndroidUnitTestReport {
|
jacocoAndroidUnitTestReport {
|
||||||
|
|
|
@ -1,204 +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:label="@string/common.appname">
|
android:name=".app.UApp"
|
||||||
|
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>
|
||||||
|
|
|
@ -887,8 +887,11 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent intent = new Intent(this, SelectAlbumActivity.class);
|
Intent intent = new Intent(this, SelectAlbumActivity.class);
|
||||||
intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, entry.getParent());
|
String albumId = Util.getShouldUseId3Tags(this) ? entry.getAlbumId() : entry.getParent();
|
||||||
|
intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, albumId);
|
||||||
intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, entry.getAlbum());
|
intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, entry.getAlbum());
|
||||||
|
intent.putExtra(Constants.INTENT_EXTRA_NAME_IS_ALBUM, true);
|
||||||
|
intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.getParent());
|
||||||
startActivityForResultWithoutTransition(this, intent);
|
startActivityForResultWithoutTransition(this, intent);
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_lyrics:
|
case R.id.menu_lyrics:
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -14,6 +14,7 @@ import android.view.View;
|
||||||
|
|
||||||
import org.moire.ultrasonic.BuildConfig;
|
import org.moire.ultrasonic.BuildConfig;
|
||||||
import org.moire.ultrasonic.R;
|
import org.moire.ultrasonic.R;
|
||||||
|
import org.moire.ultrasonic.cache.Directories;
|
||||||
import org.moire.ultrasonic.cache.PermanentFileStorage;
|
import org.moire.ultrasonic.cache.PermanentFileStorage;
|
||||||
import org.moire.ultrasonic.service.MusicService;
|
import org.moire.ultrasonic.service.MusicService;
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory;
|
import org.moire.ultrasonic.service.MusicServiceFactory;
|
||||||
|
@ -282,9 +283,10 @@ public class ServerSettingsFragment extends PreferenceFragment
|
||||||
.getInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, 0);
|
.getInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, 0);
|
||||||
|
|
||||||
// Clear permanent storage
|
// Clear permanent storage
|
||||||
final String storageServerId = MusicServiceFactory.getServerId(sharedPreferences, serverId);
|
final String storageServerId = MusicServiceFactory.getServerId();
|
||||||
|
final Directories directories = MusicServiceFactory.getDirectories();
|
||||||
final PermanentFileStorage fileStorage = new PermanentFileStorage(
|
final PermanentFileStorage fileStorage = new PermanentFileStorage(
|
||||||
MusicServiceFactory.getDirectories(getActivity()),
|
directories,
|
||||||
storageServerId,
|
storageServerId,
|
||||||
BuildConfig.DEBUG
|
BuildConfig.DEBUG
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -1,150 +0,0 @@
|
||||||
/*
|
|
||||||
This file is part of Subsonic.
|
|
||||||
|
|
||||||
Subsonic is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Subsonic is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Copyright 2009 (C) Sindre Mehus
|
|
||||||
*/
|
|
||||||
package org.moire.ultrasonic.service;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.moire.ultrasonic.BuildConfig;
|
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient;
|
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions;
|
|
||||||
import org.moire.ultrasonic.cache.Directories;
|
|
||||||
import org.moire.ultrasonic.cache.PermanentFileStorage;
|
|
||||||
import org.moire.ultrasonic.util.Constants;
|
|
||||||
import org.moire.ultrasonic.util.Util;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Sindre Mehus
|
|
||||||
* @version $Id$
|
|
||||||
*/
|
|
||||||
public class MusicServiceFactory {
|
|
||||||
private static final String LOG_TAG = MusicServiceFactory.class.getSimpleName();
|
|
||||||
private static MusicService REST_MUSIC_SERVICE = null;
|
|
||||||
private static MusicService OFFLINE_MUSIC_SERVICE = null;
|
|
||||||
|
|
||||||
public static MusicService getMusicService(Context context) {
|
|
||||||
if (Util.isOffline(context)) {
|
|
||||||
Log.d(LOG_TAG, "App is offline, returning offline music service.");
|
|
||||||
if (OFFLINE_MUSIC_SERVICE == null) {
|
|
||||||
synchronized (MusicServiceFactory.class) {
|
|
||||||
if (OFFLINE_MUSIC_SERVICE == null) {
|
|
||||||
Log.d(LOG_TAG, "Creating new offline music service");
|
|
||||||
OFFLINE_MUSIC_SERVICE = new OfflineMusicService(
|
|
||||||
createSubsonicApiClient(context),
|
|
||||||
getPermanentFileStorage(context));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return OFFLINE_MUSIC_SERVICE;
|
|
||||||
} else {
|
|
||||||
Log.d(LOG_TAG, "Returning rest music service");
|
|
||||||
if (REST_MUSIC_SERVICE == null) {
|
|
||||||
synchronized (MusicServiceFactory.class) {
|
|
||||||
if (REST_MUSIC_SERVICE == null) {
|
|
||||||
Log.d(LOG_TAG, "Creating new rest music service");
|
|
||||||
REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService(
|
|
||||||
createSubsonicApiClient(context),
|
|
||||||
getPermanentFileStorage(context)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return REST_MUSIC_SERVICE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets {@link MusicService} to initial state, so on next call to {@link #getMusicService(Context)}
|
|
||||||
* it will return updated instance of it.
|
|
||||||
*/
|
|
||||||
public static void resetMusicService() {
|
|
||||||
Log.d(LOG_TAG, "Resetting music service");
|
|
||||||
synchronized (MusicServiceFactory.class) {
|
|
||||||
REST_MUSIC_SERVICE = null;
|
|
||||||
OFFLINE_MUSIC_SERVICE = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SubsonicAPIClient createSubsonicApiClient(final Context context) {
|
|
||||||
final SharedPreferences preferences = Util.getPreferences(context);
|
|
||||||
int instance = preferences.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
|
|
||||||
String serverUrl = preferences.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
|
|
||||||
String username = preferences.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
|
|
||||||
String password = preferences.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
|
|
||||||
boolean allowSelfSignedCertificate = preferences
|
|
||||||
.getBoolean(Constants.PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + instance, false);
|
|
||||||
boolean enableLdapUserSupport = preferences
|
|
||||||
.getBoolean(Constants.PREFERENCES_KEY_LDAP_SUPPORT + instance , false);
|
|
||||||
|
|
||||||
if (serverUrl == null ||
|
|
||||||
username == null ||
|
|
||||||
password == null) {
|
|
||||||
Log.i("MusicServiceFactory", "Server credentials is not available");
|
|
||||||
return new SubsonicAPIClient("http://localhost", "", "",
|
|
||||||
SubsonicAPIVersions.fromApiVersion(Constants.REST_PROTOCOL_VERSION),
|
|
||||||
Constants.REST_CLIENT_ID, allowSelfSignedCertificate,
|
|
||||||
enableLdapUserSupport, BuildConfig.DEBUG);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SubsonicAPIClient(serverUrl, username, password,
|
|
||||||
SubsonicAPIVersions.fromApiVersion(Constants.REST_PROTOCOL_VERSION),
|
|
||||||
Constants.REST_CLIENT_ID, allowSelfSignedCertificate,
|
|
||||||
enableLdapUserSupport, BuildConfig.DEBUG);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PermanentFileStorage getPermanentFileStorage(final Context context) {
|
|
||||||
final SharedPreferences preferences = Util.getPreferences(context);
|
|
||||||
int instance = preferences.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
|
|
||||||
final String serverId = getServerId(preferences, instance);
|
|
||||||
|
|
||||||
return new PermanentFileStorage(getDirectories(context), serverId, BuildConfig.DEBUG);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getServerId(final SharedPreferences sp, final int instance) {
|
|
||||||
String serverUrl = sp.getString(
|
|
||||||
Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
|
|
||||||
return String.valueOf(Math.abs((serverUrl + instance).hashCode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Directories getDirectories(final Context context) {
|
|
||||||
return new Directories() {
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public File getInternalCacheDir() {
|
|
||||||
return context.getCacheDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public File getInternalDataDir() {
|
|
||||||
return context.getFilesDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public File getExternalCacheDir() {
|
|
||||||
return context.getExternalCacheDir();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -21,20 +21,17 @@ package org.moire.ultrasonic.util;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonParseException;
|
import com.fasterxml.jackson.core.JsonParseException;
|
||||||
|
|
||||||
import org.moire.ultrasonic.R;
|
import org.moire.ultrasonic.R;
|
||||||
import org.moire.ultrasonic.service.SubsonicRESTException;
|
import org.moire.ultrasonic.service.SubsonicRESTException;
|
||||||
import org.moire.ultrasonic.subsonic.RestErrorMapper;
|
import org.moire.ultrasonic.subsonic.RestErrorMapper;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.cert.CertPathValidatorException;
|
import java.security.cert.CertPathValidatorException;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
|
|
||||||
import javax.net.ssl.SSLException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Sindre Mehus
|
* @author Sindre Mehus
|
||||||
*/
|
*/
|
||||||
|
@ -70,7 +67,7 @@ public abstract class BackgroundTask<T> implements ProgressListener
|
||||||
protected void error(Throwable error)
|
protected void error(Throwable error)
|
||||||
{
|
{
|
||||||
Log.w(TAG, String.format("Got exception: %s", error), error);
|
Log.w(TAG, String.format("Got exception: %s", error), error);
|
||||||
new ErrorDialog(activity, getErrorMessage(error), true);
|
new ErrorDialog(activity, getErrorMessage(error), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getErrorMessage(Throwable error) {
|
protected String getErrorMessage(Throwable error) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -171,14 +171,16 @@ public class Util extends DownloadActivity
|
||||||
return preferences.getBoolean(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS, false);
|
return preferences.getBoolean(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setActiveServer(Context context, int instance)
|
public static void setActiveServer(
|
||||||
{
|
Context context,
|
||||||
|
int instance
|
||||||
|
) {
|
||||||
MusicServiceFactory.resetMusicService();
|
MusicServiceFactory.resetMusicService();
|
||||||
SharedPreferences preferences = getPreferences(context);
|
SharedPreferences preferences = getPreferences(context);
|
||||||
SharedPreferences.Editor editor = preferences.edit();
|
SharedPreferences.Editor editor = preferences.edit();
|
||||||
editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance);
|
editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance);
|
||||||
editor.commit();
|
editor.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getActiveServer(Context context)
|
public static int getActiveServer(Context context)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.moire.ultrasonic.app
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
import org.koin.android.ext.android.startKoin
|
||||||
|
import org.moire.ultrasonic.di.baseNetworkModule
|
||||||
|
import org.moire.ultrasonic.di.directoriesModule
|
||||||
|
import org.moire.ultrasonic.di.featureFlagsModule
|
||||||
|
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
|
||||||
|
|
||||||
|
class UApp : Application() {
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
val sharedPreferences = Util.getPreferences(this)
|
||||||
|
startKoin(this, listOf(
|
||||||
|
directoriesModule,
|
||||||
|
baseNetworkModule,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.moire.ultrasonic.cache
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides specific to Android implementation of [Directories].
|
||||||
|
*/
|
||||||
|
class AndroidDirectories(
|
||||||
|
private val context: Context
|
||||||
|
) : Directories {
|
||||||
|
override fun getInternalCacheDir(): File = context.cacheDir
|
||||||
|
|
||||||
|
override fun getInternalDataDir(): File = context.filesDir
|
||||||
|
|
||||||
|
override fun getExternalCacheDir(): File? = context.externalCacheDir
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.moire.ultrasonic.di
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import org.koin.dsl.module.applicationContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides base network dependencies.
|
||||||
|
*/
|
||||||
|
val baseNetworkModule = applicationContext {
|
||||||
|
bean { OkHttpClient.Builder().build() }
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.moire.ultrasonic.di
|
||||||
|
|
||||||
|
import org.koin.dsl.module.applicationContext
|
||||||
|
import org.moire.ultrasonic.cache.AndroidDirectories
|
||||||
|
import org.moire.ultrasonic.cache.Directories
|
||||||
|
|
||||||
|
val directoriesModule = applicationContext {
|
||||||
|
bean { AndroidDirectories(get()) } bind Directories::class
|
||||||
|
}
|
|
@ -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) }
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
@file:JvmName("MusicServiceModule")
|
||||||
|
package org.moire.ultrasonic.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
|
import org.koin.dsl.module.applicationContext
|
||||||
|
import org.moire.ultrasonic.BuildConfig
|
||||||
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||||
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
||||||
|
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
|
||||||
|
import org.moire.ultrasonic.api.subsonic.di.subsonicApiModule
|
||||||
|
import org.moire.ultrasonic.cache.PermanentFileStorage
|
||||||
|
import org.moire.ultrasonic.service.CachedMusicService
|
||||||
|
import org.moire.ultrasonic.service.MusicService
|
||||||
|
import org.moire.ultrasonic.service.OfflineMusicService
|
||||||
|
import org.moire.ultrasonic.service.RESTMusicService
|
||||||
|
import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader
|
||||||
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
internal const val MUSIC_SERVICE_CONTEXT = "CurrentMusicService"
|
||||||
|
internal const val ONLINE_MUSIC_SERVICE = "OnlineMusicService"
|
||||||
|
internal const val OFFLINE_MUSIC_SERVICE = "OfflineMusicService"
|
||||||
|
private const val DEFAULT_SERVER_INSTANCE = 1
|
||||||
|
private const val UNKNOWN_SERVER_URL = "not-exists"
|
||||||
|
private const val LOG_TAG = "MusicServiceModule"
|
||||||
|
|
||||||
|
fun musicServiceModule(
|
||||||
|
sp: SharedPreferences,
|
||||||
|
context: Context
|
||||||
|
) = applicationContext {
|
||||||
|
context(MUSIC_SERVICE_CONTEXT) {
|
||||||
|
subsonicApiModule()
|
||||||
|
|
||||||
|
bean(name = "ServerInstance") {
|
||||||
|
return@bean sp.getInt(
|
||||||
|
Constants.PREFERENCES_KEY_SERVER_INSTANCE,
|
||||||
|
DEFAULT_SERVER_INSTANCE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
bean(name = "ServerID") {
|
||||||
|
val serverInstance = get<Int>(name = "ServerInstance")
|
||||||
|
val serverUrl = sp.getString(
|
||||||
|
Constants.PREFERENCES_KEY_SERVER_URL + serverInstance,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
return@bean if (serverUrl == null) {
|
||||||
|
UNKNOWN_SERVER_URL
|
||||||
|
} else {
|
||||||
|
abs("$serverUrl$serverInstance".hashCode()).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bean {
|
||||||
|
val serverId = get<String>(name = "ServerID")
|
||||||
|
return@bean PermanentFileStorage(get(), serverId, BuildConfig.DEBUG)
|
||||||
|
}
|
||||||
|
|
||||||
|
bean {
|
||||||
|
val instance = get<Int>(name = "ServerInstance")
|
||||||
|
val serverUrl = sp.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null)
|
||||||
|
val username = sp.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null)
|
||||||
|
val password = sp.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null)
|
||||||
|
val allowSelfSignedCertificate = sp.getBoolean(
|
||||||
|
Constants.PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + instance,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
val enableLdapUserSupport = sp.getBoolean(
|
||||||
|
Constants.PREFERENCES_KEY_LDAP_SUPPORT + instance,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
if (serverUrl == null ||
|
||||||
|
username == null ||
|
||||||
|
password == null
|
||||||
|
) {
|
||||||
|
Log.i(LOG_TAG, "Server credentials is not available")
|
||||||
|
return@bean SubsonicClientConfiguration(
|
||||||
|
baseUrl = "http://localhost",
|
||||||
|
username = "",
|
||||||
|
password = "",
|
||||||
|
minimalProtocolVersion = SubsonicAPIVersions.fromApiVersion(
|
||||||
|
Constants.REST_PROTOCOL_VERSION
|
||||||
|
),
|
||||||
|
clientID = Constants.REST_CLIENT_ID,
|
||||||
|
allowSelfSignedCertificate = allowSelfSignedCertificate,
|
||||||
|
enableLdapUserSupport = enableLdapUserSupport,
|
||||||
|
debug = BuildConfig.DEBUG
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return@bean SubsonicClientConfiguration(
|
||||||
|
baseUrl = serverUrl,
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
minimalProtocolVersion = SubsonicAPIVersions.fromApiVersion(
|
||||||
|
Constants.REST_PROTOCOL_VERSION
|
||||||
|
),
|
||||||
|
clientID = Constants.REST_CLIENT_ID,
|
||||||
|
allowSelfSignedCertificate = allowSelfSignedCertificate,
|
||||||
|
enableLdapUserSupport = enableLdapUserSupport,
|
||||||
|
debug = BuildConfig.DEBUG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bean { return@bean SubsonicAPIClient(get()) }
|
||||||
|
|
||||||
|
bean<MusicService>(name = ONLINE_MUSIC_SERVICE) {
|
||||||
|
return@bean CachedMusicService(RESTMusicService(get(), get()))
|
||||||
|
}
|
||||||
|
|
||||||
|
bean<MusicService>(name = OFFLINE_MUSIC_SERVICE) {
|
||||||
|
return@bean OfflineMusicService(get(), get())
|
||||||
|
}
|
||||||
|
|
||||||
|
bean { return@bean SubsonicImageLoader(context, get()) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
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.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.koin.standalone.KoinComponent
|
||||||
|
import org.koin.standalone.get
|
||||||
|
import org.koin.standalone.releaseContext
|
||||||
|
import org.moire.ultrasonic.cache.Directories
|
||||||
|
import org.moire.ultrasonic.di.MUSIC_SERVICE_CONTEXT
|
||||||
|
import org.moire.ultrasonic.di.OFFLINE_MUSIC_SERVICE
|
||||||
|
import org.moire.ultrasonic.di.ONLINE_MUSIC_SERVICE
|
||||||
|
import org.moire.ultrasonic.util.Util
|
||||||
|
|
||||||
|
@Deprecated("Use DI way to get MusicService")
|
||||||
|
object MusicServiceFactory : KoinComponent {
|
||||||
|
@JvmStatic
|
||||||
|
fun getMusicService(context: Context): MusicService {
|
||||||
|
return if (Util.isOffline(context)) {
|
||||||
|
get(OFFLINE_MUSIC_SERVICE)
|
||||||
|
} else {
|
||||||
|
get(ONLINE_MUSIC_SERVICE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets [MusicService] to initial state, so on next call to [.getMusicService]
|
||||||
|
* it will return updated instance of it.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun resetMusicService() {
|
||||||
|
releaseContext(MUSIC_SERVICE_CONTEXT)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getServerId() = get<String>(name = "ServerID")
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getDirectories() = get<Directories>()
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,8 +20,8 @@
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
a:id="@+id/download_album_art_image"
|
a:id="@+id/download_album_art_image"
|
||||||
a:layout_width="wrap_content"
|
a:layout_width="match_parent"
|
||||||
a:layout_height="fill_parent"
|
a:layout_height="match_parent"
|
||||||
a:scaleType="centerCrop"
|
a:scaleType="centerCrop"
|
||||||
a:layout_alignParentTop="true"
|
a:layout_alignParentTop="true"
|
||||||
a:layout_centerInParent="true"
|
a:layout_centerInParent="true"
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
a:id="@+id/album_coverart"
|
a:id="@+id/album_coverart"
|
||||||
a:layout_width="wrap_content"
|
a:layout_width="64dp"
|
||||||
a:layout_height="wrap_content"
|
a:layout_height="64dp"
|
||||||
a:layout_gravity="left|center_vertical"
|
a:layout_gravity="left|center_vertical"
|
||||||
a:paddingLeft="3dip"/>
|
a:paddingLeft="3dip"/>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue