From 24d68fcf3c994df2f82a32fc01370af6d166f1f0 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sat, 22 Jul 2017 22:19:03 +0200 Subject: [PATCH 01/14] Revert back to compile api 22. Some used classes are missing in the newest sdk like FloatMath and apache http client. Signed-off-by: Yahor Berdnikau --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index a6920af1..e0d8d264 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,7 +1,7 @@ ext.versions = [ minSdk : 14, targetSdk : 22, - compileSdk : 26, + compileSdk : 22, buildTools : "25.0.3", androidTools : "2.3.3", From 20d95ce19d2f8f5e29d40cb50619e0fe17956b1f Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sat, 22 Jul 2017 22:49:14 +0200 Subject: [PATCH 02/14] Add optional logging to SubsonicAPI. Signed-off-by: Yahor Berdnikau --- dependencies.gradle | 3 ++- subsonic-api/build.gradle | 1 + .../moire/ultrasonic/api/subsonic/SubsonicAPI.kt | 13 +++++++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index e0d8d264..de849abe 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -30,11 +30,12 @@ ext.androidSupport = [ ] ext.other = [ - kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib-common:$versions.kotlin", + kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib-common:$versions.kotlin", retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit", gsonConverter : "com.squareup.retrofit2:converter-gson:$versions.retrofit", jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit", jacksonKotlin : "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson", + okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp", ] ext.testing = [ diff --git a/subsonic-api/build.gradle b/subsonic-api/build.gradle index eea7105f..1bdb172b 100644 --- a/subsonic-api/build.gradle +++ b/subsonic-api/build.gradle @@ -12,6 +12,7 @@ dependencies { compile other.retrofit compile other.jacksonConverter compile other.jacksonKotlin + compile other.okhttpLogging testCompile testing.junit testCompile testing.kotlinJunit diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPI.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPI.kt index 9f22e81c..94a0fece 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPI.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPI.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.jackson.JacksonConverterFactory import java.math.BigInteger @@ -17,7 +18,8 @@ class SubsonicAPI(baseUrl: String, username: String, private val password: String, clientProtocolVersion: SubsonicAPIVersions, - clientID: String) { + clientID: String, + debug: Boolean = false) { private val okHttpClient = OkHttpClient.Builder() .addInterceptor { chain -> // Adds default request params @@ -30,6 +32,13 @@ class SubsonicAPI(baseUrl: String, .addQueryParameter("f", "json") .build() chain.proceed(originalRequest.newBuilder().url(newUrl).build()) + } + .apply { + if (debug) { + val loggingInterceptor = HttpLoggingInterceptor() + loggingInterceptor.level = HttpLoggingInterceptor.Level.BASIC + this.addInterceptor(loggingInterceptor) + } }.build() private val jacksonMapper = ObjectMapper() @@ -49,7 +58,7 @@ class SubsonicAPI(baseUrl: String, * * @return initialized API instance */ - fun getApi() = subsonicAPI + fun getApi(): SubsonicAPIDefinition = subsonicAPI private fun passwordHex() = "enc:${password.toHexBytes()}" From f1ab0a3e0c96c118bd78b702b3598d18fac385e6 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sun, 23 Jul 2017 19:46:06 +0200 Subject: [PATCH 03/14] Add using new authentication method since 1.13.0. Signed-off-by: Yahor Berdnikau --- dependencies.gradle | 13 +- subsonic-api/build.gradle | 2 + ...nicAPITest.kt => SubsonicAPIClientTest.kt} | 84 ++++++++++--- .../ultrasonic/api/subsonic/SubsonicAPI.kt | 68 ----------- .../api/subsonic/SubsonicAPIClient.kt | 113 ++++++++++++++++++ ultrasonic/build.gradle | 2 + 6 files changed, 190 insertions(+), 92 deletions(-) rename subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/{SubsonicAPITest.kt => SubsonicAPIClientTest.kt} (67%) delete mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPI.kt create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt diff --git a/dependencies.gradle b/dependencies.gradle index de849abe..a177610f 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -12,11 +12,12 @@ ext.versions = [ retrofit : "2.1.0", jackson : "2.8.7", + okhttp : "3.6.0", junit : "4.12", - mockitoKotlin : "1.3.0", - kluent : "1.15", - okhttp : "3.6.0", + mockitoKotlin : "1.5.0", + kluent : "1.26", + apacheCodecs : "1.10", ] ext.gradlePlugins = [ @@ -30,7 +31,7 @@ ext.androidSupport = [ ] ext.other = [ - kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib-common:$versions.kotlin", + kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin", retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit", gsonConverter : "com.squareup.retrofit2:converter-gson:$versions.retrofit", jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit", @@ -41,7 +42,9 @@ ext.other = [ ext.testing = [ junit : "junit:junit:$versions.junit", kotlinJunit : "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin", + kotlinReflect : "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin", mockitoKotlin : "com.nhaarman:mockito-kotlin:$versions.mockitoKotlin", kluent : "org.amshove.kluent:kluent:$versions.kluent", - mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp" + mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp", + apacheCodecs : "commons-codec:commons-codec:$versions.apacheCodecs", ] diff --git a/subsonic-api/build.gradle b/subsonic-api/build.gradle index 1bdb172b..1091d2f5 100644 --- a/subsonic-api/build.gradle +++ b/subsonic-api/build.gradle @@ -16,7 +16,9 @@ dependencies { testCompile testing.junit testCompile testing.kotlinJunit + testCompile testing.kotlinReflect testCompile testing.mockitoKotlin testCompile testing.kluent testCompile testing.mockWebServer + testCompile testing.apacheCodecs } \ No newline at end of file diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPITest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClientTest.kt similarity index 67% rename from subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPITest.kt rename to subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClientTest.kt index 49bc6ab4..a8e314c9 100644 --- a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPITest.kt +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClientTest.kt @@ -6,21 +6,28 @@ import org.amshove.kluent.`should be` import org.amshove.kluent.`should contain` import org.amshove.kluent.`should equal` import org.amshove.kluent.`should not be` +import org.amshove.kluent.`should not contain` +import org.apache.commons.codec.binary.Hex import org.junit.Before import org.junit.Rule import org.junit.Test -import org.moire.ultrasonic.api.subsonic.models.* +import org.moire.ultrasonic.api.subsonic.models.Artist +import org.moire.ultrasonic.api.subsonic.models.Index +import org.moire.ultrasonic.api.subsonic.models.License +import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild +import org.moire.ultrasonic.api.subsonic.models.MusicFolder import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule import retrofit2.Response import java.nio.charset.Charset +import java.security.MessageDigest import java.text.SimpleDateFormat import java.util.* /** - * Integration test for [SubsonicAPI] class. + * Integration test for [SubsonicAPIClient] class. */ -class SubsonicAPITest { +class SubsonicAPIClientTest { companion object { const val USERNAME = "some-user" const val PASSWORD = "some-password" @@ -30,19 +37,58 @@ class SubsonicAPITest { @JvmField @Rule val mockWebServerRule = MockWebServerRule() - private lateinit var api: SubsonicAPI + private lateinit var client: SubsonicAPIClient @Before fun setUp() { - api = SubsonicAPI(mockWebServerRule.mockWebServer.url("/").toString(), USERNAME, PASSWORD, + client = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(), USERNAME, PASSWORD, CLIENT_VERSION, CLIENT_ID) } + @Test + fun `Should pass password hash and salt in query params for api version 1_13_0`() { + val clientV12 = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(), USERNAME, + PASSWORD, SubsonicAPIVersions.V1_14_0, CLIENT_ID) + enqueueResponse("ping_ok.json") + + clientV12.api.ping().execute() + + with(mockWebServerRule.mockWebServer.takeRequest()) { + requestLine `should contain` "&s=" + requestLine `should contain` "&t=" + requestLine `should not contain` "&p=enc:" + + val salt = requestLine.split('&').find { it.startsWith("s=") }?.substringAfter('=') + val token = requestLine.split('&').find { it.startsWith("t=") }?.substringAfter('=') + val expectedToken = String(Hex.encodeHex(MessageDigest.getInstance("MD5") + .digest("$PASSWORD$salt".toByteArray()), false)) + token!! `should equal` expectedToken + } + } + + @Test + fun `Should pass hex encoded password in query params for api version 1_12_0`() { + val clientV11 = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(), USERNAME, + PASSWORD, SubsonicAPIVersions.V1_12_0, CLIENT_ID) + enqueueResponse("ping_ok.json") + + clientV11.api.ping().execute() + + with(mockWebServerRule.mockWebServer.takeRequest()) { + requestLine `should not contain` "&s=" + requestLine `should not contain` "&t=" + requestLine `should contain` "&p=enc:" + val passParam = requestLine.split('&').find { it.startsWith("p=enc:") } + val encodedPassword = String(Hex.encodeHex(PASSWORD.toByteArray(), false)) + passParam `should equal` "p=enc:$encodedPassword" + } + } + @Test fun `Should parse ping ok response`() { enqueueResponse("ping_ok.json") - val response = api.getApi().ping().execute() + val response = client.api.ping().execute() assertResponseSuccessful(response) with(response.body()) { @@ -52,14 +98,14 @@ class SubsonicAPITest { @Test fun `Should parse ping error response`() { - checkErrorCallParsed { api.getApi().ping().execute() } + checkErrorCallParsed { client.api.ping().execute() } } @Test fun `Should parse get license ok response`() { enqueueResponse("license_ok.json") - val response = api.getApi().getLicense().execute() + val response = client.api.getLicense().execute() assertResponseSuccessful(response) with(response.body()) { @@ -70,7 +116,7 @@ class SubsonicAPITest { @Test fun `Should parse get license error response`() { - val response = checkErrorCallParsed { api.getApi().getLicense().execute() } + val response = checkErrorCallParsed { client.api.getLicense().execute() } response.license `should be` null } @@ -79,7 +125,7 @@ class SubsonicAPITest { fun `Should parse get music folders ok response`() { enqueueResponse("get_music_folders_ok.json") - val response = api.getApi().getMusicFolders().execute() + val response = client.api.getMusicFolders().execute() assertResponseSuccessful(response) with(response.body()) { @@ -90,7 +136,7 @@ class SubsonicAPITest { @Test fun `Should parse get music folders error response`() { - val response = checkErrorCallParsed { api.getApi().getMusicFolders().execute() } + val response = checkErrorCallParsed { client.api.getMusicFolders().execute() } response.musicFolders `should be` null } @@ -100,7 +146,7 @@ class SubsonicAPITest { // TODO: check for shortcut parsing enqueueResponse("get_indexes_ok.json") - val response = api.getApi().getIndexes(null, null).execute() + val response = client.api.getIndexes(null, null).execute() assertResponseSuccessful(response) response.body().indexes `should not be` null @@ -126,7 +172,7 @@ class SubsonicAPITest { enqueueResponse("get_indexes_ok.json") val musicFolderId = 9L - api.getApi().getIndexes(musicFolderId, null).execute() + client.api.getIndexes(musicFolderId, null).execute() with(mockWebServerRule.mockWebServer.takeRequest()) { requestLine `should contain` "musicFolderId=$musicFolderId" @@ -138,7 +184,7 @@ class SubsonicAPITest { enqueueResponse("get_indexes_ok.json") val ifModifiedSince = System.currentTimeMillis() - api.getApi().getIndexes(null, ifModifiedSince).execute() + client.api.getIndexes(null, ifModifiedSince).execute() with(mockWebServerRule.mockWebServer.takeRequest()) { requestLine `should contain` "ifModifiedSince=$ifModifiedSince" @@ -151,7 +197,7 @@ class SubsonicAPITest { val musicFolderId = 110L val ifModifiedSince = System.currentTimeMillis() - api.getApi().getIndexes(musicFolderId, ifModifiedSince).execute() + client.api.getIndexes(musicFolderId, ifModifiedSince).execute() with(mockWebServerRule.mockWebServer.takeRequest()) { requestLine `should contain` "musicFolderId=$musicFolderId" @@ -161,14 +207,14 @@ class SubsonicAPITest { @Test fun `Should parse get indexes error response`() { - val response = checkErrorCallParsed { api.getApi().getIndexes(null, null).execute() } + val response = checkErrorCallParsed { client.api.getIndexes(null, null).execute() } response.indexes `should be` null } @Test fun `Should parse getMusicDirectory error response`() { - val response = checkErrorCallParsed { api.getApi().getMusicDirectory(1).execute() } + val response = checkErrorCallParsed { client.api.getMusicDirectory(1).execute() } response.musicDirectory `should be` null } @@ -178,7 +224,7 @@ class SubsonicAPITest { enqueueResponse("get_music_directory_ok.json") val directoryId = 124L - api.getApi().getMusicDirectory(directoryId).execute() + client.api.getMusicDirectory(directoryId).execute() mockWebServerRule.mockWebServer.takeRequest().requestLine `should contain` "id=$directoryId" } @@ -187,7 +233,7 @@ class SubsonicAPITest { fun `Should parse get music directory ok response`() { enqueueResponse("get_music_directory_ok.json") - val response = api.getApi().getMusicDirectory(1).execute() + val response = client.api.getMusicDirectory(1).execute() assertResponseSuccessful(response) diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPI.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPI.kt deleted file mode 100644 index 94a0fece..00000000 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPI.kt +++ /dev/null @@ -1,68 +0,0 @@ -package org.moire.ultrasonic.api.subsonic - -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.KotlinModule -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.converter.jackson.JacksonConverterFactory -import java.math.BigInteger - -/** - * Main entry for Subsonic API calls. - * - * @see SubsonicAPI http://www.subsonic.org/pages/api.jsp - */ -class SubsonicAPI(baseUrl: String, - username: String, - private val password: String, - clientProtocolVersion: SubsonicAPIVersions, - clientID: String, - debug: Boolean = false) { - private val okHttpClient = OkHttpClient.Builder() - .addInterceptor { chain -> - // Adds default request params - val originalRequest = chain.request() - val newUrl = originalRequest.url().newBuilder() - .addQueryParameter("u", username) - .addQueryParameter("p", passwordHex()) - .addQueryParameter("v", clientProtocolVersion.restApiVersion) - .addQueryParameter("c", clientID) - .addQueryParameter("f", "json") - .build() - chain.proceed(originalRequest.newBuilder().url(newUrl).build()) - } - .apply { - if (debug) { - val loggingInterceptor = HttpLoggingInterceptor() - loggingInterceptor.level = HttpLoggingInterceptor.Level.BASIC - this.addInterceptor(loggingInterceptor) - } - }.build() - - private val jacksonMapper = ObjectMapper() - .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true) - .registerModule(KotlinModule()) - - private val retrofit = Retrofit.Builder() - .baseUrl(baseUrl) - .client(okHttpClient) - .addConverterFactory(JacksonConverterFactory.create(jacksonMapper)) - .build() - - private val subsonicAPI = retrofit.create(SubsonicAPIDefinition::class.java) - - /** - * Get API instance. - * - * @return initialized API instance - */ - fun getApi(): SubsonicAPIDefinition = subsonicAPI - - private fun passwordHex() = "enc:${password.toHexBytes()}" - - private fun String.toHexBytes(): String { - return String.format("%040x", BigInteger(1, this.toByteArray())) - } -} \ No newline at end of file diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt new file mode 100644 index 00000000..e1ab6a2a --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt @@ -0,0 +1,113 @@ +package org.moire.ultrasonic.api.subsonic + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory +import java.lang.IllegalStateException +import java.math.BigInteger +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.security.SecureRandom + +/** + * Subsonic API client that provides api access. + * + * For supported API calls see [SubsonicAPIDefinition]. + * + * @author Yahor Berdnikau + */ +class SubsonicAPIClient(baseUrl: String, + username: String, + private val password: String, + clientProtocolVersion: SubsonicAPIVersions, + clientID: String, + debug: Boolean = false) { + companion object { + internal val HEX_ARRAY = "0123456789ABCDEF".toCharArray() + } + + private val okHttpClient = OkHttpClient.Builder() + .addInterceptor { chain -> + // Adds default request params + val originalRequest = chain.request() + val newUrl = originalRequest.url().newBuilder() + .addQueryParameter("u", username) + .also { + it.addPasswordQueryParam(clientProtocolVersion) + } + .addQueryParameter("v", clientProtocolVersion.restApiVersion) + .addQueryParameter("c", clientID) + .addQueryParameter("f", "json") + .build() + chain.proceed(originalRequest.newBuilder().url(newUrl).build()) + } + .also { + if (debug) { + it.addLogging() + } + }.build() + + private val jacksonMapper = ObjectMapper() + .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true) + .registerModule(KotlinModule()) + + private val retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .client(okHttpClient) + .addConverterFactory(JacksonConverterFactory.create(jacksonMapper)) + .build() + + val api: SubsonicAPIDefinition = retrofit.create(SubsonicAPIDefinition::class.java) + + private val salt: String by lazy { + val secureRandom = SecureRandom() + BigInteger(130, secureRandom).toString(32) + } + + private val passwordMD5Hash: String by lazy { + try { + val md5Digest = MessageDigest.getInstance("MD5") + md5Digest.digest("$password$salt".toByteArray()).toHexBytes() + } catch (e: NoSuchAlgorithmException) { + throw IllegalStateException(e) + } + } + + private val passwordHex: String by lazy { + "enc:${password.toHexBytes()}" + } + + private fun String.toHexBytes(): String { + return this.toByteArray().toHexBytes() + } + + private fun ByteArray.toHexBytes(): String { + val hexChars = CharArray(this.size * 2) + for (j in 0..this.lastIndex) { + val v = this[j].toInt().and(0xFF) + hexChars[j * 2] = HEX_ARRAY[v.ushr(4)] + hexChars[j * 2 + 1] = HEX_ARRAY[v.and(0x0F)] + } + return String(hexChars) + } + + private fun OkHttpClient.Builder.addLogging() { + val loggingInterceptor = HttpLoggingInterceptor() + loggingInterceptor.level = HttpLoggingInterceptor.Level.BASIC + this.addInterceptor(loggingInterceptor) + } + + private fun HttpUrl.Builder.addPasswordQueryParam(clientProtocolVersion: SubsonicAPIVersions) { + if (clientProtocolVersion < SubsonicAPIVersions.V1_13_0) { + this.addQueryParameter("p", passwordHex) + } else { + this.addQueryParameter("t", passwordMD5Hash) + this.addQueryParameter("s", salt) + } + } +} \ No newline at end of file diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 506d0370..2f494fee 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -40,8 +40,10 @@ dependencies { } testCompile(testing.mockitoKotlin) { exclude module: "kotlin-stdlib" + exclude module: "kotlin-reflect" } testCompile(testing.kluent) { exclude module: "kotlin-stdlib" + exclude module: "kotlin-reflect" } } From 24b85a164744874bb6480bb67c6bbe0a4c2b2bce Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Mon, 24 Jul 2017 22:35:25 +0200 Subject: [PATCH 04/14] Fix base url is not have rest endpoint part. Signed-off-by: Yahor Berdnikau --- .../org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt index e1ab6a2a..24bf597a 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt @@ -57,7 +57,7 @@ class SubsonicAPIClient(baseUrl: String, .registerModule(KotlinModule()) private val retrofit = Retrofit.Builder() - .baseUrl(baseUrl) + .baseUrl("$baseUrl/rest/") .client(okHttpClient) .addConverterFactory(JacksonConverterFactory.create(jacksonMapper)) .build() From f054fae2aae819047f5555fc4b90d5c4347738ab Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Mon, 24 Jul 2017 22:38:53 +0200 Subject: [PATCH 05/14] Add missing 1.15.0 subsonic api version. Signed-off-by: Yahor Berdnikau --- .../org/moire/ultrasonic/api/subsonic/SubsonicAPIVersions.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIVersions.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIVersions.kt index 70596e7e..b6bb618c 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIVersions.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIVersions.kt @@ -25,9 +25,11 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion: V1_11_0("5.1", "1.11.0"), V1_12_0("5.2", "1.12.0"), V1_13_0("5.3", "1.13.0"), - V1_14_0("6.0", "1.14.0"); + V1_14_0("6.0", "1.14.0"), + V1_15_0("6.1", "1.15.0"); companion object { + @JvmStatic fun fromApiVersion(apiVersion: String): SubsonicAPIVersions { when (apiVersion) { "1.1.0" -> return V1_1_0 @@ -45,6 +47,7 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion: "1.12.0" -> return V1_12_0 "1.13.0" -> return V1_13_0 "1.14.0" -> return V1_14_0 + "1.15.0" -> return V1_15_0 } throw IllegalArgumentException("Unknown api version $apiVersion") } From 1caeaa9fe6157381c6f5026f67791e441d502113 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Mon, 24 Jul 2017 22:41:22 +0200 Subject: [PATCH 06/14] Add using new api for ping call. Signed-off-by: Yahor Berdnikau --- ultrasonic/build.gradle | 4 + .../service/MusicServiceFactory.java | 57 +++++++++-- .../service/OfflineMusicService.java | 7 +- .../ultrasonic/service/RESTMusicService.java | 99 ++++++++++--------- 4 files changed, 109 insertions(+), 58 deletions(-) diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 2f494fee..e35367f2 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -21,6 +21,10 @@ android { sourceSets { test.java.srcDirs += 'src/test/kotlin' } + + packagingOptions { + exclude 'META-INF/LICENSE' + } } dependencies { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicServiceFactory.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicServiceFactory.java index 4cc80d75..a36de1df 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicServiceFactory.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicServiceFactory.java @@ -19,20 +19,61 @@ package org.moire.ultrasonic.service; import android.content.Context; +import android.content.SharedPreferences; +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions; +import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Util; /** * @author Sindre Mehus * @version $Id$ */ -public class MusicServiceFactory -{ - private static final MusicService REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService()); - private static final MusicService OFFLINE_MUSIC_SERVICE = new OfflineMusicService(); +public class MusicServiceFactory { + private static MusicService REST_MUSIC_SERVICE = null; + private static MusicService OFFLINE_MUSIC_SERVICE = null; - public static MusicService getMusicService(Context context) - { - return Util.isOffline(context) ? OFFLINE_MUSIC_SERVICE : REST_MUSIC_SERVICE; - } + public static MusicService getMusicService(Context context) { + if (Util.isOffline(context)) { + if (OFFLINE_MUSIC_SERVICE == null) { + synchronized (MusicServiceFactory.class) { + if (OFFLINE_MUSIC_SERVICE == null) { + OFFLINE_MUSIC_SERVICE = new OfflineMusicService(createSubsonicApiClient(context)); + } + } + } + + return OFFLINE_MUSIC_SERVICE; + } else { + if (REST_MUSIC_SERVICE == null) { + synchronized (MusicServiceFactory.class) { + if (REST_MUSIC_SERVICE == null) { + REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService( + createSubsonicApiClient(context))); + } + } + } + + return REST_MUSIC_SERVICE; + } + } + + 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); + + if (serverUrl == null || + username == null || + password == null) { + throw new IllegalStateException("Server connection data is not available!"); + } + + return new SubsonicAPIClient(serverUrl, username, password, + SubsonicAPIVersions.fromApiVersion(Constants.REST_PROTOCOL_VERSION), + Constants.REST_CLIENT_ID, true); + } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java index 44cc47f2..56d67b5e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java @@ -23,6 +23,7 @@ import android.graphics.Bitmap; import android.media.MediaMetadataRetriever; import android.util.Log; +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; import org.moire.ultrasonic.domain.Artist; import org.moire.ultrasonic.domain.Genre; import org.moire.ultrasonic.domain.Indexes; @@ -66,7 +67,11 @@ public class OfflineMusicService extends RESTMusicService private static final String TAG = OfflineMusicService.class.getSimpleName(); private static final Pattern COMPILE = Pattern.compile(" "); - @Override + public OfflineMusicService(SubsonicAPIClient subsonicAPIClient) { + super(subsonicAPIClient); + } + + @Override public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception { return true; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java index 536102b1..7e2f0c4c 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -23,10 +23,38 @@ import android.content.SharedPreferences; import android.graphics.Bitmap; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.support.annotation.Nullable; import android.util.Log; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.conn.params.ConnManagerParams; +import org.apache.http.conn.params.ConnPerRouteBean; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.scheme.SocketFactory; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.ExecutionContext; +import org.apache.http.protocol.HttpContext; import org.moire.ultrasonic.R; -import org.moire.ultrasonic.Test.service.GetPodcastEpisodesTestReaderProvider; +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; +import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse; import org.moire.ultrasonic.domain.Bookmark; import org.moire.ultrasonic.domain.ChatMessage; import org.moire.ultrasonic.domain.Genre; @@ -36,7 +64,6 @@ import org.moire.ultrasonic.domain.Lyrics; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicFolder; import org.moire.ultrasonic.domain.Playlist; -import org.moire.ultrasonic.domain.PodcastEpisode; import org.moire.ultrasonic.domain.PodcastsChannel; import org.moire.ultrasonic.domain.SearchCriteria; import org.moire.ultrasonic.domain.SearchResult; @@ -71,36 +98,8 @@ import org.moire.ultrasonic.util.CancellableTask; import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.FileUtil; import org.moire.ultrasonic.util.ProgressListener; -import org.moire.ultrasonic.util.StreamProxy; import org.moire.ultrasonic.util.Util; -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpHost; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.conn.params.ConnManagerParams; -import org.apache.http.conn.params.ConnPerRouteBean; -import org.apache.http.conn.scheme.PlainSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.conn.scheme.SocketFactory; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.message.BasicHeader; -import org.apache.http.message.BasicNameValuePair; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.apache.http.protocol.BasicHttpContext; -import org.apache.http.protocol.ExecutionContext; -import org.apache.http.protocol.HttpContext; - import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; @@ -110,8 +109,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; -import java.io.StringReader; -import java.lang.reflect.Array; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; @@ -119,9 +116,10 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; +import retrofit2.Response; + import static java.util.Arrays.asList; /** @@ -155,11 +153,12 @@ public class RESTMusicService implements MusicService private String redirectFrom; private String redirectTo; private final ThreadSafeClientConnManager connManager; + private SubsonicAPIClient subsonicAPIClient; - public RESTMusicService() - { + public RESTMusicService(SubsonicAPIClient subsonicAPIClient) { + this.subsonicAPIClient = subsonicAPIClient; - // Create and initialize default HTTP parameters + // Create and initialize default HTTP parameters HttpParams params = new BasicHttpParams(); ConnManagerParams.setMaxTotalConnections(params, 20); ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRouteBean(20)); @@ -195,19 +194,16 @@ public class RESTMusicService implements MusicService } } - @Override - public void ping(Context context, ProgressListener progressListener) throws Exception - { - Reader reader = getReader(context, progressListener, "ping", null); - try - { - new ErrorParser(context).parse(reader); - } - finally - { - Util.close(reader); - } - } + @Override + public void ping(Context context, ProgressListener progressListener) throws Exception { + updateProgressListener(progressListener); + + final Response response = subsonicAPIClient.getApi().ping().execute(); + if (!response.isSuccessful() || + response.body().getStatus() == SubsonicResponse.Status.ERROR) { + throw new IOException("Ping request failed"); + } + } @Override public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception @@ -1788,4 +1784,9 @@ public class RESTMusicService implements MusicService } } + private void updateProgressListener(@Nullable final ProgressListener progressListener) { + if (progressListener != null) { + progressListener.updateProgress(R.string.service_connecting); + } + } } From 0d3c0f0334e9d1792a222517a079b4ec386705b6 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Tue, 25 Jul 2017 22:23:47 +0200 Subject: [PATCH 07/14] Add missing fields to License entity. Add email and licenseExpires fields. Make all fields have default value, this allows to make license field in LicenseResponse non-null. Signed-off-by: Yahor Berdnikau --- .../ultrasonic/api/subsonic/SubsonicAPIClientTest.kt | 12 ++++++++++-- .../src/integrationTest/resources/license_ok.json | 2 ++ .../moire/ultrasonic/api/subsonic/models/License.kt | 8 ++++++-- .../api/subsonic/response/LicenseResponse.kt | 3 +-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClientTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClientTest.kt index a8e314c9..c0e58df1 100644 --- a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClientTest.kt +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClientTest.kt @@ -4,6 +4,7 @@ import okhttp3.mockwebserver.MockResponse import okio.Okio import org.amshove.kluent.`should be` import org.amshove.kluent.`should contain` +import org.amshove.kluent.`should equal to` import org.amshove.kluent.`should equal` import org.amshove.kluent.`should not be` import org.amshove.kluent.`should not contain` @@ -110,7 +111,10 @@ class SubsonicAPIClientTest { assertResponseSuccessful(response) with(response.body()) { assertBaseResponseOk() - license `should equal` License(true, parseDate("2016-11-23T20:17:15.206Z")) + license `should equal` License(valid = true, + trialExpires = parseDate("2016-11-23T20:17:15.206Z"), + email = "someone@example.net", + licenseExpires = parseDate("8994-08-17T07:12:55.807Z")) } } @@ -118,7 +122,11 @@ class SubsonicAPIClientTest { fun `Should parse get license error response`() { val response = checkErrorCallParsed { client.api.getLicense().execute() } - response.license `should be` null + response.license `should not be` null + with(response.license) { + email `should equal to` "" + valid `should equal to` false + } } @Test diff --git a/subsonic-api/src/integrationTest/resources/license_ok.json b/subsonic-api/src/integrationTest/resources/license_ok.json index 5a800572..451e54ff 100644 --- a/subsonic-api/src/integrationTest/resources/license_ok.json +++ b/subsonic-api/src/integrationTest/resources/license_ok.json @@ -4,6 +4,8 @@ "version" : "1.13.0", "license" : { "valid" : true, + "email" : "someone@example.net", + "licenseExpires" : "8994-08-17T07:12:55.807Z", "trialExpires" : "2016-11-23T20:17:15.206Z" } } diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/License.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/License.kt index 5dffe77b..d323ff74 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/License.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/License.kt @@ -1,5 +1,9 @@ package org.moire.ultrasonic.api.subsonic.models -import java.util.* +import java.util.Calendar -data class License(val valid: Boolean, val trialExpires: Calendar) \ No newline at end of file +data class License( + val valid: Boolean = false, + val email: String = "", + val trialExpires: Calendar = Calendar.getInstance(), + val licenseExpires: Calendar = Calendar.getInstance()) \ No newline at end of file diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/LicenseResponse.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/LicenseResponse.kt index ea0186af..e5bcf81b 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/LicenseResponse.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/LicenseResponse.kt @@ -3,9 +3,8 @@ package org.moire.ultrasonic.api.subsonic.response import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicError import org.moire.ultrasonic.api.subsonic.models.License -import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse -class LicenseResponse(val license: License?, +class LicenseResponse(val license: License = License(), status: Status, version: SubsonicAPIVersions, error: SubsonicError?): From 67601262de34fe92cbfe39e8242a85ea1d07c230 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Tue, 25 Jul 2017 22:31:49 +0200 Subject: [PATCH 08/14] Use getLicense() method from Subsonic API kotlin implementation. Replace in RESTMusicService isLicenseValid() method body to use new kotlin subsonic API. --- .../ultrasonic/service/RESTMusicService.java | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java index 7e2f0c4c..e69330b7 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -23,6 +23,7 @@ import android.content.SharedPreferences; import android.graphics.Bitmap; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; @@ -54,6 +55,7 @@ import org.apache.http.protocol.ExecutionContext; import org.apache.http.protocol.HttpContext; import org.moire.ultrasonic.R; import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; +import org.moire.ultrasonic.api.subsonic.response.LicenseResponse; import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse; import org.moire.ultrasonic.domain.Bookmark; import org.moire.ultrasonic.domain.ChatMessage; @@ -67,7 +69,6 @@ import org.moire.ultrasonic.domain.Playlist; import org.moire.ultrasonic.domain.PodcastsChannel; import org.moire.ultrasonic.domain.SearchCriteria; import org.moire.ultrasonic.domain.SearchResult; -import org.moire.ultrasonic.domain.ServerInfo; import org.moire.ultrasonic.domain.Share; import org.moire.ultrasonic.domain.UserInfo; import org.moire.ultrasonic.domain.Version; @@ -78,7 +79,6 @@ import org.moire.ultrasonic.service.parser.ErrorParser; import org.moire.ultrasonic.service.parser.GenreParser; import org.moire.ultrasonic.service.parser.IndexesParser; import org.moire.ultrasonic.service.parser.JukeboxStatusParser; -import org.moire.ultrasonic.service.parser.LicenseParser; import org.moire.ultrasonic.service.parser.LyricsParser; import org.moire.ultrasonic.service.parser.MusicDirectoryParser; import org.moire.ultrasonic.service.parser.MusicFoldersParser; @@ -199,26 +199,19 @@ public class RESTMusicService implements MusicService updateProgressListener(progressListener); final Response response = subsonicAPIClient.getApi().ping().execute(); - if (!response.isSuccessful() || - response.body().getStatus() == SubsonicResponse.Status.ERROR) { - throw new IOException("Ping request failed"); - } + checkResponseSuccessful(response); } - @Override - public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception - { - Reader reader = getReader(context, progressListener, "getLicense", null); - try - { - ServerInfo serverInfo = new LicenseParser(context).parse(reader); - return serverInfo.isLicenseValid(); - } - finally - { - Util.close(reader); - } - } + @Override + public boolean isLicenseValid(Context context, ProgressListener progressListener) + throws Exception { + updateProgressListener(progressListener); + + final Response response = subsonicAPIClient.getApi().getLicense().execute(); + + checkResponseSuccessful(response); + return response.body().getLicense().getValid(); + } @Override public List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception @@ -1789,4 +1782,19 @@ public class RESTMusicService implements MusicService progressListener.updateProgress(R.string.service_connecting); } } + + private void checkResponseSuccessful(@NonNull final Response response) + throws IOException { + if (response.isSuccessful() && + response.body().getStatus() == SubsonicResponse.Status.OK) { + return; + } + + if (response.body().getStatus() == SubsonicResponse.Status.ERROR && + response.body().getError() != null) { + throw new IOException("Server error: " + response.body().getError().getCode()); + } else { + throw new IOException("Failed to perform request: " + response.code()); + } + } } From 9ed09212185b0ef3a39a3939aab825e442e5d4b8 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Wed, 26 Jul 2017 21:37:16 +0200 Subject: [PATCH 09/14] Make MusicFolder has default values. Signed-off-by: Yahor Berdnikau --- .../ultrasonic/api/subsonic/SubsonicAPIClientTest.kt | 2 +- .../moire/ultrasonic/api/subsonic/models/MusicFolder.kt | 2 +- .../api/subsonic/response/MusicFoldersResponse.kt | 2 +- .../org/moire/ultrasonic/data/SubsonicAPIConverter.kt | 5 +++++ .../kotlin/org/moire/ultrasonic/data/APIConverterTest.kt | 8 ++++++++ 5 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt create mode 100644 ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClientTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClientTest.kt index c0e58df1..0dd81d46 100644 --- a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClientTest.kt +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClientTest.kt @@ -146,7 +146,7 @@ class SubsonicAPIClientTest { fun `Should parse get music folders error response`() { val response = checkErrorCallParsed { client.api.getMusicFolders().execute() } - response.musicFolders `should be` null + response.musicFolders `should equal` emptyList() } @Test diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/MusicFolder.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/MusicFolder.kt index 13484b2c..291a55e1 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/MusicFolder.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/MusicFolder.kt @@ -1,3 +1,3 @@ package org.moire.ultrasonic.api.subsonic.models -data class MusicFolder(val id: Long, val name: String) \ No newline at end of file +data class MusicFolder(val id: Long = -1, val name: String = "") \ No newline at end of file diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/MusicFoldersResponse.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/MusicFoldersResponse.kt index de133a27..24f91058 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/MusicFoldersResponse.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/MusicFoldersResponse.kt @@ -14,7 +14,7 @@ class MusicFoldersResponse(status: Status, version: SubsonicAPIVersions, error: SubsonicError?, @JsonDeserialize(using = MusicFoldersDeserializer::class) - val musicFolders: List?): + val musicFolders: List = emptyList()): SubsonicResponse(status, version, error) { companion object { class MusicFoldersDeserializer(): JsonDeserializer>() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt new file mode 100644 index 00000000..c3bcb28d --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt @@ -0,0 +1,5 @@ +package org.moire.ultrasonic.data + +/** + * Created by egorr on 26.07.17. + */ diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt new file mode 100644 index 00000000..3b556b48 --- /dev/null +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt @@ -0,0 +1,8 @@ +package org.moire.ultrasonic.data + +import org.junit.Assert.* + +/** + * Created by egorr on 26.07.17. + */ +class APIConverterTest \ No newline at end of file From 4fd5493d15322e0ef29587536f203fb6cb1ad458 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Wed, 26 Jul 2017 22:04:13 +0200 Subject: [PATCH 10/14] Use getMusicFolders() method from SubsonicAPI kotlin implementation. Signed-off-by: Yahor Berdnikau --- ultrasonic/build.gradle | 1 + .../ultrasonic/service/RESTMusicService.java | 58 +++++++++---------- .../ultrasonic/data/SubsonicAPIConverter.kt | 16 ++++- .../moire/ultrasonic/data/APIConverterTest.kt | 41 ++++++++++++- 4 files changed, 79 insertions(+), 37 deletions(-) diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index e35367f2..258d5052 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -19,6 +19,7 @@ android { } sourceSets { + main.java.srcDirs += 'src/main/kotlin' test.java.srcDirs += 'src/test/kotlin' } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java index e69330b7..9ed1f279 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -56,7 +56,9 @@ import org.apache.http.protocol.HttpContext; import org.moire.ultrasonic.R; import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; import org.moire.ultrasonic.api.subsonic.response.LicenseResponse; +import org.moire.ultrasonic.api.subsonic.response.MusicFoldersResponse; import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse; +import org.moire.ultrasonic.data.APIConverter; import org.moire.ultrasonic.domain.Bookmark; import org.moire.ultrasonic.domain.ChatMessage; import org.moire.ultrasonic.domain.Genre; @@ -81,7 +83,6 @@ import org.moire.ultrasonic.service.parser.IndexesParser; import org.moire.ultrasonic.service.parser.JukeboxStatusParser; import org.moire.ultrasonic.service.parser.LyricsParser; import org.moire.ultrasonic.service.parser.MusicDirectoryParser; -import org.moire.ultrasonic.service.parser.MusicFoldersParser; import org.moire.ultrasonic.service.parser.PlaylistParser; import org.moire.ultrasonic.service.parser.PlaylistsParser; import org.moire.ultrasonic.service.parser.PodcastEpisodeParser; @@ -213,27 +214,24 @@ public class RESTMusicService implements MusicService return response.body().getLicense().getValid(); } - @Override - public List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception - { - List cachedMusicFolders = readCachedMusicFolders(context); - if (cachedMusicFolders != null && !refresh) - { - return cachedMusicFolders; - } + @Override + public List getMusicFolders(boolean refresh, + Context context, + ProgressListener progressListener) throws Exception { + List cachedMusicFolders = readCachedMusicFolders(context); + if (cachedMusicFolders != null && !refresh) { + return cachedMusicFolders; + } - Reader reader = getReader(context, progressListener, "getMusicFolders", null); - try - { - List musicFolders = new MusicFoldersParser(context).parse(reader, progressListener); - writeCachedMusicFolders(context, musicFolders); - return musicFolders; - } - finally - { - Util.close(reader); - } - } + updateProgressListener(progressListener); + Response response = subsonicAPIClient.getApi().getMusicFolders().execute(); + checkResponseSuccessful(response); + + List musicFolders = APIConverter + .convertMusicFolderList(response.body().getMusicFolders()); + writeCachedMusicFolders(context, musicFolders); + return musicFolders; + } @Override public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception @@ -337,17 +335,15 @@ public class RESTMusicService implements MusicService return String.format("indexes-%d.ser", Math.abs(s.hashCode())); } - private static ArrayList readCachedMusicFolders(Context context) - { - String filename = getCachedMusicFoldersFilename(context); - return FileUtil.deserialize(context, filename); - } + private static List readCachedMusicFolders(Context context) { + String filename = getCachedMusicFoldersFilename(context); + return FileUtil.deserialize(context, filename); + } - private static void writeCachedMusicFolders(Context context, List musicFolders) - { - String filename = getCachedMusicFoldersFilename(context); - FileUtil.serialize(context, new ArrayList(musicFolders), filename); - } + private static void writeCachedMusicFolders(Context context, List musicFolders) { + String filename = getCachedMusicFoldersFilename(context); + FileUtil.serialize(context, new ArrayList<>(musicFolders), filename); + } private static String getCachedMusicFoldersFilename(Context context) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt index c3bcb28d..93a2935e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt @@ -1,5 +1,15 @@ +// Converts entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient] to app entities. +@file:JvmName("APIConverter") package org.moire.ultrasonic.data -/** - * Created by egorr on 26.07.17. - */ +import org.moire.ultrasonic.domain.MusicFolder + +typealias APIMusicFolder = org.moire.ultrasonic.api.subsonic.models.MusicFolder + +fun convertMusicFolder(entity: APIMusicFolder): MusicFolder { + return MusicFolder(entity.id.toString(), entity.name) +} + +fun convertMusicFolderList(entitiesList: List): List { + return entitiesList.map { convertMusicFolder(it) } +} \ No newline at end of file diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt index 3b556b48..63b49026 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt @@ -1,8 +1,43 @@ +@file:Suppress("IllegalIdentifier") + package org.moire.ultrasonic.data -import org.junit.Assert.* +import org.amshove.kluent.`should equal to` +import org.junit.Test +import org.moire.ultrasonic.api.subsonic.models.MusicFolder /** - * Created by egorr on 26.07.17. + * Unit test for functions in SubsonicAPIConverter file. + * + * @author Yahor Berdnikau */ -class APIConverterTest \ No newline at end of file +class APIConverterTest { + @Test + fun `Should convert MusicFolder entity`() { + val entity = createMusicFolder(10, "some-name") + + val convertedEntity = convertMusicFolder(entity) + + convertedEntity.name `should equal to` "some-name" + convertedEntity.id `should equal to` 10.toString() + } + + @Test + fun `Should convert list of MusicFolder entities`() { + val entityList = listOf( + createMusicFolder(3, "some-name-3"), + createMusicFolder(4, "some-name-4") + ) + + val convertedList = convertMusicFolderList(entityList) + + convertedList.size `should equal to` 2 + convertedList[0].id `should equal to` 3.toString() + convertedList[0].name `should equal to` "some-name-3" + convertedList[1].id `should equal to` 4.toString() + convertedList[1].name `should equal to` "some-name-4" + } + + private fun createMusicFolder(id: Long = 0, name: String = ""): MusicFolder = + MusicFolder(id, name) +} \ No newline at end of file From a8ab61ec3b1719e6b9dba0821f8293f04d1e264e Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Wed, 26 Jul 2017 22:54:50 +0200 Subject: [PATCH 11/14] Remove unused LicenseParser. Signed-off-by: Yahor Berdnikau --- .../service/parser/LicenseParser.java | 73 ------------------- 1 file changed, 73 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/LicenseParser.java diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/LicenseParser.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/LicenseParser.java deleted file mode 100644 index bc16dba2..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/LicenseParser.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2009 (C) Sindre Mehus - */ -package org.moire.ultrasonic.service.parser; - -import android.content.Context; - -import org.xmlpull.v1.XmlPullParser; - -import java.io.Reader; - -import org.moire.ultrasonic.domain.ServerInfo; -import org.moire.ultrasonic.domain.Version; - -/** - * @author Sindre Mehus - */ -public class LicenseParser extends AbstractParser -{ - - public LicenseParser(Context context) - { - super(context); - } - - public ServerInfo parse(Reader reader) throws Exception - { - - init(reader); - - ServerInfo serverInfo = new ServerInfo(); - int eventType; - do - { - eventType = nextParseEvent(); - if (eventType == XmlPullParser.START_TAG) - { - String name = getElementName(); - if ("subsonic-response".equals(name)) - { - serverInfo.setRestVersion(new Version(get("version"))); - } - else if ("license".equals(name)) - { - serverInfo.setLicenseValid(getBoolean("valid")); - } - else if ("error".equals(name)) - { - handleError(); - } - } - } while (eventType != XmlPullParser.END_DOCUMENT); - - validate(); - - return serverInfo; - } -} \ No newline at end of file From 987ed9faf63cb2efeec0082956e459d6d6f549b5 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Wed, 26 Jul 2017 22:56:07 +0200 Subject: [PATCH 12/14] Remove unused MusicFolderParser. Signed-off-by: Yahor Berdnikau --- .../service/parser/MusicFoldersParser.java | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/MusicFoldersParser.java diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/MusicFoldersParser.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/MusicFoldersParser.java deleted file mode 100644 index 3e882554..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/MusicFoldersParser.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2009 (C) Sindre Mehus - */ -package org.moire.ultrasonic.service.parser; - -import java.io.Reader; -import java.util.ArrayList; -import java.util.List; - -import org.xmlpull.v1.XmlPullParser; - -import android.content.Context; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.MusicFolder; -import org.moire.ultrasonic.util.ProgressListener; - -/** - * @author Sindre Mehus - */ -public class MusicFoldersParser extends AbstractParser -{ - - public MusicFoldersParser(Context context) - { - super(context); - } - - public List parse(Reader reader, ProgressListener progressListener) throws Exception - { - - updateProgress(progressListener, R.string.parser_reading); - init(reader); - - List result = new ArrayList(); - int eventType; - do - { - eventType = nextParseEvent(); - if (eventType == XmlPullParser.START_TAG) - { - String tag = getElementName(); - if ("musicFolder".equals(tag)) - { - String id = get("id"); - String name = get("name"); - result.add(new MusicFolder(id, name)); - } - else if ("error".equals(tag)) - { - handleError(); - } - } - } while (eventType != XmlPullParser.END_DOCUMENT); - - validate(); - updateProgress(progressListener, R.string.parser_reading_done); - - return result; - } - -} \ No newline at end of file From ce90d5a3e94e696d32366c3f70cbef276cecf901 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Wed, 26 Jul 2017 22:58:09 +0200 Subject: [PATCH 13/14] Use different messages in progress listener. Signed-off-by: Yahor Berdnikau --- .../moire/ultrasonic/service/RESTMusicService.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java index 9ed1f279..a88bb2c1 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -25,6 +25,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.StringRes; import android.util.Log; import org.apache.http.Header; @@ -197,7 +198,7 @@ public class RESTMusicService implements MusicService @Override public void ping(Context context, ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener); + updateProgressListener(progressListener, R.string.service_connecting); final Response response = subsonicAPIClient.getApi().ping().execute(); checkResponseSuccessful(response); @@ -206,7 +207,7 @@ public class RESTMusicService implements MusicService @Override public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener); + updateProgressListener(progressListener, R.string.service_connecting); final Response response = subsonicAPIClient.getApi().getLicense().execute(); @@ -223,7 +224,7 @@ public class RESTMusicService implements MusicService return cachedMusicFolders; } - updateProgressListener(progressListener); + updateProgressListener(progressListener, R.string.parser_reading); Response response = subsonicAPIClient.getApi().getMusicFolders().execute(); checkResponseSuccessful(response); @@ -1773,9 +1774,10 @@ public class RESTMusicService implements MusicService } } - private void updateProgressListener(@Nullable final ProgressListener progressListener) { + private void updateProgressListener(@Nullable final ProgressListener progressListener, + @StringRes final int messageId) { if (progressListener != null) { - progressListener.updateProgress(R.string.service_connecting); + progressListener.updateProgress(messageId); } } From 7a5035e725dd8598c7dc5e4c84630f1212676c34 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Thu, 27 Jul 2017 08:16:24 +0200 Subject: [PATCH 14/14] Reset MusicService when user changes active server. Signed-off-by: Yahor Berdnikau --- .../service/MusicServiceFactory.java | 24 +++++++++++++++++-- .../java/org/moire/ultrasonic/util/Util.java | 2 ++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicServiceFactory.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicServiceFactory.java index a36de1df..d7d35c36 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicServiceFactory.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicServiceFactory.java @@ -20,7 +20,9 @@ package org.moire.ultrasonic.service; import android.content.Context; import android.content.SharedPreferences; +import android.util.Log; +import org.moire.ultrasonic.BuildConfig; import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions; import org.moire.ultrasonic.util.Constants; @@ -31,11 +33,13 @@ import org.moire.ultrasonic.util.Util; * @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) { @@ -46,6 +50,7 @@ public class MusicServiceFactory { 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) { @@ -59,6 +64,18 @@ public class MusicServiceFactory { } } + /** + * 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); @@ -69,11 +86,14 @@ public class MusicServiceFactory { if (serverUrl == null || username == null || password == null) { - throw new IllegalStateException("Server connection data is not available!"); + Log.i("MusicServiceFactory", "Server credentials is not available"); + return new SubsonicAPIClient("http://localhost", "", "", + SubsonicAPIVersions.fromApiVersion(Constants.REST_PROTOCOL_VERSION), + Constants.REST_CLIENT_ID, BuildConfig.DEBUG); } return new SubsonicAPIClient(serverUrl, username, password, SubsonicAPIVersions.fromApiVersion(Constants.REST_PROTOCOL_VERSION), - Constants.REST_CLIENT_ID, true); + Constants.REST_CLIENT_ID, BuildConfig.DEBUG); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java index c191d179..4c6c11c9 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java @@ -67,6 +67,7 @@ import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.DownloadServiceImpl; import org.apache.http.HttpEntity; +import org.moire.ultrasonic.service.MusicServiceFactory; import java.io.ByteArrayOutputStream; import java.io.Closeable; @@ -192,6 +193,7 @@ public class Util extends DownloadActivity public static void setActiveServer(Context context, int instance) { + MusicServiceFactory.resetMusicService(); SharedPreferences preferences = getPreferences(context); SharedPreferences.Editor editor = preferences.edit(); editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance);