Merge branch 'nitehu-feature/runtime-permissions' into develop

This commit is contained in:
Óscar García Amor 2020-06-17 10:25:35 +02:00
commit d0705cd933
133 changed files with 1512 additions and 886 deletions

View File

@ -12,7 +12,7 @@ jobs:
key: gradle-cache-{{ checksum "dependencies.gradle" }} key: gradle-cache-{{ checksum "dependencies.gradle" }}
- run: - run:
name: clean gradle.properties name: clean gradle.properties
command: echo "" > gradle.properties command: echo -e "android.useAndroidX=true\nandroid.enableJetifier=true\n" > gradle.properties
- run: - run:
name: checkstyle name: checkstyle
command: ./gradlew -Pqc ktlintCheck command: ./gradlew -Pqc ktlintCheck

View File

@ -38,7 +38,7 @@ allprojects {
apply from: 'gradle_scripts/jacoco.gradle' apply from: 'gradle_scripts/jacoco.gradle'
task wrapper(type: Wrapper) { wrapper {
gradleVersion(versions.gradle) gradleVersion(versions.gradle)
distributionType("all") distributionType("all")
} }

View File

@ -20,14 +20,14 @@ private val artistSerializer get() = object : ObjectSerializer<Artist>(SERIALIZE
item: Artist item: Artist
) { ) {
output.writeString(item.id) output.writeString(item.id)
.writeString(item.name) .writeString(item.name)
.writeString(item.index) .writeString(item.index)
.writeString(item.coverArt) .writeString(item.coverArt)
.apply { .apply {
val albumCount = item.albumCount val albumCount = item.albumCount
if (albumCount != null) writeLong(albumCount) else writeNull() if (albumCount != null) writeLong(albumCount) else writeNull()
} }
.writeInt(item.closeness) .writeInt(item.closeness)
} }
override fun deserializeObject( override fun deserializeObject(

View File

@ -20,9 +20,9 @@ private val indexesSerializer get() = object : ObjectSerializer<Indexes>(SERIALI
) { ) {
val artistListSerializer = getArtistListSerializer() val artistListSerializer = getArtistListSerializer()
output.writeLong(item.lastModified) output.writeLong(item.lastModified)
.writeString(item.ignoredArticles) .writeString(item.ignoredArticles)
.writeObject<MutableList<Artist>>(context, item.shortcuts, artistListSerializer) .writeObject<MutableList<Artist>>(context, item.shortcuts, artistListSerializer)
.writeObject<MutableList<Artist>>(context, item.artists, artistListSerializer) .writeObject<MutableList<Artist>>(context, item.artists, artistListSerializer)
} }
override fun deserializeObject( override fun deserializeObject(
@ -37,8 +37,10 @@ private val indexesSerializer get() = object : ObjectSerializer<Indexes>(SERIALI
val ignoredArticles = input.readString() ?: return null val ignoredArticles = input.readString() ?: return null
val shortcutsList = input.readObject(context, artistListDeserializer) ?: return null val shortcutsList = input.readObject(context, artistListDeserializer) ?: return null
val artistsList = input.readObject(context, artistListDeserializer) ?: return null val artistsList = input.readObject(context, artistListDeserializer) ?: return null
return Indexes(lastModified, ignoredArticles, shortcutsList.toMutableList(), return Indexes(
artistsList.toMutableList()) lastModified, ignoredArticles, shortcutsList.toMutableList(),
artistsList.toMutableList()
)
} }
} }

View File

@ -41,10 +41,10 @@ private val musicFolderSerializer = object : ObjectSerializer<MusicFolder>(SERIA
fun getMusicFolderSerializer(): DomainEntitySerializer<MusicFolder> = musicFolderSerializer fun getMusicFolderSerializer(): DomainEntitySerializer<MusicFolder> = musicFolderSerializer
private val musicFolderListSerializer = private val musicFolderListSerializer =
CollectionSerializers.getListSerializer(musicFolderSerializer) CollectionSerializers.getListSerializer(musicFolderSerializer)
/** /**
* Serializer/deserializer for [List] of [MusicFolder] items. * Serializer/deserializer for [List] of [MusicFolder] items.
*/ */
fun getMusicFolderListSerializer(): DomainEntitySerializer<List<MusicFolder>> = fun getMusicFolderListSerializer(): DomainEntitySerializer<List<MusicFolder>> =
musicFolderListSerializer musicFolderListSerializer

View File

@ -2,11 +2,11 @@ package org.moire.ultrasonic.cache
import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.mock
import com.twitter.serial.util.SerializationUtils import com.twitter.serial.util.SerializationUtils
import java.io.File
import org.amshove.kluent.`it returns` import org.amshove.kluent.`it returns`
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import java.io.File
internal const val INTERNAL_DATA_FOLDER = "data" internal const val INTERNAL_DATA_FOLDER = "data"
internal const val INTERNAL_CACHE_FOLDER = "cache" internal const val INTERNAL_CACHE_FOLDER = "cache"

View File

@ -1,12 +1,12 @@
package org.moire.ultrasonic.cache package org.moire.ultrasonic.cache
import java.io.File
import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.`should contain` import org.amshove.kluent.`should contain`
import org.amshove.kluent.`should equal` import org.amshove.kluent.`should equal`
import org.junit.Test import org.junit.Test
import org.moire.ultrasonic.cache.serializers.getMusicFolderSerializer import org.moire.ultrasonic.cache.serializers.getMusicFolderSerializer
import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.domain.MusicFolder
import java.io.File
/** /**
* Integration test for [PermanentFileStorage]. * Integration test for [PermanentFileStorage].

View File

@ -32,8 +32,8 @@ class ArtistSerializerTest : BaseStorageTest() {
@Test @Test
fun `Should correctly serialize list of Artists`() { fun `Should correctly serialize list of Artists`() {
val itemsList = listOf( val itemsList = listOf(
Artist(id = "1"), Artist(id = "1"),
Artist(id = "2", name = "some") Artist(id = "2", name = "some")
) )
storage.store("some-name", itemsList, getArtistListSerializer()) storage.store("some-name", itemsList, getArtistListSerializer())
@ -45,8 +45,8 @@ class ArtistSerializerTest : BaseStorageTest() {
fun `Should correctly deserialize list of Artists`() { fun `Should correctly deserialize list of Artists`() {
val name = "some-name" val name = "some-name"
val itemsList = listOf( val itemsList = listOf(
Artist(id = "1"), Artist(id = "1"),
Artist(id = "2", name = "some") Artist(id = "2", name = "some")
) )
storage.store(name, itemsList, getArtistListSerializer()) storage.store(name, itemsList, getArtistListSerializer())

View File

@ -12,11 +12,10 @@ import org.moire.ultrasonic.domain.Indexes
class IndexesSerializerTest : BaseStorageTest() { class IndexesSerializerTest : BaseStorageTest() {
@Test @Test
fun `Should correctly serialize Indexes object`() { fun `Should correctly serialize Indexes object`() {
val item = Indexes(220L, "", mutableListOf( val item = Indexes(
Artist("12") 220L, "", mutableListOf(Artist("12")),
), mutableListOf( mutableListOf(Artist("233", "some"))
Artist("233", "some") )
))
storage.store("some-name", item, getIndexesSerializer()) storage.store("some-name", item, getIndexesSerializer())
@ -26,11 +25,10 @@ class IndexesSerializerTest : BaseStorageTest() {
@Test @Test
fun `Should correctly deserialize Indexes object`() { fun `Should correctly deserialize Indexes object`() {
val name = "some-name" val name = "some-name"
val item = Indexes(220L, "", mutableListOf( val item = Indexes(
Artist("12") 220L, "", mutableListOf(Artist("12")),
), mutableListOf( mutableListOf(Artist("233", "some"))
Artist("233", "some") )
))
storage.store(name, item, getIndexesSerializer()) storage.store(name, item, getIndexesSerializer())
val loadedItem = storage.load(name, getIndexesSerializer()) val loadedItem = storage.load(name, getIndexesSerializer())

View File

@ -32,8 +32,8 @@ class MusicFolderSerializerTest : BaseStorageTest() {
@Test @Test
fun `Should correctly serialize list of MusicFolders objects`() { fun `Should correctly serialize list of MusicFolders objects`() {
val itemsList = listOf( val itemsList = listOf(
MusicFolder("1", "1"), MusicFolder("1", "1"),
MusicFolder("2", "2") MusicFolder("2", "2")
) )
storage.store("some-name", itemsList, getMusicFolderListSerializer()) storage.store("some-name", itemsList, getMusicFolderListSerializer())
@ -45,8 +45,8 @@ class MusicFolderSerializerTest : BaseStorageTest() {
fun `Should correctly deserialize list of MusicFolder objects`() { fun `Should correctly deserialize list of MusicFolder objects`() {
val name = "some-name" val name = "some-name"
val itemsList = listOf( val itemsList = listOf(
MusicFolder("1", "1"), MusicFolder("1", "1"),
MusicFolder("2", "2") MusicFolder("2", "2")
) )
storage.store(name, itemsList, getMusicFolderListSerializer()) storage.store(name, itemsList, getMusicFolderListSerializer())

View File

@ -1,9 +1,8 @@
package org.moire.ultrasonic.domain package org.moire.ultrasonic.domain
import org.moire.ultrasonic.domain.MusicDirectory.Entry
import java.io.Serializable import java.io.Serializable
import java.util.Date import java.util.Date
import org.moire.ultrasonic.domain.MusicDirectory.Entry
data class Bookmark( data class Bookmark(
val position: Int = 0, val position: Int = 0,

View File

@ -1,7 +1,7 @@
package org.moire.ultrasonic.domain package org.moire.ultrasonic.domain
import org.moire.ultrasonic.domain.MusicDirectory.Entry
import java.io.Serializable import java.io.Serializable
import org.moire.ultrasonic.domain.MusicDirectory.Entry
data class Share( data class Share(
var id: String? = null, var id: String? = null,

View File

@ -7,7 +7,7 @@ import android.database.Cursor;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.support.v4.widget.CursorAdapter; import androidx.cursoradapter.widget.CursorAdapter;
/** /**

View File

@ -93,7 +93,7 @@ final class ActionBarHelperCompat {
Class supportActivity = activity.getClass(); Class supportActivity = activity.getClass();
Method getActionBar = supportActivity.getMethod("getSupportActionBar"); Method getActionBar = supportActivity.getMethod("getSupportActionBar");
mActionBar = getActionBar.invoke(activity, null); mActionBar = getActionBar.invoke(activity, (Object)null);
Class supportActionBar = mActionBar.getClass(); Class supportActionBar = mActionBar.getClass();
mHomeAsUpEnabled = supportActionBar.getMethod("setDisplayHomeAsUpEnabled", Boolean.TYPE); mHomeAsUpEnabled = supportActionBar.getMethod("setDisplayHomeAsUpEnabled", Boolean.TYPE);

View File

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

View File

@ -7,6 +7,7 @@ import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever import com.nhaarman.mockito_kotlin.whenever
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import com.squareup.picasso.Request import com.squareup.picasso.Request
import java.io.IOException
import org.amshove.kluent.`should equal` import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be` import org.amshove.kluent.`should not be`
import org.amshove.kluent.`should throw` import org.amshove.kluent.`should throw`
@ -16,7 +17,6 @@ import org.junit.runner.RunWith
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.response.StreamResponse import org.moire.ultrasonic.api.subsonic.response.StreamResponse
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import java.io.IOException
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class CoverArtRequestHandlerTest { class CoverArtRequestHandlerTest {

View File

@ -3,9 +3,9 @@ package org.moire.ultrasonic.subsonic.loader.image
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import com.squareup.picasso.Request import com.squareup.picasso.Request
import com.squareup.picasso.RequestHandler import com.squareup.picasso.RequestHandler
import java.io.IOException
import okio.Okio import okio.Okio
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import java.io.IOException
/** /**
* Loads avatars from subsonic api. * Loads avatars from subsonic api.

View File

@ -3,9 +3,9 @@ package org.moire.ultrasonic.subsonic.loader.image
import com.squareup.picasso.Picasso.LoadedFrom.NETWORK import com.squareup.picasso.Picasso.LoadedFrom.NETWORK
import com.squareup.picasso.Request import com.squareup.picasso.Request
import com.squareup.picasso.RequestHandler import com.squareup.picasso.RequestHandler
import java.io.IOException
import okio.Okio import okio.Okio
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import java.io.IOException
/** /**
* Loads cover arts from subsonic api. * Loads cover arts from subsonic api.
@ -14,8 +14,8 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request
override fun canHandleRequest(data: Request): Boolean { override fun canHandleRequest(data: Request): Boolean {
return with(data.uri) { return with(data.uri) {
scheme == SCHEME && scheme == SCHEME &&
authority == AUTHORITY && authority == AUTHORITY &&
path == "/$COVER_ART_PATH" path == "/$COVER_ART_PATH"
} }
} }

View File

@ -3,7 +3,7 @@ package org.moire.ultrasonic.subsonic.loader.image
import android.net.Uri import android.net.Uri
internal const val SCHEME = "subsonic_api" internal const val SCHEME = "subsonic_api"
internal const val AUTHORITY = BuildConfig.APPLICATION_ID internal const val AUTHORITY = BuildConfig.LIBRARY_PACKAGE_NAME
internal const val COVER_ART_PATH = "cover_art" internal const val COVER_ART_PATH = "cover_art"
internal const val AVATAR_PATH = "avatar" internal const val AVATAR_PATH = "avatar"
internal const val QUERY_ID = "id" internal const val QUERY_ID = "id"

View File

@ -1,5 +1,11 @@
package org.moire.ultrasonic.api.subsonic package org.moire.ultrasonic.api.subsonic
import java.io.InputStream
import java.nio.charset.Charset
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import okio.Okio import okio.Okio
@ -9,30 +15,29 @@ import org.amshove.kluent.`should not be`
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
import retrofit2.Response import retrofit2.Response
import java.io.InputStream
import java.nio.charset.Charset
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
const val USERNAME = "some-user" const val USERNAME = "some-user"
const val PASSWORD = "some-password" const val PASSWORD = "some-password"
val CLIENT_VERSION = SubsonicAPIVersions.V1_16_0 val CLIENT_VERSION = SubsonicAPIVersions.V1_16_0
const val CLIENT_ID = "test-client" const val CLIENT_ID = "test-client"
val dateFormat by lazy(LazyThreadSafetyMode.NONE, { val dateFormat by lazy(
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US) LazyThreadSafetyMode.NONE,
}) {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US)
}
)
fun MockWebServerRule.enqueueResponse(resourceName: String) { fun MockWebServerRule.enqueueResponse(resourceName: String) {
mockWebServer.enqueueResponse(resourceName) mockWebServer.enqueueResponse(resourceName)
} }
fun MockWebServer.enqueueResponse(resourceName: String) { fun MockWebServer.enqueueResponse(resourceName: String) {
enqueue(MockResponse() enqueue(
MockResponse()
.setBody(loadJsonResponse(resourceName)) .setBody(loadJsonResponse(resourceName))
.setHeader("Content-Type", "application/json;charset=UTF-8")) .setHeader("Content-Type", "application/json;charset=UTF-8")
)
} }
fun Any.loadJsonResponse(name: String): String { fun Any.loadJsonResponse(name: String): String {

View File

@ -31,7 +31,7 @@ class GetStreamUrlTest {
client = SubsonicAPIClient(config) 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()}"
} }
@Test @Test

View File

@ -26,8 +26,10 @@ class SubsonicApiCreatePlaylistTest : SubsonicAPIClientTest() {
fun `Should pass id param in request`() { fun `Should pass id param in request`() {
val id = "56" val id = "56"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "playlistId=$id") { responseResourceName = "ping_ok.json",
expectedParam = "playlistId=$id"
) {
client.api.createPlaylist(id = id).execute() client.api.createPlaylist(id = id).execute()
} }
} }
@ -36,8 +38,10 @@ class SubsonicApiCreatePlaylistTest : SubsonicAPIClientTest() {
fun `Should pass name param in request`() { fun `Should pass name param in request`() {
val name = "some-name" val name = "some-name"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "name=$name") { responseResourceName = "ping_ok.json",
expectedParam = "name=$name"
) {
client.api.createPlaylist(name = name).execute() client.api.createPlaylist(name = name).execute()
} }
} }
@ -46,8 +50,10 @@ class SubsonicApiCreatePlaylistTest : SubsonicAPIClientTest() {
fun `Should pass song id param in request`() { fun `Should pass song id param in request`() {
val songId = listOf("4410", "852") val songId = listOf("4410", "852")
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "songId=${songId[0]}&songId=${songId[1]}") { responseResourceName = "ping_ok.json",
expectedParam = "songId=${songId[0]}&songId=${songId[1]}"
) {
client.api.createPlaylist(songIds = songId).execute() client.api.createPlaylist(songIds = songId).execute()
} }
} }

View File

@ -1,10 +1,10 @@
package org.moire.ultrasonic.api.subsonic package org.moire.ultrasonic.api.subsonic
import java.util.Calendar
import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.`should equal` import org.amshove.kluent.`should equal`
import org.junit.Test import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
import java.util.Calendar
/** /**
* Instrumentation test for [SubsonicAPIDefinition.createShare] call. * Instrumentation test for [SubsonicAPIDefinition.createShare] call.
@ -30,9 +30,9 @@ class SubsonicApiCreateShareTest : SubsonicAPIClientTest() {
with(response.body()!!.shares[0]) { with(response.body()!!.shares[0]) {
id `should be equal to` "0" id `should be equal to` "0"
url `should be equal to` "https://subsonic.com/ext/share/awdwo?jwt=" + url `should be equal to` "https://subsonic.com/ext/share/awdwo?jwt=" +
"eyJhbGciOiJIUzI1NiJ9." + "eyJhbGciOiJIUzI1NiJ9." +
"eyJwYXRoIjoiL2V4dC9zaGFyZS9hd2R3byIsImV4cCI6MTU0MTYyNjQzMX0.iy8dkt_ZZc8hJ692" + "eyJwYXRoIjoiL2V4dC9zaGFyZS9hd2R3byIsImV4cCI6MTU0MTYyNjQzMX0.iy8dkt_ZZc8hJ692" +
"UxorHdHWFU2RB-fMCmCA4IJ_dTw" "UxorHdHWFU2RB-fMCmCA4IJ_dTw"
username `should be equal to` "admin" username `should be equal to` "admin"
created `should equal` parseDate("2017-11-07T21:33:51.748Z") created `should equal` parseDate("2017-11-07T21:33:51.748Z")
expires `should equal` parseDate("2018-11-07T21:33:51.748Z") expires `should equal` parseDate("2018-11-07T21:33:51.748Z")
@ -40,20 +40,25 @@ class SubsonicApiCreateShareTest : SubsonicAPIClientTest() {
description `should be equal to` "Awesome link!" description `should be equal to` "Awesome link!"
visitCount `should be equal to` 0 visitCount `should be equal to` 0
items.size `should be equal to` 1 items.size `should be equal to` 1
items[0] `should equal` MusicDirectoryChild(id = "4212", parent = "4186", isDir = false, items[0] `should equal` MusicDirectoryChild(
title = "Heaven Knows", album = "Going to Hell", artist = "The Pretty Reckless", id = "4212", parent = "4186", isDir = false,
track = 3, year = 2014, genre = "Hard Rock", coverArt = "4186", size = 9025090, title = "Heaven Knows", album = "Going to Hell", artist = "The Pretty Reckless",
contentType = "audio/mpeg", suffix = "mp3", duration = 225, bitRate = 320, track = 3, year = 2014, genre = "Hard Rock", coverArt = "4186", size = 9025090,
path = "The Pretty Reckless/Going to Hell/03 Heaven Knows.mp3", isVideo = false, contentType = "audio/mpeg", suffix = "mp3", duration = 225, bitRate = 320,
playCount = 2, discNumber = 1, created = parseDate("2016-10-23T21:30:40.000Z"), path = "The Pretty Reckless/Going to Hell/03 Heaven Knows.mp3", isVideo = false,
albumId = "388", artistId = "238", type = "music") playCount = 2, discNumber = 1,
created = parseDate("2016-10-23T21:30:40.000Z"),
albumId = "388", artistId = "238", type = "music"
)
} }
} }
@Test @Test
fun `Should pass ids in request param`() { fun `Should pass ids in request param`() {
val idsList = listOf("some-id1", "some-id2") val idsList = listOf("some-id1", "some-id2")
mockWebServerRule.assertRequestParam(expectedParam = "id=${idsList[0]}&id=${idsList[1]}") { mockWebServerRule.assertRequestParam(
expectedParam = "id=${idsList[0]}&id=${idsList[1]}"
) {
client.api.createShare(idsList).execute() client.api.createShare(idsList).execute()
} }
} }
@ -64,7 +69,7 @@ class SubsonicApiCreateShareTest : SubsonicAPIClientTest() {
mockWebServerRule.assertRequestParam(expectedParam = "description=$description") { mockWebServerRule.assertRequestParam(expectedParam = "description=$description") {
client.api.createShare(idsToShare = listOf("id1", "id2"), description = description) client.api.createShare(idsToShare = listOf("id1", "id2"), description = description)
.execute() .execute()
} }
} }

View File

@ -26,8 +26,10 @@ class SubsonicApiDeletePlaylistTest : SubsonicAPIClientTest() {
fun `Should pass id param in request`() { fun `Should pass id param in request`() {
val id = "534" val id = "534"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "id=$id") { responseResourceName = "ping_ok.json",
expectedParam = "id=$id"
) {
client.api.deletePlaylist(id).execute() client.api.deletePlaylist(id).execute()
} }
} }

View File

@ -1,5 +1,6 @@
package org.moire.ultrasonic.api.subsonic package org.moire.ultrasonic.api.subsonic
import java.io.IOException
import org.amshove.kluent.`should equal` import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be` import org.amshove.kluent.`should not be`
import org.amshove.kluent.`should throw` import org.amshove.kluent.`should throw`
@ -15,7 +16,6 @@ import org.moire.ultrasonic.api.subsonic.SubsonicError.UserNotAuthorizedForOpera
import org.moire.ultrasonic.api.subsonic.SubsonicError.WrongUsernameOrPassword import org.moire.ultrasonic.api.subsonic.SubsonicError.WrongUsernameOrPassword
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import retrofit2.Response import retrofit2.Response
import java.io.IOException
/** /**
* Integration test that checks validity of api errors parsing. * Integration test that checks validity of api errors parsing.
@ -150,8 +150,8 @@ class SubsonicApiErrorsTest : SubsonicAPIClientTest() {
} }
private fun Response<SubsonicResponse>.assertError(expectedError: SubsonicError) = private fun Response<SubsonicResponse>.assertError(expectedError: SubsonicError) =
with(body()!!) { with(body()!!) {
error `should not be` null error `should not be` null
error `should equal` expectedError error `should equal` expectedError
} }
} }

View File

@ -30,14 +30,18 @@ class SubsonicApiGetAlbumList2Test : SubsonicAPIClientTest() {
assertResponseSuccessful(response) assertResponseSuccessful(response)
with(response.body()!!.albumList) { with(response.body()!!.albumList) {
this.size `should be equal to` 2 this.size `should be equal to` 2
this[0] `should equal` Album(id = "962", name = "Fury", artist = "Sick Puppies", this[0] `should equal` Album(
artistId = "473", coverArt = "al-962", songCount = 13, duration = 2591, id = "962", name = "Fury", artist = "Sick Puppies",
created = parseDate("2017-09-02T17:34:51.000Z"), year = 2016, artistId = "473", coverArt = "al-962", songCount = 13, duration = 2591,
genre = "Alternative Rock") created = parseDate("2017-09-02T17:34:51.000Z"), year = 2016,
this[1] `should equal` Album(id = "961", name = "Endless Forms Most Beautiful", genre = "Alternative Rock"
artist = "Nightwish", artistId = "559", coverArt = "al-961", songCount = 22, )
duration = 9469, created = parseDate("2017-09-02T16:22:47.000Z"), this[1] `should equal` Album(
year = 2015, genre = "Symphonic Metal") id = "961", name = "Endless Forms Most Beautiful",
artist = "Nightwish", artistId = "559", coverArt = "al-961", songCount = 22,
duration = 9469, created = parseDate("2017-09-02T16:22:47.000Z"),
year = 2015, genre = "Symphonic Metal"
)
} }
} }
@ -45,8 +49,10 @@ class SubsonicApiGetAlbumList2Test : SubsonicAPIClientTest() {
fun `Should pass type in request params`() { fun `Should pass type in request params`() {
val type = AlbumListType.SORTED_BY_NAME val type = AlbumListType.SORTED_BY_NAME
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "type=${type.typeName}") { responseResourceName = "get_album_list_2_ok.json",
expectedParam = "type=${type.typeName}"
) {
client.api.getAlbumList2(type = type).execute() client.api.getAlbumList2(type = type).execute()
} }
} }
@ -55,8 +61,10 @@ class SubsonicApiGetAlbumList2Test : SubsonicAPIClientTest() {
fun `Should pass size in request param`() { fun `Should pass size in request param`() {
val size = 45 val size = 45
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "size=$size") { responseResourceName = "get_album_list_2_ok.json",
expectedParam = "size=$size"
) {
client.api.getAlbumList2(STARRED, size = size).execute() client.api.getAlbumList2(STARRED, size = size).execute()
} }
} }
@ -65,8 +73,10 @@ class SubsonicApiGetAlbumList2Test : SubsonicAPIClientTest() {
fun `Should pass offset in request param`() { fun `Should pass offset in request param`() {
val offset = 33 val offset = 33
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "offset=$offset") { responseResourceName = "get_album_list_2_ok.json",
expectedParam = "offset=$offset"
) {
client.api.getAlbumList2(STARRED, offset = offset).execute() client.api.getAlbumList2(STARRED, offset = offset).execute()
} }
} }
@ -75,8 +85,10 @@ class SubsonicApiGetAlbumList2Test : SubsonicAPIClientTest() {
fun `Should pass from year in request params`() { fun `Should pass from year in request params`() {
val fromYear = 3030 val fromYear = 3030
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "fromYear=$fromYear") { responseResourceName = "get_album_list_2_ok.json",
expectedParam = "fromYear=$fromYear"
) {
client.api.getAlbumList2(STARRED, fromYear = fromYear).execute() client.api.getAlbumList2(STARRED, fromYear = fromYear).execute()
} }
} }
@ -85,8 +97,10 @@ class SubsonicApiGetAlbumList2Test : SubsonicAPIClientTest() {
fun `Should pass toYear in request param`() { fun `Should pass toYear in request param`() {
val toYear = 2014 val toYear = 2014
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "toYear=$toYear") { responseResourceName = "get_album_list_2_ok.json",
expectedParam = "toYear=$toYear"
) {
client.api.getAlbumList2(STARRED, toYear = toYear).execute() client.api.getAlbumList2(STARRED, toYear = toYear).execute()
} }
} }
@ -95,8 +109,10 @@ class SubsonicApiGetAlbumList2Test : SubsonicAPIClientTest() {
fun `Should pass genre in request param`() { fun `Should pass genre in request param`() {
val genre = "MathRock" val genre = "MathRock"
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "genre=$genre") { responseResourceName = "get_album_list_2_ok.json",
expectedParam = "genre=$genre"
) {
client.api.getAlbumList2(STARRED, genre = genre).execute() client.api.getAlbumList2(STARRED, genre = genre).execute()
} }
} }
@ -105,8 +121,10 @@ class SubsonicApiGetAlbumList2Test : SubsonicAPIClientTest() {
fun `Should pass music folder id in request param`() { fun `Should pass music folder id in request param`() {
val musicFolderId = "9422" val musicFolderId = "9422"
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "musicFolderId=$musicFolderId") { responseResourceName = "get_album_list_2_ok.json",
expectedParam = "musicFolderId=$musicFolderId"
) {
client.api.getAlbumList2(STARRED, musicFolderId = musicFolderId).execute() client.api.getAlbumList2(STARRED, musicFolderId = musicFolderId).execute()
} }
} }

View File

@ -29,11 +29,13 @@ class SubsonicApiGetAlbumListRequestTest : SubsonicAPIClientTest() {
assertResponseSuccessful(response) assertResponseSuccessful(response)
with(response.body()!!.albumList) { with(response.body()!!.albumList) {
size `should be equal to` 2 size `should be equal to` 2
this[1] `should equal` MusicDirectoryChild(id = "9997", parent = "9996", isDir = true, this[1] `should equal` MusicDirectoryChild(
title = "Endless Forms Most Beautiful", album = "Endless Forms Most Beautiful", id = "9997", parent = "9996", isDir = true,
artist = "Nightwish", year = 2015, genre = "Symphonic Metal", title = "Endless Forms Most Beautiful", album = "Endless Forms Most Beautiful",
coverArt = "9997", playCount = 11, artist = "Nightwish", year = 2015, genre = "Symphonic Metal",
created = parseDate("2017-09-02T16:22:49.000Z")) coverArt = "9997", playCount = 11,
created = parseDate("2017-09-02T16:22:49.000Z")
)
} }
} }
@ -41,8 +43,10 @@ class SubsonicApiGetAlbumListRequestTest : SubsonicAPIClientTest() {
fun `Should pass type in request params`() { fun `Should pass type in request params`() {
val listType = AlbumListType.HIGHEST val listType = AlbumListType.HIGHEST
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "type=${listType.typeName}") { responseResourceName = "get_album_list_ok.json",
expectedParam = "type=${listType.typeName}"
) {
client.api.getAlbumList(type = listType).execute() client.api.getAlbumList(type = listType).execute()
} }
} }
@ -51,8 +55,10 @@ class SubsonicApiGetAlbumListRequestTest : SubsonicAPIClientTest() {
fun `Should pass size in request params`() { fun `Should pass size in request params`() {
val size = 45 val size = 45
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "size=$size") { responseResourceName = "get_album_list_ok.json",
expectedParam = "size=$size"
) {
client.api.getAlbumList(type = BY_GENRE, size = size).execute() client.api.getAlbumList(type = BY_GENRE, size = size).execute()
} }
} }
@ -61,8 +67,10 @@ class SubsonicApiGetAlbumListRequestTest : SubsonicAPIClientTest() {
fun `Should pass offset in request params`() { fun `Should pass offset in request params`() {
val offset = 3 val offset = 3
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "offset=$offset") { responseResourceName = "get_album_list_ok.json",
expectedParam = "offset=$offset"
) {
client.api.getAlbumList(type = BY_GENRE, offset = offset).execute() client.api.getAlbumList(type = BY_GENRE, offset = offset).execute()
} }
} }
@ -71,8 +79,10 @@ class SubsonicApiGetAlbumListRequestTest : SubsonicAPIClientTest() {
fun `Should pass from year in request params`() { fun `Should pass from year in request params`() {
val fromYear = 2001 val fromYear = 2001
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "fromYear=$fromYear") { responseResourceName = "get_album_list_ok.json",
expectedParam = "fromYear=$fromYear"
) {
client.api.getAlbumList(type = BY_GENRE, fromYear = fromYear).execute() client.api.getAlbumList(type = BY_GENRE, fromYear = fromYear).execute()
} }
} }
@ -81,8 +91,10 @@ class SubsonicApiGetAlbumListRequestTest : SubsonicAPIClientTest() {
fun `Should pass to year in request params`() { fun `Should pass to year in request params`() {
val toYear = 2017 val toYear = 2017
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "toYear=$toYear") { responseResourceName = "get_album_list_ok.json",
expectedParam = "toYear=$toYear"
) {
client.api.getAlbumList(type = BY_GENRE, toYear = toYear).execute() client.api.getAlbumList(type = BY_GENRE, toYear = toYear).execute()
} }
} }
@ -91,8 +103,10 @@ class SubsonicApiGetAlbumListRequestTest : SubsonicAPIClientTest() {
fun `Should pass genre in request params`() { fun `Should pass genre in request params`() {
val genre = "Rock" val genre = "Rock"
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "genre=$genre") { responseResourceName = "get_album_list_ok.json",
expectedParam = "genre=$genre"
) {
client.api.getAlbumList(type = BY_GENRE, genre = genre).execute() client.api.getAlbumList(type = BY_GENRE, genre = genre).execute()
} }
} }
@ -101,8 +115,10 @@ class SubsonicApiGetAlbumListRequestTest : SubsonicAPIClientTest() {
fun `Should pass music folder id in request params`() { fun `Should pass music folder id in request params`() {
val folderId = "545" val folderId = "545"
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "musicFolderId=$folderId") { responseResourceName = "get_album_list_ok.json",
expectedParam = "musicFolderId=$folderId"
) {
client.api.getAlbumList(type = BY_GENRE, musicFolderId = folderId).execute() client.api.getAlbumList(type = BY_GENRE, musicFolderId = folderId).execute()
} }
} }

View File

@ -25,8 +25,10 @@ class SubsonicApiGetAlbumTest : SubsonicAPIClientTest() {
fun `Should add id to request params`() { fun `Should add id to request params`() {
val id = "76" val id = "76"
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "id=$id") { responseResourceName = "get_album_ok.json",
expectedParam = "id=$id"
) {
client.api.getAlbum(id).execute() client.api.getAlbum(id).execute()
} }
} }
@ -50,22 +52,26 @@ class SubsonicApiGetAlbumTest : SubsonicAPIClientTest() {
year `should be equal to` 2008 year `should be equal to` 2008
genre `should be equal to` "Hard Rock" genre `should be equal to` "Hard Rock"
songList.size `should be equal to` 15 songList.size `should be equal to` 15
songList[0] `should equal` MusicDirectoryChild(id = "6491", parent = "6475", songList[0] `should equal` MusicDirectoryChild(
isDir = false, title = "Rock 'n' Roll Train", album = "Black Ice", id = "6491", parent = "6475",
artist = "AC/DC", track = 1, year = 2008, genre = "Hard Rock", isDir = false, title = "Rock 'n' Roll Train", album = "Black Ice",
coverArt = "6475", size = 7205451, contentType = "audio/mpeg", suffix = "mp3", artist = "AC/DC", track = 1, year = 2008, genre = "Hard Rock",
duration = 261, bitRate = 219, coverArt = "6475", size = 7205451, contentType = "audio/mpeg", suffix = "mp3",
path = "AC_DC/Black Ice/01 Rock 'n' Roll Train.mp3", duration = 261, bitRate = 219,
isVideo = false, playCount = 0, discNumber = 1, path = "AC_DC/Black Ice/01 Rock 'n' Roll Train.mp3",
created = parseDate("2016-10-23T15:31:20.000Z"), isVideo = false, playCount = 0, discNumber = 1,
albumId = "618", artistId = "362", type = "music") created = parseDate("2016-10-23T15:31:20.000Z"),
songList[5] `should equal` MusicDirectoryChild(id = "6492", parent = "6475", albumId = "618", artistId = "362", type = "music"
isDir = false, title = "Smash 'n' Grab", album = "Black Ice", artist = "AC/DC", )
track = 6, year = 2008, genre = "Hard Rock", coverArt = "6475", size = 6697204, songList[5] `should equal` MusicDirectoryChild(
contentType = "audio/mpeg", suffix = "mp3", duration = 246, bitRate = 216, id = "6492", parent = "6475",
path = "AC_DC/Black Ice/06 Smash 'n' Grab.mp3", isVideo = false, playCount = 0, isDir = false, title = "Smash 'n' Grab", album = "Black Ice", artist = "AC/DC",
discNumber = 1, created = parseDate("2016-10-23T15:31:20.000Z"), track = 6, year = 2008, genre = "Hard Rock", coverArt = "6475", size = 6697204,
albumId = "618", artistId = "362", type = "music") contentType = "audio/mpeg", suffix = "mp3", duration = 246, bitRate = 216,
path = "AC_DC/Black Ice/06 Smash 'n' Grab.mp3", isVideo = false, playCount = 0,
discNumber = 1, created = parseDate("2016-10-23T15:31:20.000Z"),
albumId = "618", artistId = "362", type = "music"
)
} }
} }
} }

View File

@ -25,8 +25,10 @@ class SubsonicApiGetArtistTest : SubsonicAPIClientTest() {
fun `Should pass id param in request`() { fun `Should pass id param in request`() {
val id = "929" val id = "929"
mockWebServerRule.assertRequestParam(responseResourceName = "get_artist_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "id=$id") { responseResourceName = "get_artist_ok.json",
expectedParam = "id=$id"
) {
client.api.getArtist(id).execute() client.api.getArtist(id).execute()
} }
} }
@ -44,14 +46,18 @@ class SubsonicApiGetArtistTest : SubsonicAPIClientTest() {
coverArt `should be equal to` "ar-362" coverArt `should be equal to` "ar-362"
albumCount `should be equal to` 2 albumCount `should be equal to` 2
albumsList.size `should be equal to` 2 albumsList.size `should be equal to` 2
albumsList[0] `should equal` Album(id = "618", name = "Black Ice", artist = "AC/DC", albumsList[0] `should equal` Album(
artistId = "362", coverArt = "al-618", songCount = 15, duration = 3331, id = "618", name = "Black Ice", artist = "AC/DC",
created = parseDate("2016-10-23T15:31:22.000Z"), artistId = "362", coverArt = "al-618", songCount = 15, duration = 3331,
year = 2008, genre = "Hard Rock") created = parseDate("2016-10-23T15:31:22.000Z"),
albumsList[1] `should equal` Album(id = "617", name = "Rock or Bust", artist = "AC/DC", year = 2008, genre = "Hard Rock"
artistId = "362", coverArt = "al-617", songCount = 11, duration = 2095, )
created = parseDate("2016-10-23T15:31:23.000Z"), albumsList[1] `should equal` Album(
year = 2014, genre = "Hard Rock") id = "617", name = "Rock or Bust", artist = "AC/DC",
artistId = "362", coverArt = "al-617", songCount = 11, duration = 2095,
created = parseDate("2016-10-23T15:31:23.000Z"),
year = 2014, genre = "Hard Rock"
)
} }
} }
} }

View File

@ -35,17 +35,26 @@ class SubsonicApiGetArtistsTest : SubsonicAPIClientTest() {
shortcutList `should equal` emptyList() shortcutList `should equal` emptyList()
indexList.size `should be equal to` 2 indexList.size `should be equal to` 2
indexList `should equal` listOf( indexList `should equal` listOf(
Index(name = "A", artists = listOf( Index(
Artist(id = "362", name = "AC/DC", coverArt = "ar-362", albumCount = 2), name = "A",
Artist(id = "254", name = "Acceptance", coverArt = "ar-254", artists = listOf(
albumCount = 1) Artist(id = "362", name = "AC/DC", coverArt = "ar-362", albumCount = 2),
)), Artist(id = "254", name = "Acceptance", coverArt = "ar-254", albumCount = 1)
Index(name = "T", artists = listOf( )
Artist(id = "516", name = "Tangerine Dream", coverArt = "ar-516", ),
albumCount = 1), Index(
Artist(id = "242", name = "Taproot", coverArt = "ar-242", name = "T",
albumCount = 2) artists = listOf(
)) Artist(
id = "516", name = "Tangerine Dream", coverArt = "ar-516",
albumCount = 1
),
Artist(
id = "242", name = "Taproot", coverArt = "ar-242",
albumCount = 2
)
)
)
) )
} }
} }
@ -55,8 +64,10 @@ class SubsonicApiGetArtistsTest : SubsonicAPIClientTest() {
mockWebServerRule.enqueueResponse("get_artists_ok.json") mockWebServerRule.enqueueResponse("get_artists_ok.json")
val musicFolderId = "101" val musicFolderId = "101"
mockWebServerRule.assertRequestParam(responseResourceName = "get_artists_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "musicFolderId=$musicFolderId") { responseResourceName = "get_artists_ok.json",
expectedParam = "musicFolderId=$musicFolderId"
) {
client.api.getArtists(musicFolderId).execute() client.api.getArtists(musicFolderId).execute()
} }
} }

View File

@ -40,8 +40,10 @@ class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() {
@Test @Test
fun `Should return successful call stream`() { fun `Should return successful call stream`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse() mockWebServerRule.mockWebServer.enqueue(
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))) MockResponse()
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))
)
val response = client.stream("some") val response = client.stream("some")

View File

@ -32,15 +32,17 @@ class SubsonicApiGetBookmarksTest : SubsonicAPIClientTest() {
comment `should be equal to` "Look at this" comment `should be equal to` "Look at this"
created `should equal` parseDate("2017-11-18T15:22:22.144Z") created `should equal` parseDate("2017-11-18T15:22:22.144Z")
changed `should equal` parseDate("2017-11-18T15:22:22.144Z") changed `should equal` parseDate("2017-11-18T15:22:22.144Z")
entry `should equal` MusicDirectoryChild(id = "10349", parent = "10342", entry `should equal` MusicDirectoryChild(
isDir = false, title = "Amerika", album = "Home of the Strange", id = "10349", parent = "10342",
artist = "Young the Giant", track = 1, year = 2016, genre = "Indie Rock", isDir = false, title = "Amerika", album = "Home of the Strange",
coverArt = "10342", size = 9628673, contentType = "audio/mpeg", artist = "Young the Giant", track = 1, year = 2016, genre = "Indie Rock",
suffix = "mp3", duration = 240, bitRate = 320, coverArt = "10342", size = 9628673, contentType = "audio/mpeg",
path = "Young the Giant/Home of the Strange/01 Amerika.mp3", suffix = "mp3", duration = 240, bitRate = 320,
isVideo = false, playCount = 2, discNumber = 1, path = "Young the Giant/Home of the Strange/01 Amerika.mp3",
created = parseDate("2017-11-01T17:46:52.000Z"), isVideo = false, playCount = 2, discNumber = 1,
albumId = "984", artistId = "571", type = "music") created = parseDate("2017-11-01T17:46:52.000Z"),
albumId = "984", artistId = "571", type = "music"
)
} }
} }
} }

View File

@ -27,10 +27,14 @@ class SubsonicApiGetChatMessagesTest : SubsonicAPIClientTest() {
assertResponseSuccessful(response) assertResponseSuccessful(response)
with(response.body()!!.chatMessages) { with(response.body()!!.chatMessages) {
size `should be equal to` 2 size `should be equal to` 2
this[0] `should equal` ChatMessage(username = "sindre", time = 1269771845310, this[0] `should equal` ChatMessage(
message = "Sindre was here") username = "sindre", time = 1269771845310,
this[1] `should equal` ChatMessage(username = "ben", time = 1269771842504, message = "Sindre was here"
message = "Ben too") )
this[1] `should equal` ChatMessage(
username = "ben", time = 1269771842504,
message = "Ben too"
)
} }
} }

View File

@ -40,8 +40,10 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() {
@Test @Test
fun `Should return successful call stream`() { fun `Should return successful call stream`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse() mockWebServerRule.mockWebServer.enqueue(
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))) MockResponse()
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))
)
val response = client.getCoverArt("some-id") val response = client.getCoverArt("some-id")

View File

@ -23,19 +23,27 @@ class SubsonicApiGetIndexesTest : SubsonicAPIClientTest() {
lastModified `should equal` 1491069027523 lastModified `should equal` 1491069027523
ignoredArticles `should equal` "The El La Los Las Le Les" ignoredArticles `should equal` "The El La Los Las Le Les"
shortcutList `should equal` listOf( shortcutList `should equal` listOf(
Artist(id = "889", name = "podcasts"), Artist(id = "889", name = "podcasts"),
Artist(id = "890", name = "audiobooks") Artist(id = "890", name = "audiobooks")
) )
indexList `should equal` mutableListOf( indexList `should equal` mutableListOf(
Index("A", listOf( Index(
Artist(id = "50", name = "Ace Of Base", "A",
starred = parseDate("2017-04-02T20:16:29.815Z")), listOf(
Artist(id = "379", name = "A Perfect Circle") Artist(
)), id = "50", name = "Ace Of Base",
Index("H", listOf( starred = parseDate("2017-04-02T20:16:29.815Z")
Artist(id = "299", name = "Haddaway"), ),
Artist(id = "297", name = "Halestorm") Artist(id = "379", name = "A Perfect Circle")
)) )
),
Index(
"H",
listOf(
Artist(id = "299", name = "Haddaway"),
Artist(id = "297", name = "Halestorm")
)
)
) )
} }
} }
@ -44,8 +52,10 @@ class SubsonicApiGetIndexesTest : SubsonicAPIClientTest() {
fun `Should add music folder id as a query param for getIndexes api call`() { fun `Should add music folder id as a query param for getIndexes api call`() {
val musicFolderId = "9" val musicFolderId = "9"
mockWebServerRule.assertRequestParam(responseResourceName = "get_indexes_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "musicFolderId=$musicFolderId") { responseResourceName = "get_indexes_ok.json",
expectedParam = "musicFolderId=$musicFolderId"
) {
client.api.getIndexes(musicFolderId, null).execute() client.api.getIndexes(musicFolderId, null).execute()
} }
} }
@ -54,8 +64,10 @@ class SubsonicApiGetIndexesTest : SubsonicAPIClientTest() {
fun `Should add ifModifiedSince as a query param for getIndexes api call`() { fun `Should add ifModifiedSince as a query param for getIndexes api call`() {
val ifModifiedSince = System.currentTimeMillis() val ifModifiedSince = System.currentTimeMillis()
mockWebServerRule.assertRequestParam(responseResourceName = "get_indexes_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "ifModifiedSince=$ifModifiedSince") { responseResourceName = "get_indexes_ok.json",
expectedParam = "ifModifiedSince=$ifModifiedSince"
) {
client.api.getIndexes(null, ifModifiedSince).execute() client.api.getIndexes(null, ifModifiedSince).execute()
} }
} }

View File

@ -18,10 +18,12 @@ class SubsonicApiGetLicenseTest : SubsonicAPIClientTest() {
assertResponseSuccessful(response) assertResponseSuccessful(response)
with(response.body()!!) { with(response.body()!!) {
assertBaseResponseOk() assertBaseResponseOk()
license `should equal` License(valid = true, license `should equal` License(
trialExpires = parseDate("2016-11-23T20:17:15.206Z"), valid = true,
email = "someone@example.net", trialExpires = parseDate("2016-11-23T20:17:15.206Z"),
licenseExpires = parseDate("8994-08-17T07:12:55.807Z")) email = "someone@example.net",
licenseExpires = parseDate("8994-08-17T07:12:55.807Z")
)
} }
} }

View File

@ -25,7 +25,7 @@ class SubsonicApiGetLyricsTest : SubsonicAPIClientTest() {
artist `should be equal to` "Amorphis" artist `should be equal to` "Amorphis"
title `should be equal to` "Alone" title `should be equal to` "Alone"
text `should be equal to` "Tear dimmed rememberance\nIn a womb of time\nBreath upon " + text `should be equal to` "Tear dimmed rememberance\nIn a womb of time\nBreath upon " +
"me\nPossessed by the" "me\nPossessed by the"
} }
} }
@ -33,8 +33,10 @@ class SubsonicApiGetLyricsTest : SubsonicAPIClientTest() {
fun `Should pass artist param in request`() { fun `Should pass artist param in request`() {
val artist = "some-artist" val artist = "some-artist"
mockWebServerRule.assertRequestParam(responseResourceName = "get_lyrics_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "artist=$artist") { responseResourceName = "get_lyrics_ok.json",
expectedParam = "artist=$artist"
) {
client.api.getLyrics(artist = artist).execute() client.api.getLyrics(artist = artist).execute()
} }
} }
@ -43,8 +45,10 @@ class SubsonicApiGetLyricsTest : SubsonicAPIClientTest() {
fun `Should pass title param in request`() { fun `Should pass title param in request`() {
val title = "some-title" val title = "some-title"
mockWebServerRule.assertRequestParam(responseResourceName = "get_lyrics_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "title=$title") { responseResourceName = "get_lyrics_ok.json",
expectedParam = "title=$title"
) {
client.api.getLyrics(title = title).execute() client.api.getLyrics(title = title).execute()
} }
} }

View File

@ -26,8 +26,10 @@ class SubsonicApiGetMusicDirectoryTest : SubsonicAPIClientTest() {
fun `GetMusicDirectory should add directory id to query params`() { fun `GetMusicDirectory should add directory id to query params`() {
val directoryId = "124" val directoryId = "124"
mockWebServerRule.assertRequestParam(responseResourceName = "get_music_directory_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "id=$directoryId") { responseResourceName = "get_music_directory_ok.json",
expectedParam = "id=$directoryId"
) {
client.api.getMusicDirectory(directoryId).execute() client.api.getMusicDirectory(directoryId).execute()
} }
} }
@ -50,22 +52,26 @@ class SubsonicApiGetMusicDirectoryTest : SubsonicAPIClientTest() {
starred `should equal` null starred `should equal` null
playCount `should be equal to` 1 playCount `should be equal to` 1
childList.size `should be` 2 childList.size `should be` 2
childList[0] `should equal` MusicDirectoryChild(id = "4844", parent = "4836", childList[0] `should equal` MusicDirectoryChild(
isDir = false, title = "Crash", album = "12 Stones", artist = "12 Stones", id = "4844", parent = "4836",
track = 1, year = 2002, genre = "Alternative Rock", coverArt = "4836", isDir = false, title = "Crash", album = "12 Stones", artist = "12 Stones",
size = 5348318L, contentType = "audio/mpeg", suffix = "mp3", duration = 222, track = 1, year = 2002, genre = "Alternative Rock", coverArt = "4836",
bitRate = 192, path = "12 Stones/12 Stones/01 Crash.mp3", isVideo = false, size = 5348318L, contentType = "audio/mpeg", suffix = "mp3", duration = 222,
playCount = 0, discNumber = 1, bitRate = 192, path = "12 Stones/12 Stones/01 Crash.mp3", isVideo = false,
created = parseDate("2016-10-23T15:19:10.000Z"), playCount = 0, discNumber = 1,
albumId = "454", artistId = "288", type = "music") created = parseDate("2016-10-23T15:19:10.000Z"),
childList[1] `should equal` MusicDirectoryChild(id = "4845", parent = "4836", albumId = "454", artistId = "288", type = "music"
isDir = false, title = "Broken", album = "12 Stones", artist = "12 Stones", )
track = 2, year = 2002, genre = "Alternative Rock", coverArt = "4836", childList[1] `should equal` MusicDirectoryChild(
size = 4309043L, contentType = "audio/mpeg", suffix = "mp3", duration = 179, id = "4845", parent = "4836",
bitRate = 192, path = "12 Stones/12 Stones/02 Broken.mp3", isVideo = false, isDir = false, title = "Broken", album = "12 Stones", artist = "12 Stones",
playCount = 0, discNumber = 1, track = 2, year = 2002, genre = "Alternative Rock", coverArt = "4836",
created = parseDate("2016-10-23T15:19:09.000Z"), size = 4309043L, contentType = "audio/mpeg", suffix = "mp3", duration = 179,
albumId = "454", artistId = "288", type = "music") bitRate = 192, path = "12 Stones/12 Stones/02 Broken.mp3", isVideo = false,
playCount = 0, discNumber = 1,
created = parseDate("2016-10-23T15:19:09.000Z"),
albumId = "454", artistId = "288", type = "music"
)
} }
} }
} }

View File

@ -18,8 +18,9 @@ class SubsonicApiGetMusicFoldersTest : SubsonicAPIClientTest() {
with(response.body()!!) { with(response.body()!!) {
assertBaseResponseOk() assertBaseResponseOk()
musicFolders `should equal` listOf( musicFolders `should equal` listOf(
MusicFolder("0", "Music"), MusicFolder("0", "Music"),
MusicFolder("2", "Test")) MusicFolder("2", "Test")
)
} }
} }

View File

@ -39,15 +39,17 @@ class SubsonicApiGetPlaylistTest : SubsonicAPIClientTest() {
changed `should equal` parseDate("2017-08-27T11:17:26.218Z") changed `should equal` parseDate("2017-08-27T11:17:26.218Z")
coverArt `should be equal to` "pl-0" coverArt `should be equal to` "pl-0"
entriesList.size `should be equal to` 2 entriesList.size `should be equal to` 2
entriesList[1] `should equal` MusicDirectoryChild(id = "4215", parent = "4186", entriesList[1] `should equal` MusicDirectoryChild(
isDir = false, title = "Going to Hell", album = "Going to Hell", id = "4215", parent = "4186",
artist = "The Pretty Reckless", track = 2, year = 2014, isDir = false, title = "Going to Hell", album = "Going to Hell",
genre = "Hard Rock", coverArt = "4186", size = 11089627, artist = "The Pretty Reckless", track = 2, year = 2014,
contentType = "audio/mpeg", suffix = "mp3", duration = 277, bitRate = 320, genre = "Hard Rock", coverArt = "4186", size = 11089627,
path = "The Pretty Reckless/Going to Hell/02 Going to Hell.mp3", contentType = "audio/mpeg", suffix = "mp3", duration = 277, bitRate = 320,
isVideo = false, playCount = 0, discNumber = 1, path = "The Pretty Reckless/Going to Hell/02 Going to Hell.mp3",
created = parseDate("2016-10-23T21:30:41.000Z"), isVideo = false, playCount = 0, discNumber = 1,
albumId = "388", artistId = "238", type = "music") created = parseDate("2016-10-23T21:30:41.000Z"),
albumId = "388", artistId = "238", type = "music"
)
} }
} }
@ -55,8 +57,10 @@ class SubsonicApiGetPlaylistTest : SubsonicAPIClientTest() {
fun `Should pass id as request param`() { fun `Should pass id as request param`() {
val playlistId = "453" val playlistId = "453"
mockWebServerRule.assertRequestParam(responseResourceName = "get_playlist_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "id=$playlistId") { responseResourceName = "get_playlist_ok.json",
expectedParam = "id=$playlistId"
) {
client.api.getPlaylist(playlistId).execute() client.api.getPlaylist(playlistId).execute()
} }
} }

View File

@ -29,12 +29,14 @@ class SubsonicApiGetPlaylistsTest : SubsonicAPIClientTest() {
assertResponseSuccessful(response) assertResponseSuccessful(response)
with(response.body()!!.playlists) { with(response.body()!!.playlists) {
size `should be equal to` 1 size `should be equal to` 1
this[0] `should equal` Playlist(id = "0", name = "Aug 27, 2017 11:17 AM", this[0] `should equal` Playlist(
owner = "admin", public = false, songCount = 16, duration = 3573, id = "0", name = "Aug 27, 2017 11:17 AM",
comment = "Some comment", owner = "admin", public = false, songCount = 16, duration = 3573,
created = parseDate("2017-08-27T11:17:26.216Z"), comment = "Some comment",
changed = parseDate("2017-08-27T11:17:26.218Z"), created = parseDate("2017-08-27T11:17:26.216Z"),
coverArt = "pl-0") changed = parseDate("2017-08-27T11:17:26.218Z"),
coverArt = "pl-0"
)
} }
} }
@ -42,8 +44,10 @@ class SubsonicApiGetPlaylistsTest : SubsonicAPIClientTest() {
fun `Should pass username as a parameter`() { fun `Should pass username as a parameter`() {
val username = "SomeUsername" val username = "SomeUsername"
mockWebServerRule.assertRequestParam(responseResourceName = "get_playlists_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "username=$username") { responseResourceName = "get_playlists_ok.json",
expectedParam = "username=$username"
) {
client.api.getPlaylists(username = username).execute() client.api.getPlaylists(username = username).execute()
} }
} }

View File

@ -34,33 +34,35 @@ class SubsonicApiGetPodcastsTest : SubsonicAPIClientTest() {
url `should be equal to` "http://feeds.codenewbie.org/cnpodcast.xml" url `should be equal to` "http://feeds.codenewbie.org/cnpodcast.xml"
title `should be equal to` "CodeNewbie" title `should be equal to` "CodeNewbie"
description `should be equal to` "Stories and interviews from people on their coding " + description `should be equal to` "Stories and interviews from people on their coding " +
"journey." "journey."
coverArt `should be equal to` "pod-2" coverArt `should be equal to` "pod-2"
originalImageUrl `should be equal to` "http://codenewbie.blubrry.com/wp-content/" + originalImageUrl `should be equal to` "http://codenewbie.blubrry.com/wp-content/" +
"uploads/powerpress/220808.jpg" "uploads/powerpress/220808.jpg"
status `should be equal to` "completed" status `should be equal to` "completed"
errorMessage `should be equal to` "" errorMessage `should be equal to` ""
episodeList.size `should be equal to` 10 episodeList.size `should be equal to` 10
episodeList[0] `should equal` MusicDirectoryChild(id = "148", parent = "9959", episodeList[0] `should equal` MusicDirectoryChild(
isDir = false, id = "148", parent = "9959",
title = "S1:EP3 How to teach yourself computer science (Vaidehi Joshi)", isDir = false,
album = "CodeNewbie", artist = "podcasts", coverArt = "9959", title = "S1:EP3 How to teach yourself computer science (Vaidehi Joshi)",
size = 38274221, contentType = "audio/mpeg", suffix = "mp3", album = "CodeNewbie", artist = "podcasts", coverArt = "9959",
duration = 2397, bitRate = 128, isVideo = false, playCount = 0, size = 38274221, contentType = "audio/mpeg", suffix = "mp3",
created = parseDate("2017-08-30T09:33:39.000Z"), type = "podcast", duration = 2397, bitRate = 128, isVideo = false, playCount = 0,
streamId = "9982", channelId = "2", created = parseDate("2017-08-30T09:33:39.000Z"), type = "podcast",
description = "Vaidehi decided to take on a year-long challenge. " + streamId = "9982", channelId = "2",
"She'd pick a computer science topic every week, do tons of research " + description = "Vaidehi decided to take on a year-long challenge. " +
"and write a technical blog post explaining it in simple terms and " + "She'd pick a computer science topic every week, do tons of research " +
"beautiful illustrations. And then she actually did it. She tells us " + "and write a technical blog post explaining it in simple terms and " +
"about her project, basecs, how it's changed her as a developer, and " + "beautiful illustrations. And then she actually did it. She tells us " +
"how she handles the trolls and negativity from people who don't " + "about her project, basecs, how it's changed her as a developer, and " +
"appreciate her work. Show Notes Technical Writer position at " + "how she handles the trolls and negativity from people who don't " +
"CodeNewbie basecs 100 Days of Code Conway's Game of Life Hexes and " + "appreciate her work. Show Notes Technical Writer position at " +
"Other Magical Numbers (Vaidehi's blog post) Bits, Bytes, Building " + "CodeNewbie basecs 100 Days of Code Conway's Game of Life Hexes and " +
"With Binary (Vaidehi's blog post) Rust", "Other Magical Numbers (Vaidehi's blog post) Bits, Bytes, Building " +
status = "completed", "With Binary (Vaidehi's blog post) Rust",
publishDate = parseDate("2017-08-29T00:01:01.000Z")) status = "completed",
publishDate = parseDate("2017-08-29T00:01:01.000Z")
)
} }
} }
@ -68,8 +70,10 @@ class SubsonicApiGetPodcastsTest : SubsonicAPIClientTest() {
fun `Should pass include episodes in request`() { fun `Should pass include episodes in request`() {
val includeEpisodes = true val includeEpisodes = true
mockWebServerRule.assertRequestParam(responseResourceName = "get_podcasts_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "includeEpisodes=$includeEpisodes") { responseResourceName = "get_podcasts_ok.json",
expectedParam = "includeEpisodes=$includeEpisodes"
) {
client.api.getPodcasts(includeEpisodes = includeEpisodes).execute() client.api.getPodcasts(includeEpisodes = includeEpisodes).execute()
} }
} }
@ -78,8 +82,10 @@ class SubsonicApiGetPodcastsTest : SubsonicAPIClientTest() {
fun `Should pass id in request param`() { fun `Should pass id in request param`() {
val id = "249" val id = "249"
mockWebServerRule.assertRequestParam(responseResourceName = "get_podcasts_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "id=$id") { responseResourceName = "get_podcasts_ok.json",
expectedParam = "id=$id"
) {
client.api.getPodcasts(id = id).execute() client.api.getPodcasts(id = id).execute()
} }
} }

View File

@ -27,14 +27,16 @@ class SubsonicApiGetRandomSongsTest : SubsonicAPIClientTest() {
assertResponseSuccessful(response) assertResponseSuccessful(response)
with(response.body()!!.songsList) { with(response.body()!!.songsList) {
size `should be equal to` 3 size `should be equal to` 3
this[1] `should equal` MusicDirectoryChild(id = "3061", parent = "3050", isDir = false, this[1] `should equal` MusicDirectoryChild(
title = "Sure as Hell", album = "Who Are You Now?", artist = "This Providence", id = "3061", parent = "3050", isDir = false,
track = 1, year = 2009, genre = "Indie Rock", coverArt = "3050", title = "Sure as Hell", album = "Who Are You Now?", artist = "This Providence",
size = 1969692, contentType = "audio/mpeg", suffix = "mp3", duration = 110, track = 1, year = 2009, genre = "Indie Rock", coverArt = "3050",
bitRate = 142, path = "This Providence/Who Are You Now_/01 Sure as Hell.mp3", size = 1969692, contentType = "audio/mpeg", suffix = "mp3", duration = 110,
isVideo = false, playCount = 0, discNumber = 1, bitRate = 142, path = "This Providence/Who Are You Now_/01 Sure as Hell.mp3",
created = parseDate("2016-10-23T21:32:46.000Z"), albumId = "272", isVideo = false, playCount = 0, discNumber = 1,
artistId = "152", type = "music") created = parseDate("2016-10-23T21:32:46.000Z"), albumId = "272",
artistId = "152", type = "music"
)
} }
} }
@ -42,8 +44,10 @@ class SubsonicApiGetRandomSongsTest : SubsonicAPIClientTest() {
fun `Should pass size in request param`() { fun `Should pass size in request param`() {
val size = 384433 val size = 384433
mockWebServerRule.assertRequestParam(responseResourceName = "get_random_songs_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "size=$size") { responseResourceName = "get_random_songs_ok.json",
expectedParam = "size=$size"
) {
client.api.getRandomSongs(size = size).execute() client.api.getRandomSongs(size = size).execute()
} }
} }
@ -52,8 +56,10 @@ class SubsonicApiGetRandomSongsTest : SubsonicAPIClientTest() {
fun `Should pass genre in request param`() { fun `Should pass genre in request param`() {
val genre = "PostRock" val genre = "PostRock"
mockWebServerRule.assertRequestParam(responseResourceName = "get_random_songs_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "genre=$genre") { responseResourceName = "get_random_songs_ok.json",
expectedParam = "genre=$genre"
) {
client.api.getRandomSongs(genre = genre).execute() client.api.getRandomSongs(genre = genre).execute()
} }
} }
@ -62,8 +68,10 @@ class SubsonicApiGetRandomSongsTest : SubsonicAPIClientTest() {
fun `Should pass from year in request param`() { fun `Should pass from year in request param`() {
val fromYear = 1919 val fromYear = 1919
mockWebServerRule.assertRequestParam(responseResourceName = "get_random_songs_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "fromYear=$fromYear") { responseResourceName = "get_random_songs_ok.json",
expectedParam = "fromYear=$fromYear"
) {
client.api.getRandomSongs(fromYear = fromYear).execute() client.api.getRandomSongs(fromYear = fromYear).execute()
} }
} }
@ -72,8 +80,10 @@ class SubsonicApiGetRandomSongsTest : SubsonicAPIClientTest() {
fun `Should pass to year in request params`() { fun `Should pass to year in request params`() {
val toYear = 2012 val toYear = 2012
mockWebServerRule.assertRequestParam(responseResourceName = "get_random_songs_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "toYear=$toYear") { responseResourceName = "get_random_songs_ok.json",
expectedParam = "toYear=$toYear"
) {
client.api.getRandomSongs(toYear = toYear).execute() client.api.getRandomSongs(toYear = toYear).execute()
} }
} }
@ -82,8 +92,10 @@ class SubsonicApiGetRandomSongsTest : SubsonicAPIClientTest() {
fun `Should pass music folder id in request param`() { fun `Should pass music folder id in request param`() {
val musicFolderId = "4919" val musicFolderId = "4919"
mockWebServerRule.assertRequestParam(responseResourceName = "get_random_songs_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "musicFolderId=$musicFolderId") { responseResourceName = "get_random_songs_ok.json",
expectedParam = "musicFolderId=$musicFolderId"
) {
client.api.getRandomSongs(musicFolderId = musicFolderId).execute() client.api.getRandomSongs(musicFolderId = musicFolderId).execute()
} }
} }

View File

@ -29,8 +29,8 @@ class SubsonicApiGetSharesTest : SubsonicAPIClientTest() {
with(response.body()!!.shares[0]) { with(response.body()!!.shares[0]) {
id `should be equal to` "0" id `should be equal to` "0"
url `should be equal to` "https://subsonic.com/ext/share/awdwo?jwt=eyJhbGciOiJIUzI1" + url `should be equal to` "https://subsonic.com/ext/share/awdwo?jwt=eyJhbGciOiJIUzI1" +
"NiJ9.eyJwYXRoIjoiL2V4dC9zaGFyZS9hd2R3byIsImV4cCI6MTU0MTYyNjQzMX0.iy8dkt_ZZc8" + "NiJ9.eyJwYXRoIjoiL2V4dC9zaGFyZS9hd2R3byIsImV4cCI6MTU0MTYyNjQzMX0.iy8dkt_ZZc8" +
"hJ692UxorHdHWFU2RB-fMCmCA4IJ_dTw" "hJ692UxorHdHWFU2RB-fMCmCA4IJ_dTw"
username `should be equal to` "admin" username `should be equal to` "admin"
created `should equal` parseDate("2017-11-07T21:33:51.748Z") created `should equal` parseDate("2017-11-07T21:33:51.748Z")
expires `should equal` parseDate("2018-11-07T21:33:51.748Z") expires `should equal` parseDate("2018-11-07T21:33:51.748Z")
@ -38,13 +38,15 @@ class SubsonicApiGetSharesTest : SubsonicAPIClientTest() {
visitCount `should be equal to` 0 visitCount `should be equal to` 0
description `should be equal to` "Awesome link!" description `should be equal to` "Awesome link!"
items.size `should be equal to` 1 items.size `should be equal to` 1
items[0] `should equal` MusicDirectoryChild(id = "4212", parent = "4186", isDir = false, items[0] `should equal` MusicDirectoryChild(
title = "Heaven Knows", album = "Going to Hell", artist = "The Pretty Reckless", id = "4212", parent = "4186", isDir = false,
track = 3, year = 2014, genre = "Hard Rock", coverArt = "4186", size = 9025090, title = "Heaven Knows", album = "Going to Hell", artist = "The Pretty Reckless",
contentType = "audio/mpeg", suffix = "mp3", duration = 225, bitRate = 320, track = 3, year = 2014, genre = "Hard Rock", coverArt = "4186", size = 9025090,
path = "The Pretty Reckless/Going to Hell/03 Heaven Knows.mp3", isVideo = false, contentType = "audio/mpeg", suffix = "mp3", duration = 225, bitRate = 320,
playCount = 2, discNumber = 1, created = parseDate("2016-10-23T21:30:40.000Z"), path = "The Pretty Reckless/Going to Hell/03 Heaven Knows.mp3", isVideo = false,
albumId = "388", artistId = "238", type = "music") playCount = 2, discNumber = 1, created = parseDate("2016-10-23T21:30:40.000Z"),
albumId = "388", artistId = "238", type = "music"
)
} }
} }
} }

View File

@ -27,23 +27,27 @@ class SubsonicApiGetSongsByGenreTest : SubsonicAPIClientTest() {
assertResponseSuccessful(response) assertResponseSuccessful(response)
response.body()!!.songsList.size `should be equal to` 2 response.body()!!.songsList.size `should be equal to` 2
with(response.body()!!.songsList) { with(response.body()!!.songsList) {
this[0] `should equal` MusicDirectoryChild(id = "575", parent = "576", isDir = false, this[0] `should equal` MusicDirectoryChild(
title = "Time Machine (Vadim Zhukov Remix)", album = "668", id = "575", parent = "576", isDir = false,
artist = "Tasadi", year = 2008, genre = "Trance", size = 22467672, title = "Time Machine (Vadim Zhukov Remix)", album = "668",
contentType = "audio/mpeg", suffix = "mp3", duration = 561, bitRate = 320, artist = "Tasadi", year = 2008, genre = "Trance", size = 22467672,
path = "Tasadi/668/00 Time Machine (Vadim Zhukov Remix).mp3", contentType = "audio/mpeg", suffix = "mp3", duration = 561, bitRate = 320,
isVideo = false, playCount = 0, created = parseDate("2016-10-23T21:58:29.000Z"), path = "Tasadi/668/00 Time Machine (Vadim Zhukov Remix).mp3",
albumId = "0", artistId = "0", type = "music") isVideo = false, playCount = 0, created = parseDate("2016-10-23T21:58:29.000Z"),
this[1] `should equal` MusicDirectoryChild(id = "621", parent = "622", isDir = false, albumId = "0", artistId = "0", type = "music"
title = "My Heart (Vadim Zhukov Remix)", album = "668", )
artist = "DJ Polyakov PPK Feat Kate Cameron", year = 2009, genre = "Trance", this[1] `should equal` MusicDirectoryChild(
size = 26805932, contentType = "audio/mpeg", suffix = "mp3", duration = 670, id = "621", parent = "622", isDir = false,
bitRate = 320, title = "My Heart (Vadim Zhukov Remix)", album = "668",
path = "DJ Polyakov PPK Feat Kate Cameron/668/00 My Heart (Vadim Zhukov " + artist = "DJ Polyakov PPK Feat Kate Cameron", year = 2009, genre = "Trance",
"Remix).mp3", size = 26805932, contentType = "audio/mpeg", suffix = "mp3", duration = 670,
isVideo = false, playCount = 2, bitRate = 320,
created = parseDate("2016-10-23T21:58:29.000Z"), path = "DJ Polyakov PPK Feat Kate Cameron/668/00 My Heart (Vadim Zhukov " +
albumId = "5", artistId = "4", type = "music") "Remix).mp3",
isVideo = false, playCount = 2,
created = parseDate("2016-10-23T21:58:29.000Z"),
albumId = "5", artistId = "4", type = "music"
)
} }
} }

View File

@ -30,8 +30,10 @@ class SubsonicApiGetStarred2Test : SubsonicAPIClientTest() {
with(response.body()!!.starred2) { with(response.body()!!.starred2) {
albumList `should equal` emptyList() albumList `should equal` emptyList()
artistList.size `should be equal to` 1 artistList.size `should be equal to` 1
artistList[0] `should equal` Artist(id = "364", name = "Parov Stelar", artistList[0] `should equal` Artist(
starred = parseDate("2017-08-12T18:32:58.768Z")) id = "364", name = "Parov Stelar",
starred = parseDate("2017-08-12T18:32:58.768Z")
)
songList `should equal` emptyList() songList `should equal` emptyList()
} }
} }
@ -40,8 +42,10 @@ class SubsonicApiGetStarred2Test : SubsonicAPIClientTest() {
fun `Should pass music folder id in request param`() { fun `Should pass music folder id in request param`() {
val musicFolderId = "441" val musicFolderId = "441"
mockWebServerRule.assertRequestParam(responseResourceName = "get_starred_2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "musicFolderId=$musicFolderId") { responseResourceName = "get_starred_2_ok.json",
expectedParam = "musicFolderId=$musicFolderId"
) {
client.api.getStarred2(musicFolderId = musicFolderId).execute() client.api.getStarred2(musicFolderId = musicFolderId).execute()
} }
} }

View File

@ -29,8 +29,10 @@ class SubsonicApiGetStarredTest : SubsonicAPIClientTest() {
with(response.body()!!.starred) { with(response.body()!!.starred) {
albumList `should equal` emptyList() albumList `should equal` emptyList()
artistList.size `should be equal to` 1 artistList.size `should be equal to` 1
artistList[0] `should equal` Artist(id = "364", name = "Parov Stelar", artistList[0] `should equal` Artist(
starred = parseDate("2017-08-12T18:32:58.768Z")) id = "364", name = "Parov Stelar",
starred = parseDate("2017-08-12T18:32:58.768Z")
)
songList `should equal` emptyList() songList `should equal` emptyList()
} }
} }
@ -39,8 +41,10 @@ class SubsonicApiGetStarredTest : SubsonicAPIClientTest() {
fun `Should pass music folder id in request param`() { fun `Should pass music folder id in request param`() {
val musicFolderId = "441" val musicFolderId = "441"
mockWebServerRule.assertRequestParam(responseResourceName = "get_starred_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "musicFolderId=$musicFolderId") { responseResourceName = "get_starred_ok.json",
expectedParam = "musicFolderId=$musicFolderId"
) {
client.api.getStarred(musicFolderId = musicFolderId).execute() client.api.getStarred(musicFolderId = musicFolderId).execute()
} }
} }

View File

@ -27,13 +27,15 @@ class SubsonicApiGetVideosListTest : SubsonicAPIClientTest() {
assertResponseSuccessful(response) assertResponseSuccessful(response)
with(response.body()!!.videosList) { with(response.body()!!.videosList) {
size `should be equal to` 1 size `should be equal to` 1
this[0] `should equal` MusicDirectoryChild(id = "10402", parent = "10401", this[0] `should equal` MusicDirectoryChild(
isDir = false, title = "MVI_0512", album = "Incoming", size = 21889646, id = "10402", parent = "10401",
contentType = "video/avi", suffix = "avi", isDir = false, title = "MVI_0512", album = "Incoming", size = 21889646,
transcodedContentType = "video/x-flv", transcodedSuffix = "flv", contentType = "video/avi", suffix = "avi",
path = "Incoming/MVI_0512.avi", isVideo = true, transcodedContentType = "video/x-flv", transcodedSuffix = "flv",
playCount = 0, created = parseDate("2017-11-19T12:34:33.000Z"), path = "Incoming/MVI_0512.avi", isVideo = true,
type = "video") playCount = 0, created = parseDate("2017-11-19T12:34:33.000Z"),
type = "video"
)
} }
} }
} }

View File

@ -51,15 +51,17 @@ class SubsonicApiJukeboxControlTest : SubsonicAPIClientTest() {
gain `should be equal to` 0.88f gain `should be equal to` 0.88f
position `should be equal to` 2 position `should be equal to` 2
playlistEntries.size `should be equal to` 2 playlistEntries.size `should be equal to` 2
playlistEntries[1] `should equal` MusicDirectoryChild(id = "4215", parent = "4186", playlistEntries[1] `should equal` MusicDirectoryChild(
isDir = false, title = "Going to Hell", album = "Going to Hell", id = "4215", parent = "4186",
artist = "The Pretty Reckless", track = 2, year = 2014, genre = "Hard Rock", isDir = false, title = "Going to Hell", album = "Going to Hell",
coverArt = "4186", size = 11089627, contentType = "audio/mpeg", artist = "The Pretty Reckless", track = 2, year = 2014, genre = "Hard Rock",
suffix = "mp3", duration = 277, bitRate = 320, coverArt = "4186", size = 11089627, contentType = "audio/mpeg",
path = "The Pretty Reckless/Going to Hell/02 Going to Hell.mp3", suffix = "mp3", duration = 277, bitRate = 320,
isVideo = false, playCount = 0, discNumber = 1, path = "The Pretty Reckless/Going to Hell/02 Going to Hell.mp3",
created = parseDate("2016-10-23T21:30:41.000Z"), albumId = "388", isVideo = false, playCount = 0, discNumber = 1,
artistId = "238", type = "music") created = parseDate("2016-10-23T21:30:41.000Z"), albumId = "388",
artistId = "238", type = "music"
)
} }
} }

View File

@ -1,10 +1,5 @@
package org.moire.ultrasonic.api.subsonic package org.moire.ultrasonic.api.subsonic
import okhttp3.mockwebserver.MockWebServer
import org.amshove.kluent.`should throw`
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.io.InputStream import java.io.InputStream
import java.net.InetAddress import java.net.InetAddress
import java.security.KeyStore import java.security.KeyStore
@ -14,6 +9,11 @@ import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLHandshakeException
import javax.net.ssl.TrustManagerFactory import javax.net.ssl.TrustManagerFactory
import okhttp3.mockwebserver.MockWebServer
import org.amshove.kluent.`should throw`
import org.junit.After
import org.junit.Before
import org.junit.Test
private const val PORT = 8443 private const val PORT = 8443
private const val HOST = "localhost" private const val HOST = "localhost"
@ -26,8 +26,11 @@ class SubsonicApiSSLTest {
@Before @Before
fun setUp() { fun setUp() {
val sslContext = createSSLContext(loadResourceStream("self-signed.pem"), val sslContext = createSSLContext(
loadResourceStream("self-signed.p12"), "") loadResourceStream("self-signed.pem"),
loadResourceStream("self-signed.p12"),
""
)
mockWebServer.useHttps(sslContext.socketFactory, false) mockWebServer.useHttps(sslContext.socketFactory, false)
mockWebServer.start(InetAddress.getByName(HOST), PORT) mockWebServer.start(InetAddress.getByName(HOST), PORT)
} }
@ -47,11 +50,13 @@ class SubsonicApiSSLTest {
trustStore.load(null) trustStore.load(null)
certificatePemStream.use { certificatePemStream.use {
cert = (CertificateFactory.getInstance("X.509") cert = (
.generateCertificate(certificatePemStream)) as X509Certificate CertificateFactory.getInstance("X.509")
.generateCertificate(certificatePemStream)
) as X509Certificate
} }
val alias = cert?.subjectX500Principal?.name val alias = cert?.subjectX500Principal?.name
?: throw IllegalStateException("Failed to load certificate") ?: throw IllegalStateException("Failed to load certificate")
trustStore.setCertificateEntry(alias, cert) trustStore.setCertificateEntry(alias, cert)
val tmf = TrustManagerFactory.getInstance("X509") val tmf = TrustManagerFactory.getInstance("X509")

View File

@ -1,7 +1,7 @@
package org.moire.ultrasonic.api.subsonic package org.moire.ultrasonic.api.subsonic
import org.junit.Test
import java.util.Calendar import java.util.Calendar
import org.junit.Test
/** /**
* Integration test for [SubsonicAPIClient] for scrobble call. * Integration test for [SubsonicAPIClient] for scrobble call.
@ -27,8 +27,10 @@ class SubsonicApiScrobbleTest : SubsonicAPIClientTest() {
fun `Should pass id param in request`() { fun `Should pass id param in request`() {
val id = "some-id" val id = "some-id"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "id=$id") { responseResourceName = "ping_ok.json",
expectedParam = "id=$id"
) {
client.api.scrobble(id = id).execute() client.api.scrobble(id = id).execute()
} }
} }
@ -37,8 +39,10 @@ class SubsonicApiScrobbleTest : SubsonicAPIClientTest() {
fun `Should pass time param in request`() { fun `Should pass time param in request`() {
val time = Calendar.getInstance().timeInMillis val time = Calendar.getInstance().timeInMillis
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "time=$time") { responseResourceName = "ping_ok.json",
expectedParam = "time=$time"
) {
client.api.scrobble(id = "some-id", time = time).execute() client.api.scrobble(id = "some-id", time = time).execute()
} }
} }
@ -47,8 +51,10 @@ class SubsonicApiScrobbleTest : SubsonicAPIClientTest() {
fun `Should pass submission param in request`() { fun `Should pass submission param in request`() {
val submission = false val submission = false
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "submission=$submission") { responseResourceName = "ping_ok.json",
expectedParam = "submission=$submission"
) {
client.api.scrobble(id = "some-id", submission = submission).execute() client.api.scrobble(id = "some-id", submission = submission).execute()
} }
} }

View File

@ -1,12 +1,12 @@
package org.moire.ultrasonic.api.subsonic package org.moire.ultrasonic.api.subsonic
import java.util.Calendar
import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.`should equal` import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be` import org.amshove.kluent.`should not be`
import org.junit.Test import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
import org.moire.ultrasonic.api.subsonic.models.SearchResult import org.moire.ultrasonic.api.subsonic.models.SearchResult
import java.util.Calendar
/** /**
* Integration test for [SubsonicAPIClient] for search call. * Integration test for [SubsonicAPIClient] for search call.
@ -33,17 +33,19 @@ class SubsonicApiSearchTest : SubsonicAPIClientTest() {
offset `should be equal to` 10 offset `should be equal to` 10
totalHits `should be equal to` 53 totalHits `should be equal to` 53
matchList.size `should be equal to` 1 matchList.size `should be equal to` 1
matchList[0] `should equal` MusicDirectoryChild(id = "5831", parent = "5766", matchList[0] `should equal` MusicDirectoryChild(
isDir = false, title = "You'll Be Under My Wheels", id = "5831", parent = "5766",
album = "Need for Speed Most Wanted", artist = "The Prodigy", isDir = false, title = "You'll Be Under My Wheels",
track = 17, year = 2005, genre = "Rap", coverArt = "5766", album = "Need for Speed Most Wanted", artist = "The Prodigy",
size = 5607024, contentType = "audio/mpeg", suffix = "mp3", duration = 233, track = 17, year = 2005, genre = "Rap", coverArt = "5766",
bitRate = 192, size = 5607024, contentType = "audio/mpeg", suffix = "mp3", duration = 233,
path = "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels" + bitRate = 192,
".mp3", path = "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels" +
isVideo = false, playCount = 0, discNumber = 1, ".mp3",
created = parseDate("2016-10-23T20:09:02.000Z"), albumId = "568", isVideo = false, playCount = 0, discNumber = 1,
artistId = "505", type = "music") created = parseDate("2016-10-23T20:09:02.000Z"), albumId = "568",
artistId = "505", type = "music"
)
} }
} }
@ -51,8 +53,10 @@ class SubsonicApiSearchTest : SubsonicAPIClientTest() {
fun `Should pass artist param`() { fun `Should pass artist param`() {
val artist = "some-artist" val artist = "some-artist"
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "artist=$artist") { responseResourceName = "search_ok.json",
expectedParam = "artist=$artist"
) {
client.api.search(artist = artist).execute() client.api.search(artist = artist).execute()
} }
} }
@ -61,8 +65,10 @@ class SubsonicApiSearchTest : SubsonicAPIClientTest() {
fun `Should pass album param`() { fun `Should pass album param`() {
val album = "some-album" val album = "some-album"
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "album=$album") { responseResourceName = "search_ok.json",
expectedParam = "album=$album"
) {
client.api.search(album = album).execute() client.api.search(album = album).execute()
} }
} }
@ -71,8 +77,10 @@ class SubsonicApiSearchTest : SubsonicAPIClientTest() {
fun `Should pass title param`() { fun `Should pass title param`() {
val title = "some-title" val title = "some-title"
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "title=$title") { responseResourceName = "search_ok.json",
expectedParam = "title=$title"
) {
client.api.search(title = title).execute() client.api.search(title = title).execute()
} }
} }
@ -81,8 +89,10 @@ class SubsonicApiSearchTest : SubsonicAPIClientTest() {
fun `Should contain any param`() { fun `Should contain any param`() {
val any = "AnyString" val any = "AnyString"
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "any=$any") { responseResourceName = "search_ok.json",
expectedParam = "any=$any"
) {
client.api.search(any = any).execute() client.api.search(any = any).execute()
} }
} }
@ -91,8 +101,10 @@ class SubsonicApiSearchTest : SubsonicAPIClientTest() {
fun `Should contain count param`() { fun `Should contain count param`() {
val count = 11 val count = 11
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "count=$count") { responseResourceName = "search_ok.json",
expectedParam = "count=$count"
) {
client.api.search(count = count).execute() client.api.search(count = count).execute()
} }
} }
@ -101,8 +113,10 @@ class SubsonicApiSearchTest : SubsonicAPIClientTest() {
fun `Should contain offset param`() { fun `Should contain offset param`() {
val offset = 54 val offset = 54
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "offset=$offset") { responseResourceName = "search_ok.json",
expectedParam = "offset=$offset"
) {
client.api.search(offset = offset).execute() client.api.search(offset = offset).execute()
} }
} }
@ -111,8 +125,10 @@ class SubsonicApiSearchTest : SubsonicAPIClientTest() {
fun `Should contain newerThan param`() { fun `Should contain newerThan param`() {
val newerThan = Calendar.getInstance() val newerThan = Calendar.getInstance()
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "newerThan=${newerThan.time.time}") { responseResourceName = "search_ok.json",
expectedParam = "newerThan=${newerThan.time.time}"
) {
client.api.search(newerThan = newerThan.time.time).execute() client.api.search(newerThan = newerThan.time.time).execute()
} }
} }

View File

@ -32,26 +32,32 @@ class SubsonicApiSearchThreeTest : SubsonicAPIClientTest() {
assertResponseSuccessful(response) assertResponseSuccessful(response)
with(response.body()!!.searchResult) { with(response.body()!!.searchResult) {
artistList.size `should be equal to` 1 artistList.size `should be equal to` 1
artistList[0] `should equal` Artist(id = "505", name = "The Prodigy", artistList[0] `should equal` Artist(
coverArt = "ar-505", albumCount = 5) id = "505", name = "The Prodigy",
coverArt = "ar-505", albumCount = 5
)
albumList.size `should be equal to` 1 albumList.size `should be equal to` 1
albumList[0] `should equal` Album(id = "855", albumList[0] `should equal` Album(
name = "Always Outnumbered, Never Outgunned", id = "855",
artist = "The Prodigy", artistId = "505", coverArt = "al-855", songCount = 12, name = "Always Outnumbered, Never Outgunned",
duration = 3313, created = parseDate("2016-10-23T20:57:27.000Z"), artist = "The Prodigy", artistId = "505", coverArt = "al-855", songCount = 12,
year = 2004, genre = "Electronic") duration = 3313, created = parseDate("2016-10-23T20:57:27.000Z"),
year = 2004, genre = "Electronic"
)
songList.size `should be equal to` 1 songList.size `should be equal to` 1
songList[0] `should equal` MusicDirectoryChild(id = "5831", parent = "5766", songList[0] `should equal` MusicDirectoryChild(
isDir = false, id = "5831", parent = "5766",
title = "You'll Be Under My Wheels", album = "Need for Speed Most Wanted", isDir = false,
artist = "The Prodigy", track = 17, year = 2005, genre = "Rap", title = "You'll Be Under My Wheels", album = "Need for Speed Most Wanted",
coverArt = "5766", size = 5607024, contentType = "audio/mpeg", artist = "The Prodigy", track = 17, year = 2005, genre = "Rap",
suffix = "mp3", duration = 233, bitRate = 192, coverArt = "5766", size = 5607024, contentType = "audio/mpeg",
path = "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels" + suffix = "mp3", duration = 233, bitRate = 192,
".mp3", path = "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels" +
isVideo = false, playCount = 0, discNumber = 1, ".mp3",
created = parseDate("2016-10-23T20:09:02.000Z"), albumId = "568", isVideo = false, playCount = 0, discNumber = 1,
artistId = "505", type = "music") created = parseDate("2016-10-23T20:09:02.000Z"), albumId = "568",
artistId = "505", type = "music"
)
} }
} }
@ -59,8 +65,10 @@ class SubsonicApiSearchThreeTest : SubsonicAPIClientTest() {
fun `Should pass query as request param`() { fun `Should pass query as request param`() {
val query = "some-wip-query" val query = "some-wip-query"
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "query=$query") { responseResourceName = "search3_ok.json",
expectedParam = "query=$query"
) {
client.api.search3(query = query).execute() client.api.search3(query = query).execute()
} }
} }
@ -69,8 +77,10 @@ class SubsonicApiSearchThreeTest : SubsonicAPIClientTest() {
fun `Should pass artist count as request param`() { fun `Should pass artist count as request param`() {
val artistCount = 67 val artistCount = 67
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "artistCount=$artistCount") { responseResourceName = "search3_ok.json",
expectedParam = "artistCount=$artistCount"
) {
client.api.search3("some", artistCount = artistCount).execute() client.api.search3("some", artistCount = artistCount).execute()
} }
} }
@ -79,8 +89,10 @@ class SubsonicApiSearchThreeTest : SubsonicAPIClientTest() {
fun `Should pass artist offset as request param`() { fun `Should pass artist offset as request param`() {
val artistOffset = 34 val artistOffset = 34
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "artistOffset=$artistOffset") { responseResourceName = "search3_ok.json",
expectedParam = "artistOffset=$artistOffset"
) {
client.api.search3("some", artistOffset = artistOffset).execute() client.api.search3("some", artistOffset = artistOffset).execute()
} }
} }
@ -89,8 +101,10 @@ class SubsonicApiSearchThreeTest : SubsonicAPIClientTest() {
fun `Should pass album count as request param`() { fun `Should pass album count as request param`() {
val albumCount = 21 val albumCount = 21
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "albumCount=$albumCount") { responseResourceName = "search3_ok.json",
expectedParam = "albumCount=$albumCount"
) {
client.api.search3("some", albumCount = albumCount).execute() client.api.search3("some", albumCount = albumCount).execute()
} }
} }
@ -99,8 +113,10 @@ class SubsonicApiSearchThreeTest : SubsonicAPIClientTest() {
fun `Should pass album offset as request param`() { fun `Should pass album offset as request param`() {
val albumOffset = 43 val albumOffset = 43
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "albumOffset=$albumOffset") { responseResourceName = "search3_ok.json",
expectedParam = "albumOffset=$albumOffset"
) {
client.api.search3("some", albumOffset = albumOffset).execute() client.api.search3("some", albumOffset = albumOffset).execute()
} }
} }
@ -109,8 +125,10 @@ class SubsonicApiSearchThreeTest : SubsonicAPIClientTest() {
fun `Should pass song count as request param`() { fun `Should pass song count as request param`() {
val songCount = 15 val songCount = 15
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "songCount=$songCount") { responseResourceName = "search3_ok.json",
expectedParam = "songCount=$songCount"
) {
client.api.search3("some", songCount = songCount).execute() client.api.search3("some", songCount = songCount).execute()
} }
} }
@ -119,8 +137,10 @@ class SubsonicApiSearchThreeTest : SubsonicAPIClientTest() {
fun `Should pass music folder id as request param`() { fun `Should pass music folder id as request param`() {
val musicFolderId = "43" val musicFolderId = "43"
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "musicFolderId=$musicFolderId") { responseResourceName = "search3_ok.json",
expectedParam = "musicFolderId=$musicFolderId"
) {
client.api.search3("some", musicFolderId = musicFolderId).execute() client.api.search3("some", musicFolderId = musicFolderId).execute()
} }
} }

View File

@ -33,23 +33,27 @@ class SubsonicApiSearchTwoTest : SubsonicAPIClientTest() {
artistList.size `should be equal to` 1 artistList.size `should be equal to` 1
artistList[0] `should equal` Artist(id = "522", name = "The Prodigy") artistList[0] `should equal` Artist(id = "522", name = "The Prodigy")
albumList.size `should be equal to` 1 albumList.size `should be equal to` 1
albumList[0] `should equal` MusicDirectoryChild(id = "8867", parent = "522", albumList[0] `should equal` MusicDirectoryChild(
isDir = true, title = "Always Outnumbered, Never Outgunned", id = "8867", parent = "522",
album = "Always Outnumbered, Never Outgunned", artist = "The Prodigy", isDir = true, title = "Always Outnumbered, Never Outgunned",
year = 2004, genre = "Electronic", coverArt = "8867", playCount = 0, album = "Always Outnumbered, Never Outgunned", artist = "The Prodigy",
created = parseDate("2016-10-23T20:57:27.000Z")) year = 2004, genre = "Electronic", coverArt = "8867", playCount = 0,
created = parseDate("2016-10-23T20:57:27.000Z")
)
songList.size `should be equal to` 1 songList.size `should be equal to` 1
songList[0] `should equal` MusicDirectoryChild(id = "5831", parent = "5766", songList[0] `should equal` MusicDirectoryChild(
isDir = false, id = "5831", parent = "5766",
title = "You'll Be Under My Wheels", album = "Need for Speed Most Wanted", isDir = false,
artist = "The Prodigy", track = 17, year = 2005, genre = "Rap", title = "You'll Be Under My Wheels", album = "Need for Speed Most Wanted",
coverArt = "5766", size = 5607024, contentType = "audio/mpeg", artist = "The Prodigy", track = 17, year = 2005, genre = "Rap",
suffix = "mp3", duration = 233, bitRate = 192, coverArt = "5766", size = 5607024, contentType = "audio/mpeg",
path = "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels" + suffix = "mp3", duration = 233, bitRate = 192,
".mp3", path = "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels" +
isVideo = false, playCount = 0, discNumber = 1, ".mp3",
created = parseDate("2016-10-23T20:09:02.000Z"), isVideo = false, playCount = 0, discNumber = 1,
albumId = "568", artistId = "505", type = "music") created = parseDate("2016-10-23T20:09:02.000Z"),
albumId = "568", artistId = "505", type = "music"
)
} }
} }
@ -57,8 +61,10 @@ class SubsonicApiSearchTwoTest : SubsonicAPIClientTest() {
fun `Should pass query id in request param`() { fun `Should pass query id in request param`() {
val query = "some" val query = "some"
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "query=$query") { responseResourceName = "search2_ok.json",
expectedParam = "query=$query"
) {
client.api.search2(query).execute() client.api.search2(query).execute()
} }
} }
@ -67,8 +73,10 @@ class SubsonicApiSearchTwoTest : SubsonicAPIClientTest() {
fun `Should pass artist count in request param`() { fun `Should pass artist count in request param`() {
val artistCount = 45 val artistCount = 45
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "artistCount=$artistCount") { responseResourceName = "search2_ok.json",
expectedParam = "artistCount=$artistCount"
) {
client.api.search2("some", artistCount = artistCount).execute() client.api.search2("some", artistCount = artistCount).execute()
} }
} }
@ -77,8 +85,10 @@ class SubsonicApiSearchTwoTest : SubsonicAPIClientTest() {
fun `Should pass artist offset in request param`() { fun `Should pass artist offset in request param`() {
val artistOffset = 13 val artistOffset = 13
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "artistOffset=$artistOffset") { responseResourceName = "search2_ok.json",
expectedParam = "artistOffset=$artistOffset"
) {
client.api.search2("some", artistOffset = artistOffset).execute() client.api.search2("some", artistOffset = artistOffset).execute()
} }
} }
@ -87,8 +97,10 @@ class SubsonicApiSearchTwoTest : SubsonicAPIClientTest() {
fun `Should pass album count in request param`() { fun `Should pass album count in request param`() {
val albumCount = 30 val albumCount = 30
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "albumCount=$albumCount") { responseResourceName = "search2_ok.json",
expectedParam = "albumCount=$albumCount"
) {
client.api.search2("some", albumCount = albumCount).execute() client.api.search2("some", albumCount = albumCount).execute()
} }
} }
@ -97,8 +109,10 @@ class SubsonicApiSearchTwoTest : SubsonicAPIClientTest() {
fun `Should pass album offset in request param`() { fun `Should pass album offset in request param`() {
val albumOffset = 91 val albumOffset = 91
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "albumOffset=$albumOffset") { responseResourceName = "search2_ok.json",
expectedParam = "albumOffset=$albumOffset"
) {
client.api.search2("some", albumOffset = albumOffset).execute() client.api.search2("some", albumOffset = albumOffset).execute()
} }
} }
@ -107,8 +121,10 @@ class SubsonicApiSearchTwoTest : SubsonicAPIClientTest() {
fun `Should pass song count in request param`() { fun `Should pass song count in request param`() {
val songCount = 22 val songCount = 22
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "songCount=$songCount") { responseResourceName = "search2_ok.json",
expectedParam = "songCount=$songCount"
) {
client.api.search2("some", songCount = songCount).execute() client.api.search2("some", songCount = songCount).execute()
} }
} }
@ -117,8 +133,10 @@ class SubsonicApiSearchTwoTest : SubsonicAPIClientTest() {
fun `Should pass music folder id in request param`() { fun `Should pass music folder id in request param`() {
val musicFolderId = "565" val musicFolderId = "565"
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "musicFolderId=$musicFolderId") { responseResourceName = "search2_ok.json",
expectedParam = "musicFolderId=$musicFolderId"
) {
client.api.search2("some", musicFolderId = musicFolderId).execute() client.api.search2("some", musicFolderId = musicFolderId).execute()
} }
} }

View File

@ -36,8 +36,10 @@ class SubsonicApiSetRatingTest : SubsonicAPIClientTest() {
val id = "110" val id = "110"
val rating = 5 val rating = 5
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "id=$id") { responseResourceName = "ping_ok.json",
expectedParam = "id=$id"
) {
client.api.setRating(id, rating).execute() client.api.setRating(id, rating).execute()
} }
} }
@ -47,8 +49,10 @@ class SubsonicApiSetRatingTest : SubsonicAPIClientTest() {
val id = "110" val id = "110"
val rating = 5 val rating = 5
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "rating=$rating") { responseResourceName = "ping_ok.json",
expectedParam = "rating=$rating"
) {
client.api.setRating(id, rating).execute() client.api.setRating(id, rating).execute()
} }
} }

View File

@ -29,8 +29,10 @@ class SubsonicApiStarTest : SubsonicAPIClientTest() {
fun `Should pass id param`() { fun `Should pass id param`() {
val id = "110" val id = "110"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "id=$id") { responseResourceName = "ping_ok.json",
expectedParam = "id=$id"
) {
client.api.star(id = id).execute() client.api.star(id = id).execute()
} }
} }
@ -39,8 +41,10 @@ class SubsonicApiStarTest : SubsonicAPIClientTest() {
fun `Should pass artist id param`() { fun `Should pass artist id param`() {
val artistId = "123" val artistId = "123"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "artistId=$artistId") { responseResourceName = "ping_ok.json",
expectedParam = "artistId=$artistId"
) {
client.api.star(artistId = artistId).execute() client.api.star(artistId = artistId).execute()
} }
} }
@ -49,8 +53,10 @@ class SubsonicApiStarTest : SubsonicAPIClientTest() {
fun `Should pass album id param`() { fun `Should pass album id param`() {
val albumId = "1001" val albumId = "1001"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "albumId=$albumId") { responseResourceName = "ping_ok.json",
expectedParam = "albumId=$albumId"
) {
client.api.star(albumId = albumId).execute() client.api.star(albumId = albumId).execute()
} }
} }

View File

@ -40,8 +40,10 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() {
@Test @Test
fun `Should return successfull call stream`() { fun `Should return successfull call stream`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse() mockWebServerRule.mockWebServer.enqueue(
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))) MockResponse()
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))
)
val response = client.stream("some-id") val response = client.stream("some-id")
@ -67,8 +69,10 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() {
fun `Should pass max bit rate as param`() { fun `Should pass max bit rate as param`() {
val maxBitRate = 360 val maxBitRate = 360
mockWebServerRule.assertRequestParam("ping_ok.json", mockWebServerRule.assertRequestParam(
"maxBitRate=$maxBitRate") { "ping_ok.json",
"maxBitRate=$maxBitRate"
) {
client.api.stream("some-id", maxBitRate = maxBitRate).execute() client.api.stream("some-id", maxBitRate = maxBitRate).execute()
} }
} }
@ -77,8 +81,10 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() {
fun `Should pass format as param`() { fun `Should pass format as param`() {
val format = "aac" val format = "aac"
mockWebServerRule.assertRequestParam("ping_ok.json", mockWebServerRule.assertRequestParam(
"format=$format") { "ping_ok.json",
"format=$format"
) {
client.api.stream("some-id", format = format).execute() client.api.stream("some-id", format = format).execute()
} }
} }
@ -87,8 +93,10 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() {
fun `Should pass time offset as param`() { fun `Should pass time offset as param`() {
val timeOffset = 155 val timeOffset = 155
mockWebServerRule.assertRequestParam("ping_ok.json", mockWebServerRule.assertRequestParam(
"timeOffset=$timeOffset") { "ping_ok.json",
"timeOffset=$timeOffset"
) {
client.api.stream("some-id", timeOffset = timeOffset).execute() client.api.stream("some-id", timeOffset = timeOffset).execute()
} }
} }
@ -97,8 +105,10 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() {
fun `Should pass video size as param`() { fun `Should pass video size as param`() {
val videoSize = "44144" val videoSize = "44144"
mockWebServerRule.assertRequestParam("ping_ok.json", mockWebServerRule.assertRequestParam(
"size=$videoSize") { "ping_ok.json",
"size=$videoSize"
) {
client.api.stream("some-id", videoSize = videoSize).execute() client.api.stream("some-id", videoSize = videoSize).execute()
} }
} }
@ -107,8 +117,10 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() {
fun `Should pass estimate content length as param`() { fun `Should pass estimate content length as param`() {
val estimateContentLength = true val estimateContentLength = true
mockWebServerRule.assertRequestParam("ping_ok.json", mockWebServerRule.assertRequestParam(
"estimateContentLength=$estimateContentLength") { "ping_ok.json",
"estimateContentLength=$estimateContentLength"
) {
client.api.stream("some-id", estimateContentLength = estimateContentLength).execute() client.api.stream("some-id", estimateContentLength = estimateContentLength).execute()
} }
} }
@ -117,8 +129,10 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() {
fun `Should pass converted as param`() { fun `Should pass converted as param`() {
val converted = false val converted = false
mockWebServerRule.assertRequestParam("ping_ok.json", mockWebServerRule.assertRequestParam(
"converted=$converted") { "ping_ok.json",
"converted=$converted"
) {
client.api.stream("some-id", converted = converted).execute() client.api.stream("some-id", converted = converted).execute()
} }
} }

View File

@ -20,17 +20,22 @@ class SubsonicApiUnstarTest : SubsonicAPIClientTest() {
@Test @Test
fun `Should parse error response`() { fun `Should parse error response`() {
checkErrorCallParsed(mockWebServerRule, { checkErrorCallParsed(
client.api.unstar().execute() mockWebServerRule,
}) {
client.api.unstar().execute()
}
)
} }
@Test @Test
fun `Should pass id param`() { fun `Should pass id param`() {
val id = "545" val id = "545"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "id=$id") { responseResourceName = "ping_ok.json",
expectedParam = "id=$id"
) {
client.api.unstar(id = id).execute() client.api.unstar(id = id).execute()
} }
} }
@ -39,8 +44,10 @@ class SubsonicApiUnstarTest : SubsonicAPIClientTest() {
fun `Should pass artistId param`() { fun `Should pass artistId param`() {
val artistId = "644" val artistId = "644"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "artistId=$artistId") { responseResourceName = "ping_ok.json",
expectedParam = "artistId=$artistId"
) {
client.api.unstar(artistId = artistId).execute() client.api.unstar(artistId = artistId).execute()
} }
} }
@ -49,8 +56,10 @@ class SubsonicApiUnstarTest : SubsonicAPIClientTest() {
fun `Should pass albumId param`() { fun `Should pass albumId param`() {
val albumId = "3344" val albumId = "3344"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "albumId=$albumId") { responseResourceName = "ping_ok.json",
expectedParam = "albumId=$albumId"
) {
client.api.unstar(albumId = albumId).execute() client.api.unstar(albumId = albumId).execute()
} }
} }

View File

@ -26,8 +26,10 @@ class SubsonicApiUpdatePlaylistTest : SubsonicAPIClientTest() {
fun `Should pass playlist id param in request`() { fun `Should pass playlist id param in request`() {
val id = "5453" val id = "5453"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "playlistId=$id") { responseResourceName = "ping_ok.json",
expectedParam = "playlistId=$id"
) {
client.api.updatePlaylist(id = id).execute() client.api.updatePlaylist(id = id).execute()
} }
} }
@ -36,8 +38,10 @@ class SubsonicApiUpdatePlaylistTest : SubsonicAPIClientTest() {
fun `Should pass name param in request`() { fun `Should pass name param in request`() {
val name = "some-name" val name = "some-name"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "name=$name") { responseResourceName = "ping_ok.json",
expectedParam = "name=$name"
) {
client.api.updatePlaylist("22", name = name).execute() client.api.updatePlaylist("22", name = name).execute()
} }
} }
@ -46,8 +50,10 @@ class SubsonicApiUpdatePlaylistTest : SubsonicAPIClientTest() {
fun `Should pass comment param in request`() { fun `Should pass comment param in request`() {
val comment = "some-unusual-comment" val comment = "some-unusual-comment"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "comment=$comment") { responseResourceName = "ping_ok.json",
expectedParam = "comment=$comment"
) {
client.api.updatePlaylist("42", comment = comment).execute() client.api.updatePlaylist("42", comment = comment).execute()
} }
} }
@ -56,8 +62,10 @@ class SubsonicApiUpdatePlaylistTest : SubsonicAPIClientTest() {
fun `Should pass public param in request`() { fun `Should pass public param in request`() {
val public = true val public = true
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "public=$public") { responseResourceName = "ping_ok.json",
expectedParam = "public=$public"
) {
client.api.updatePlaylist("53", public = public).execute() client.api.updatePlaylist("53", public = public).execute()
} }
} }
@ -66,8 +74,10 @@ class SubsonicApiUpdatePlaylistTest : SubsonicAPIClientTest() {
fun `Should pass song ids to update in request`() { fun `Should pass song ids to update in request`() {
val songIds = listOf("45", "81") val songIds = listOf("45", "81")
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "songIdToAdd=${songIds[0]}&songIdToAdd=${songIds[1]}") { responseResourceName = "ping_ok.json",
expectedParam = "songIdToAdd=${songIds[0]}&songIdToAdd=${songIds[1]}"
) {
client.api.updatePlaylist("25", songIdsToAdd = songIds).execute() client.api.updatePlaylist("25", songIdsToAdd = songIds).execute()
} }
} }
@ -76,9 +86,11 @@ class SubsonicApiUpdatePlaylistTest : SubsonicAPIClientTest() {
fun `Should pass song indexes to remove in request`() { fun `Should pass song indexes to remove in request`() {
val songIndexesToRemove = listOf(129, 1) val songIndexesToRemove = listOf(129, 1)
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json", mockWebServerRule.assertRequestParam(
expectedParam = "songIndexToRemove=${songIndexesToRemove[0]}&" + responseResourceName = "ping_ok.json",
"songIndexToRemove=${songIndexesToRemove[1]}") { expectedParam = "songIndexToRemove=${songIndexesToRemove[0]}&" +
"songIndexToRemove=${songIndexesToRemove[1]}"
) {
client.api.updatePlaylist("49", songIndexesToRemove = songIndexesToRemove).execute() client.api.updatePlaylist("49", songIndexesToRemove = songIndexesToRemove).execute()
} }
} }

View File

@ -29,7 +29,7 @@ abstract class BaseInterceptorTest {
* params to the [Request]. * params to the [Request].
*/ */
fun createRequest(additionalParams: (Request.Builder) -> Unit): Request = Request.Builder() fun createRequest(additionalParams: (Request.Builder) -> Unit): Request = Request.Builder()
.url(mockWebServerRule.mockWebServer.url("/")) .url(mockWebServerRule.mockWebServer.url("/"))
.also { additionalParams(it) } .also { additionalParams(it) }
.build() .build()
} }

View File

@ -1,12 +1,12 @@
package org.moire.ultrasonic.api.subsonic.interceptors package org.moire.ultrasonic.api.subsonic.interceptors
import java.security.MessageDigest
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should contain` import org.amshove.kluent.`should contain`
import org.amshove.kluent.`should not contain` import org.amshove.kluent.`should not contain`
import org.apache.commons.codec.binary.Hex import org.apache.commons.codec.binary.Hex
import org.junit.Test import org.junit.Test
import java.security.MessageDigest
/** /**
* Integration test for [PasswordMD5Interceptor]. * Integration test for [PasswordMD5Interceptor].
@ -28,9 +28,14 @@ class PasswordMD5InterceptorTest : BaseInterceptorTest() {
requestLine `should not contain` "p=enc:" requestLine `should not contain` "p=enc:"
val salt = requestLine.split('&').find { it.startsWith("s=") } val salt = requestLine.split('&').find { it.startsWith("s=") }
?.substringAfter('=')?.substringBefore(" ") ?.substringAfter('=')?.substringBefore(" ")
val expectedToken = String(Hex.encodeHex(MessageDigest.getInstance("MD5") val expectedToken = String(
.digest("$password$salt".toByteArray()), true)) Hex.encodeHex(
MessageDigest.getInstance("MD5")
.digest("$password$salt".toByteArray()),
true
)
)
requestLine `should contain` "t=$expectedToken" requestLine `should contain` "t=$expectedToken"
} }
} }

View File

@ -1,5 +1,6 @@
package org.moire.ultrasonic.api.subsonic.interceptors package org.moire.ultrasonic.api.subsonic.interceptors
import kotlin.LazyThreadSafetyMode.NONE
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should contain` import org.amshove.kluent.`should contain`
@ -7,7 +8,6 @@ import org.amshove.kluent.`should equal`
import org.junit.Test import org.junit.Test
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.enqueueResponse import org.moire.ultrasonic.api.subsonic.enqueueResponse
import kotlin.LazyThreadSafetyMode.NONE
/** /**
* Integration test for [VersionInterceptor]. * Integration test for [VersionInterceptor].
@ -41,7 +41,7 @@ class VersionInterceptorTest : BaseInterceptorTest() {
client.newCall(createRequest {}).execute() client.newCall(createRequest {}).execute()
(interceptor as VersionInterceptor) (interceptor as VersionInterceptor)
.protocolVersion `should equal` SubsonicAPIVersions.V1_13_0 .protocolVersion `should equal` SubsonicAPIVersions.V1_13_0
} }
@Test @Test
@ -51,7 +51,7 @@ class VersionInterceptorTest : BaseInterceptorTest() {
client.newCall(createRequest {}).execute() client.newCall(createRequest {}).execute()
(interceptor as VersionInterceptor) (interceptor as VersionInterceptor)
.protocolVersion `should equal` SubsonicAPIVersions.V1_16_0 .protocolVersion `should equal` SubsonicAPIVersions.V1_16_0
} }
@Test @Test
@ -65,9 +65,11 @@ class VersionInterceptorTest : BaseInterceptorTest() {
@Test @Test
fun `Should not update version on non-json response`() { fun `Should not update version on non-json response`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse() mockWebServerRule.mockWebServer.enqueue(
MockResponse()
.setBody("asdqwnekjnqwkjen") .setBody("asdqwnekjnqwkjen")
.setHeader("Content-Type", "application/octet-stream")) .setHeader("Content-Type", "application/octet-stream")
)
client.newCall(createRequest {}).execute() client.newCall(createRequest {}).execute()

View File

@ -82,8 +82,9 @@ internal class ApiVersionCheckWrapper(
): Call<SearchTwoResponse> { ): Call<SearchTwoResponse> {
checkVersion(V1_4_0) checkVersion(V1_4_0)
checkParamVersion(musicFolderId, V1_12_0) checkParamVersion(musicFolderId, V1_12_0)
return api.search2(query, artistCount, artistOffset, albumCount, albumOffset, songCount, return api.search2(
musicFolderId) query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
)
} }
override fun search3( override fun search3(
@ -97,8 +98,9 @@ internal class ApiVersionCheckWrapper(
): Call<SearchThreeResponse> { ): Call<SearchThreeResponse> {
checkVersion(V1_8_0) checkVersion(V1_8_0)
checkParamVersion(musicFolderId, V1_12_0) checkParamVersion(musicFolderId, V1_12_0)
return api.search3(query, artistCount, artistOffset, albumCount, albumOffset, return api.search3(
songCount, musicFolderId) query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
)
} }
override fun getPlaylists(username: String?): Call<GetPlaylistsResponse> { override fun getPlaylists(username: String?): Call<GetPlaylistsResponse> {
@ -216,8 +218,9 @@ internal class ApiVersionCheckWrapper(
checkParamVersion(videoSize, V1_6_0) checkParamVersion(videoSize, V1_6_0)
checkParamVersion(estimateContentLength, V1_8_0) checkParamVersion(estimateContentLength, V1_8_0)
checkParamVersion(converted, V1_14_0) checkParamVersion(converted, V1_14_0)
return api.stream(id, maxBitRate, format, timeOffset, videoSize, return api.stream(
estimateContentLength, converted) id, maxBitRate, format, timeOffset, videoSize, estimateContentLength, converted
)
} }
override fun jukeboxControl( override fun jukeboxControl(

View File

@ -4,6 +4,11 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.ResponseBody import okhttp3.ResponseBody
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
@ -17,11 +22,6 @@ import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import retrofit2.Response import retrofit2.Response
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory import retrofit2.converter.jackson.JacksonConverterFactory
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
private const val READ_TIMEOUT = 60_000L private const val READ_TIMEOUT = 60_000L
@ -44,10 +44,10 @@ class SubsonicAPIClient(
} }
private val proxyPasswordInterceptor = ProxyPasswordInterceptor( private val proxyPasswordInterceptor = ProxyPasswordInterceptor(
config.minimalProtocolVersion, config.minimalProtocolVersion,
PasswordHexInterceptor(config.password), PasswordHexInterceptor(config.password),
PasswordMD5Interceptor(config.password), PasswordMD5Interceptor(config.password),
config.enableLdapUserSupport config.enableLdapUserSupport
) )
/** /**
@ -61,38 +61,38 @@ class SubsonicAPIClient(
} }
private val okHttpClient = baseOkClient.newBuilder() private val okHttpClient = baseOkClient.newBuilder()
.readTimeout(READ_TIMEOUT, MILLISECONDS) .readTimeout(READ_TIMEOUT, MILLISECONDS)
.apply { if (config.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", config.username) .addQueryParameter("u", config.username)
.addQueryParameter("c", config.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())
} }
.addInterceptor(versionInterceptor) .addInterceptor(versionInterceptor)
.addInterceptor(proxyPasswordInterceptor) .addInterceptor(proxyPasswordInterceptor)
.addInterceptor(RangeHeaderInterceptor()) .addInterceptor(RangeHeaderInterceptor())
.apply { if (config.debug) addLogging() } .apply { if (config.debug) addLogging() }
.build() .build()
private val jacksonMapper = ObjectMapper() private val jacksonMapper = ObjectMapper()
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true) .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(KotlinModule()) .registerModule(KotlinModule())
private val retrofit = Retrofit.Builder() private val retrofit = Retrofit.Builder()
.baseUrl("${config.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),
config.minimalProtocolVersion config.minimalProtocolVersion
) )
val api: SubsonicAPIDefinition get() = wrappedApi val api: SubsonicAPIDefinition get() = wrappedApi
@ -118,9 +118,9 @@ class SubsonicAPIClient(
* Prefer this method over [SubsonicAPIDefinition.stream] as this handles error cases. * Prefer this method over [SubsonicAPIDefinition.stream] as this handles error cases.
*/ */
fun stream(id: String, maxBitrate: Int? = null, offset: Long? = null): StreamResponse = fun stream(id: String, maxBitrate: Int? = null, offset: Long? = null): StreamResponse =
handleStreamResponse { handleStreamResponse {
api.stream(id, maxBitrate, offset = offset).execute() api.stream(id, maxBitrate, offset = offset).execute()
} }
/** /**
* Convenient method to get user avatar using [username]. * Convenient method to get user avatar using [username].
@ -138,14 +138,18 @@ class SubsonicAPIClient(
return if (response.isSuccessful) { return if (response.isSuccessful) {
val responseBody = response.body() val responseBody = response.body()
val contentType = responseBody?.contentType() val contentType = responseBody?.contentType()
if (contentType != null && if (
contentType.type().equals("application", true) && contentType != null &&
contentType.subtype().equals("json", true)) { contentType.type().equals("application", true) &&
contentType.subtype().equals("json", true)
) {
val error = jacksonMapper.readValue<SubsonicResponse>(responseBody.byteStream()) val error = jacksonMapper.readValue<SubsonicResponse>(responseBody.byteStream())
StreamResponse(apiError = error.error, responseHttpCode = response.code()) StreamResponse(apiError = error.error, responseHttpCode = response.code())
} else { } else {
StreamResponse(stream = responseBody?.byteStream(), StreamResponse(
responseHttpCode = response.code()) stream = responseBody?.byteStream(),
responseHttpCode = response.code()
)
} }
} else { } else {
StreamResponse(responseHttpCode = response.code()) StreamResponse(responseHttpCode = response.code())

View File

@ -149,7 +149,7 @@ interface SubsonicAPIDefinition {
@Query("songIdToAdd") songIdsToAdd: List<String>? = null, @Query("songIdToAdd") songIdsToAdd: List<String>? = null,
@Query("songIndexToRemove") songIndexesToRemove: List<Int>? = null @Query("songIndexToRemove") songIndexesToRemove: List<Int>? = null
): ):
Call<SubsonicResponse> Call<SubsonicResponse>
@GET("getPodcasts.view") @GET("getPodcasts.view")
fun getPodcasts( fun getPodcasts(

View File

@ -1,9 +1,9 @@
package org.moire.ultrasonic.api.subsonic.interceptors package org.moire.ultrasonic.api.subsonic.interceptors
import kotlin.LazyThreadSafetyMode.NONE
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Interceptor.Chain import okhttp3.Interceptor.Chain
import okhttp3.Response import okhttp3.Response
import kotlin.LazyThreadSafetyMode.NONE
/** /**
* Adds password param converted to hex string in request url. * Adds password param converted to hex string in request url.
@ -19,7 +19,7 @@ class PasswordHexInterceptor(private val password: String) : Interceptor {
override fun intercept(chain: Chain): Response { override fun intercept(chain: Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
val updatedUrl = originalRequest.url().newBuilder() val updatedUrl = originalRequest.url().newBuilder()
.addEncodedQueryParameter("p", passwordHex).build() .addEncodedQueryParameter("p", passwordHex).build()
return chain.proceed(originalRequest.newBuilder().url(updatedUrl).build()) return chain.proceed(originalRequest.newBuilder().url(updatedUrl).build())
} }
} }

View File

@ -1,12 +1,12 @@
package org.moire.ultrasonic.api.subsonic.interceptors package org.moire.ultrasonic.api.subsonic.interceptors
import okhttp3.Interceptor
import okhttp3.Interceptor.Chain
import okhttp3.Response
import java.math.BigInteger import java.math.BigInteger
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.security.SecureRandom import java.security.SecureRandom
import okhttp3.Interceptor
import okhttp3.Interceptor.Chain
import okhttp3.Response
/** /**
* Adds password param as MD5 hash with random salt. Salt is also added as a param. * Adds password param as MD5 hash with random salt. Salt is also added as a param.
@ -32,9 +32,9 @@ class PasswordMD5Interceptor(private val password: String) : Interceptor {
override fun intercept(chain: Chain): Response { override fun intercept(chain: Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
val updatedUrl = originalRequest.url().newBuilder() val updatedUrl = originalRequest.url().newBuilder()
.addQueryParameter("t", passwordMD5Hash) .addQueryParameter("t", passwordMD5Hash)
.addQueryParameter("s", salt) .addQueryParameter("s", salt)
.build() .build()
return chain.proceed(originalRequest.newBuilder().url(updatedUrl).build()) return chain.proceed(originalRequest.newBuilder().url(updatedUrl).build())
} }

View File

@ -20,10 +20,9 @@ internal class ProxyPasswordInterceptor(
var apiVersion: SubsonicAPIVersions = initialAPIVersions var apiVersion: SubsonicAPIVersions = initialAPIVersions
override fun intercept(chain: Chain): Response = override fun intercept(chain: Chain): Response =
if (apiVersion < SubsonicAPIVersions.V1_13_0 || if (apiVersion < SubsonicAPIVersions.V1_13_0 || forceHexPassword) {
forceHexPassword) { hexInterceptor.intercept(chain)
hexInterceptor.intercept(chain) } else {
} else { mD5Interceptor.intercept(chain)
mD5Interceptor.intercept(chain) }
}
} }

View File

@ -1,9 +1,9 @@
package org.moire.ultrasonic.api.subsonic.interceptors package org.moire.ultrasonic.api.subsonic.interceptors
import java.util.concurrent.TimeUnit.MILLISECONDS
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Interceptor.Chain import okhttp3.Interceptor.Chain
import okhttp3.Response import okhttp3.Response
import java.util.concurrent.TimeUnit.MILLISECONDS
internal const val SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000 internal const val SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000
// Allow 20 seconds extra timeout pear MB offset. // Allow 20 seconds extra timeout pear MB offset.
@ -24,9 +24,11 @@ internal class RangeHeaderInterceptor : Interceptor {
val offsetValue = headers["Range"] ?: "0" val offsetValue = headers["Range"] ?: "0"
val offset = "bytes=$offsetValue-" val offset = "bytes=$offsetValue-"
chain.withReadTimeout(getReadTimeout(offsetValue.toInt()), MILLISECONDS) chain.withReadTimeout(getReadTimeout(offsetValue.toInt()), MILLISECONDS)
.proceed(originalRequest.newBuilder() .proceed(
.removeHeader("Range").addHeader("Range", offset) originalRequest.newBuilder()
.build()) .removeHeader("Range").addHeader("Range", offset)
.build()
)
} else { } else {
chain.proceed(originalRequest) chain.proceed(originalRequest)
} }
@ -37,5 +39,5 @@ internal class RangeHeaderInterceptor : Interceptor {
// on the server. In that case, the server uses a long time before sending any data, // on the server. In that case, the server uses a long time before sending any data,
// causing the client to time out. // causing the client to time out.
private fun getReadTimeout(offset: Int) = private fun getReadTimeout(offset: Int) =
(SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE).toInt() (SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE).toInt()
} }

View File

@ -3,11 +3,11 @@ package org.moire.ultrasonic.api.subsonic.interceptors
import com.fasterxml.jackson.core.JsonFactory import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonToken import com.fasterxml.jackson.core.JsonToken
import java.io.IOException
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Interceptor.Chain import okhttp3.Interceptor.Chain
import okhttp3.Response import okhttp3.Response
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import java.io.IOException
private const val DEFAULT_PEEK_BYTE_COUNT = 1000L private const val DEFAULT_PEEK_BYTE_COUNT = 1000L
@ -29,12 +29,14 @@ internal class VersionInterceptor(
val originalRequest = chain.request() val originalRequest = chain.request()
val newRequest = originalRequest.newBuilder() val newRequest = originalRequest.newBuilder()
.url(originalRequest .url(
.url() originalRequest
.newBuilder() .url()
.addQueryParameter("v", protocolVersion.restApiVersion) .newBuilder()
.build()) .addQueryParameter("v", protocolVersion.restApiVersion)
.build() .build()
)
.build()
val response = chain.proceed(newRequest) val response = chain.proceed(newRequest)
if (response.isSuccessful) { if (response.isSuccessful) {
@ -54,8 +56,10 @@ internal class VersionInterceptor(
val jsonReader = jsonFactory.createParser(content) val jsonReader = jsonFactory.createParser(content)
jsonReader.nextToken() jsonReader.nextToken()
if (jsonReader.currentToken == JsonToken.START_OBJECT) { if (jsonReader.currentToken == JsonToken.START_OBJECT) {
while (jsonReader.currentName != "version" && while (
jsonReader.currentToken != null) { jsonReader.currentName != "version" &&
jsonReader.currentToken != null
) {
jsonReader.nextToken() jsonReader.nextToken()
} }
val versionStr = jsonReader.nextTextValue() val versionStr = jsonReader.nextTextValue()

View File

@ -1,7 +1,7 @@
package org.moire.ultrasonic.api.subsonic.response package org.moire.ultrasonic.api.subsonic.response
import org.moire.ultrasonic.api.subsonic.SubsonicError
import java.io.InputStream import java.io.InputStream
import org.moire.ultrasonic.api.subsonic.SubsonicError
/** /**
* Special response that contains either [stream] of data from api, or [apiError], * Special response that contains either [stream] of data from api, or [apiError],

View File

@ -24,14 +24,16 @@ open class SubsonicResponse(
companion object { companion object {
fun getStatusFromJson(jsonValue: String) = values() fun getStatusFromJson(jsonValue: String) = values()
.filter { it.jsonValue == jsonValue }.firstOrNull() .filter { it.jsonValue == jsonValue }.firstOrNull()
?: throw IllegalArgumentException("Unknown status value: $jsonValue") ?: throw IllegalArgumentException("Unknown status value: $jsonValue")
class StatusJsonDeserializer : JsonDeserializer<Status>() { class StatusJsonDeserializer : JsonDeserializer<Status>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): Status { override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): Status {
if (p.currentName != "status") { if (p.currentName != "status") {
throw JsonParseException(p, throw JsonParseException(
"Current token is not status. Current token name ${p.currentName}.") p,
"Current token is not status. Current token name ${p.currentName}."
)
} }
return getStatusFromJson(p.text) return getStatusFromJson(p.text)
} }

View File

@ -16,8 +16,10 @@ class ProxyPasswordInterceptorTest {
private val mockPasswordMd5Interceptor = mock<PasswordMD5Interceptor>() private val mockPasswordMd5Interceptor = mock<PasswordMD5Interceptor>()
private val mockChain = mock<Chain>() private val mockChain = mock<Chain>()
private val proxyInterceptor = ProxyPasswordInterceptor(V1_12_0, private val proxyInterceptor = ProxyPasswordInterceptor(
mockPasswordHexInterceptor, mockPasswordMd5Interceptor, false) V1_12_0,
mockPasswordHexInterceptor, mockPasswordMd5Interceptor, false
)
@Test @Test
fun `Should use hex password on versions less then 1 13 0`() { fun `Should use hex password on versions less then 1 13 0`() {
@ -37,8 +39,10 @@ class ProxyPasswordInterceptorTest {
@Test @Test
fun `Should use hex password if forceHex is true`() { fun `Should use hex password if forceHex is true`() {
val interceptor = ProxyPasswordInterceptor(V1_16_0, mockPasswordHexInterceptor, val interceptor = ProxyPasswordInterceptor(
mockPasswordMd5Interceptor, true) V1_16_0, mockPasswordHexInterceptor,
mockPasswordMd5Interceptor, true
)
interceptor.intercept(mockChain) interceptor.intercept(mockChain)

View File

@ -11,7 +11,7 @@ class StreamResponseTest {
@Test @Test
fun `Should have error if subsonic error is not null`() { fun `Should have error if subsonic error is not null`() {
StreamResponse(apiError = RequestedDataWasNotFound, responseHttpCode = 200) StreamResponse(apiError = RequestedDataWasNotFound, responseHttpCode = 200)
.hasError() `should be equal to` true .hasError() `should be equal to` true
} }
@Test @Test

View File

@ -1,18 +1,21 @@
ext.versions = [ ext.versions = [
minSdk : 14, minSdk : 14,
targetSdk : 22, targetSdk : 23,
compileSdk : 28, compileSdk : 28,
gradle : '4.6', gradle : '6.5',
androidTools : "3.2.0", androidTools : "4.0.0",
ktlint : "0.28.0", ktlint : "0.37.1",
ktlintGradle : "5.1.0", ktlintGradle : "9.2.1",
detekt : "1.0.0.RC6-4", detekt : "1.0.0.RC6-4",
jacoco : "0.8.2", jacoco : "0.8.5",
androidSupport : "22.2.1", androidSupport : "22.2.1",
androidLegacySupport : "1.0.0",
androidSupportDesign : "1.0.0",
multidex : "2.0.1",
kotlin : "1.2.71", kotlin : "1.3.72",
retrofit : "2.4.0", retrofit : "2.4.0",
jackson : "2.9.5", jackson : "2.9.5",
@ -30,20 +33,22 @@ ext.versions = [
apacheCodecs : "1.10", apacheCodecs : "1.10",
testRunner : "1.0.1", testRunner : "1.0.1",
robolectric : "3.8", robolectric : "3.8",
dexter : "6.1.2",
] ]
ext.gradlePlugins = [ ext.gradlePlugins = [
androidTools : "com.android.tools.build:gradle:$versions.androidTools", androidTools : "com.android.tools.build:gradle:$versions.androidTools",
kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin", kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin",
ktlintGradle : "gradle.plugin.org.jlleitschuh.gradle:ktlint-gradle:$versions.ktlintGradle", ktlintGradle : "org.jlleitschuh.gradle:ktlint-gradle:$versions.ktlintGradle",
detekt : "gradle.plugin.io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$versions.detekt", detekt : "gradle.plugin.io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$versions.detekt",
jacoco : "org.jacoco:org.jacoco.core:$versions.jacoco" jacoco : "org.jacoco:org.jacoco.core:$versions.jacoco"
] ]
ext.androidSupport = [ ext.androidSupport = [
support : "com.android.support:support-v4:$versions.androidSupport", support : "androidx.legacy:legacy-support-v4:$versions.androidLegacySupport",
design : "com.android.support:design:$versions.androidSupport", design : "com.google.android.material:material:$versions.androidSupportDesign",
annotations : "com.android.support:support-annotations:$versions.androidSupport" annotations: "com.android.support:support-annotations:$versions.androidSupport",
multidex : "androidx.multidex:multidex:$versions.multidex",
] ]
ext.other = [ ext.other = [
@ -60,6 +65,7 @@ ext.other = [
koinJava : "org.koin:koin-java:$versions.koin", koinJava : "org.koin:koin-java:$versions.koin",
koinAndroid : "org.koin:koin-android:$versions.koin", koinAndroid : "org.koin:koin-android:$versions.koin",
picasso : "com.squareup.picasso:picasso:$versions.picasso", picasso : "com.squareup.picasso:picasso:$versions.picasso",
dexter : "com.karumi:dexter:$versions.dexter",
] ]
ext.testing = [ ext.testing = [

View File

@ -8,3 +8,5 @@ kotlin.caching.enabled=true
kotlin.incremental.usePreciseJavaTracking=true kotlin.incremental.usePreciseJavaTracking=true
android.enableBuildCache=true android.enableBuildCache=true
android.useAndroidX=true
android.enableJetifier=true

View File

@ -1,5 +1,6 @@
#Sat Jun 13 17:12:11 CEST 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip

View File

@ -12,7 +12,7 @@ android {
compileSdkVersion versions.compileSdk compileSdkVersion versions.compileSdk
defaultConfig { defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
minSdkVersion versions.minSdk minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
} }

View File

@ -31,9 +31,9 @@ tasks.create(name: 'jacocoFullReport', type: JacocoReport, dependsOn: 'jacocoMer
group = "Reporting" group = "Reporting"
description = "Generate full Jacoco coverage report including all modules." description = "Generate full Jacoco coverage report including all modules."
classDirectories = files() getClassDirectories().setFrom(files())
sourceDirectories = files() getSourceDirectories().setFrom(files())
executionData = files() getExecutionData().setFrom(files())
reports { reports {
xml.enabled = true xml.enabled = true

View File

@ -41,9 +41,9 @@ jacocoTestReport {
} }
afterEvaluate { afterEvaluate {
classDirectories = files(classDirectories.files.collect { getClassDirectories().setFrom(files(classDirectories.files.collect {
fileTree(dir: it, excludes: jacocoExclude) fileTree(dir: it, excludes: jacocoExclude)
}) }))
} }
} }

View File

@ -30,6 +30,7 @@ android {
} }
debug { debug {
minifyEnabled false minifyEnabled false
multiDexEnabled true
testCoverageEnabled true testCoverageEnabled true
applicationIdSuffix ".debug" applicationIdSuffix ".debug"
} }
@ -64,6 +65,7 @@ dependencies {
implementation androidSupport.support implementation androidSupport.support
implementation androidSupport.design implementation androidSupport.design
implementation androidSupport.multidex
implementation other.kotlinStdlib implementation other.kotlinStdlib
implementation other.koinAndroid implementation other.koinAndroid
@ -75,6 +77,7 @@ dependencies {
testImplementation testing.kotlinJunit testImplementation testing.kotlinJunit
testImplementation testing.mockitoKotlin testImplementation testing.mockitoKotlin
testImplementation testing.kluent testImplementation testing.kluent
implementation other.dexter
} }
jacoco { jacoco {

View File

@ -1649,7 +1649,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
private void displaySongRating() private void displaySongRating()
{ {
int rating = currentSong.getUserRating() == null ? 0 : currentSong.getUserRating(); int rating = currentSong == null || currentSong.getUserRating() == null ? 0 : currentSong.getUserRating();
fiveStar1ImageView.setImageDrawable(rating > 0 ? fullStar : hollowStar); fiveStar1ImageView.setImageDrawable(rating > 0 ? fullStar : hollowStar);
fiveStar2ImageView.setImageDrawable(rating > 1 ? fullStar : hollowStar); fiveStar2ImageView.setImageDrawable(rating > 1 ? fullStar : hollowStar);
fiveStar3ImageView.setImageDrawable(rating > 2 ? fullStar : hollowStar); fiveStar3ImageView.setImageDrawable(rating > 2 ? fullStar : hollowStar);

View File

@ -21,7 +21,7 @@ package org.moire.ultrasonic.activity;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.ActionBar; import androidx.appcompat.app.ActionBar;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;

View File

@ -2,7 +2,7 @@ package org.moire.ultrasonic.activity;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.support.v7.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Util; import org.moire.ultrasonic.util.Util;

View File

@ -1,9 +1,9 @@
package org.moire.ultrasonic.activity; package org.moire.ultrasonic.activity;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import android.support.v7.app.ActionBar; import androidx.appcompat.app.ActionBar;
import android.support.v7.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import android.view.MenuItem; import android.view.MenuItem;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;

View File

@ -19,7 +19,7 @@
package org.moire.ultrasonic.activity; package org.moire.ultrasonic.activity;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.ActionBar; import androidx.appcompat.app.ActionBar;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.fragment.SettingsFragment; import org.moire.ultrasonic.fragment.SettingsFragment;

View File

@ -30,7 +30,7 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.support.v7.app.ActionBar; import androidx.appcompat.app.ActionBar;
import android.util.Log; import android.util.Log;
import android.view.*; import android.view.*;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;

View File

@ -8,7 +8,7 @@ import android.preference.EditTextPreference;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;

View File

@ -6,7 +6,7 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.*; import android.preference.*;
import android.provider.SearchRecentSuggestions; import android.provider.SearchRecentSuggestions;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import org.koin.java.standalone.KoinJavaComponent; import org.koin.java.standalone.KoinJavaComponent;
@ -382,21 +382,18 @@ public class SettingsFragment extends PreferenceFragment
File dir = new File(path); File dir = new File(path);
if (!FileUtil.ensureDirectoryExistsAndIsReadWritable(dir)) { if (!FileUtil.ensureDirectoryExistsAndIsReadWritable(dir)) {
Util.toast(getActivity(), R.string.settings_cache_location_error, false); PermissionUtil.handlePermissionFailed(getActivity(), new PermissionUtil.PermissionRequestFinishedCallback() {
@Override
// Reset it to the default. public void onPermissionRequestFinished() {
String defaultPath = FileUtil.getDefaultMusicDirectory().getPath(); String currentPath = settings.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory().getPath());
if (!defaultPath.equals(path)) { cacheLocation.setSummary(currentPath);
Util.getPreferences(getActivity()).edit() cacheLocation.setText(currentPath);
.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath) }
.apply(); });
cacheLocation.setSummary(defaultPath);
cacheLocation.setText(defaultPath);
}
// Clear download queue.
DownloadService downloadService = DownloadServiceImpl.getInstance();
downloadService.clear();
} }
// Clear download queue.
DownloadService downloadService = DownloadServiceImpl.getInstance();
downloadService.clear();
} }
} }

View File

@ -36,8 +36,8 @@ import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
import android.os.PowerManager; import android.os.PowerManager;
import android.support.v4.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.RemoteViews; import android.widget.RemoteViews;

View File

@ -339,7 +339,7 @@ public class OfflineMusicService extends RESTMusicService
{ {
try try
{ {
Bitmap bitmap = FileUtil.getAvatarBitmap(username, size, highQuality); Bitmap bitmap = FileUtil.getAvatarBitmap(context, username, size, highQuality);
return Util.scaleBitmap(bitmap, size); return Util.scaleBitmap(bitmap, size);
} }
catch (Exception e) catch (Exception e)

View File

@ -20,9 +20,9 @@ package org.moire.ultrasonic.service;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.support.annotation.NonNull; import androidx.annotation.NonNull;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import android.support.annotation.StringRes; import androidx.annotation.StringRes;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
@ -1013,7 +1013,7 @@ public class RESTMusicService implements MusicService {
synchronized (username) { synchronized (username) {
// Use cached file, if existing. // Use cached file, if existing.
Bitmap bitmap = FileUtil.getAvatarBitmap(username, size, highQuality); Bitmap bitmap = FileUtil.getAvatarBitmap(context, username, size, highQuality);
if (bitmap == null) { if (bitmap == null) {
InputStream in = null; InputStream in = null;
@ -1031,7 +1031,7 @@ public class RESTMusicService implements MusicService {
OutputStream out = null; OutputStream out = null;
try { try {
out = new FileOutputStream(FileUtil.getAvatarFile(username)); out = new FileOutputStream(FileUtil.getAvatarFile(context, username));
out.write(bytes); out.write(bytes);
} finally { } finally {
Util.close(out); Util.close(out);

View File

@ -47,7 +47,7 @@ public class CacheCleaner
catch (Exception ex) catch (Exception ex)
{ {
// If an exception is thrown, assume we execute correctly the next time // If an exception is thrown, assume we execute correctly the next time
Log.w("Exception in CacheCleaner.clean", ex); Log.w(TAG, "Exception in CacheCleaner.clean", ex);
} }
} }
@ -60,7 +60,7 @@ public class CacheCleaner
catch (Exception ex) catch (Exception ex)
{ {
// If an exception is thrown, assume we execute correctly the next time // If an exception is thrown, assume we execute correctly the next time
Log.w("Exception in CacheCleaner.cleanSpace", ex); Log.w(TAG,"Exception in CacheCleaner.cleanSpace", ex);
} }
} }
@ -73,11 +73,11 @@ public class CacheCleaner
catch (Exception ex) catch (Exception ex)
{ {
// If an exception is thrown, assume we execute correctly the next time // If an exception is thrown, assume we execute correctly the next time
Log.w("Exception in CacheCleaner.cleanPlaylists", ex); Log.w(TAG, "Exception in CacheCleaner.cleanPlaylists", ex);
} }
} }
private static void deleteEmptyDirs(Iterable<File> dirs, Collection<File> doNotDelete) private void deleteEmptyDirs(Iterable<File> dirs, Collection<File> doNotDelete)
{ {
for (File dir : dirs) for (File dir : dirs)
{ {
@ -91,9 +91,9 @@ public class CacheCleaner
if (children != null) if (children != null)
{ {
// No songs left in the folder // No songs left in the folder
if (children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath())) if (children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(context, dir).getPath()))
{ {
Util.delete(FileUtil.getAlbumArtFile(dir)); Util.delete(FileUtil.getAlbumArtFile(context, dir));
children = dir.listFiles(); children = dir.listFiles();
} }

View File

@ -21,10 +21,12 @@ package org.moire.ultrasonic.util;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import org.moire.ultrasonic.activity.MainActivity;
import org.moire.ultrasonic.activity.SubsonicTabActivity; import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory;
@ -106,12 +108,12 @@ public class FileUtil
public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry)
{ {
File albumDir = getAlbumDirectory(context, entry); File albumDir = getAlbumDirectory(context, entry);
return getAlbumArtFile(albumDir); return getAlbumArtFile(context, albumDir);
} }
public static File getAvatarFile(String username) public static File getAvatarFile(Context context, String username)
{ {
File albumArtDir = getAlbumArtDirectory(); File albumArtDir = getAlbumArtDirectory(context);
if (albumArtDir == null || username == null) if (albumArtDir == null || username == null)
{ {
@ -122,9 +124,9 @@ public class FileUtil
return new File(albumArtDir, String.format("%s.jpeg", md5Hex)); return new File(albumArtDir, String.format("%s.jpeg", md5Hex));
} }
public static File getAlbumArtFile(File albumDir) public static File getAlbumArtFile(Context context, File albumDir)
{ {
File albumArtDir = getAlbumArtDirectory(); File albumArtDir = getAlbumArtDirectory(context);
if (albumArtDir == null || albumDir == null) if (albumArtDir == null || albumDir == null)
{ {
@ -135,11 +137,11 @@ public class FileUtil
return new File(albumArtDir, String.format("%s.jpeg", md5Hex)); return new File(albumArtDir, String.format("%s.jpeg", md5Hex));
} }
public static Bitmap getAvatarBitmap(String username, int size, boolean highQuality) public static Bitmap getAvatarBitmap(Context context, String username, int size, boolean highQuality)
{ {
if (username == null) return null; if (username == null) return null;
File avatarFile = getAvatarFile(username); File avatarFile = getAvatarFile(context, username);
SubsonicTabActivity subsonicTabActivity = SubsonicTabActivity.getInstance(); SubsonicTabActivity subsonicTabActivity = SubsonicTabActivity.getInstance();
Bitmap bitmap = null; Bitmap bitmap = null;
@ -299,7 +301,7 @@ public class FileUtil
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt); return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt);
} }
public static File getAlbumArtDirectory() public static File getAlbumArtDirectory(Context context)
{ {
File albumArtDir = new File(getUltraSonicDirectory(), "artwork"); File albumArtDir = new File(getUltraSonicDirectory(), "artwork");
ensureDirectoryExistsAndIsReadWritable(albumArtDir); ensureDirectoryExistsAndIsReadWritable(albumArtDir);
@ -316,10 +318,13 @@ public class FileUtil
File dir; File dir;
if (!TextUtils.isEmpty(entry.getPath())) { if (!TextUtils.isEmpty(entry.getPath()))
{
File f = new File(fileSystemSafeDir(entry.getPath())); File f = new File(fileSystemSafeDir(entry.getPath()));
dir = new File(String.format("%s/%s", getMusicDirectory(context).getPath(), entry.isDirectory() ? f.getPath() : f.getParent())); dir = new File(String.format("%s/%s", getMusicDirectory(context).getPath(), entry.isDirectory() ? f.getPath() : f.getParent()));
} else { }
else
{
String artist = fileSystemSafe(entry.getArtist()); String artist = fileSystemSafe(entry.getArtist());
String album = fileSystemSafe(entry.getAlbum()); String album = fileSystemSafe(entry.getAlbum());
@ -360,7 +365,11 @@ public class FileUtil
public static File getUltraSonicDirectory() public static File getUltraSonicDirectory()
{ {
return new File(Environment.getExternalStorageDirectory(), "Android/data/org.moire.ultrasonic"); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
return new File(Environment.getExternalStorageDirectory(), "Android/data/org.moire.ultrasonic");
// After Android M, the location of the files must be queried differently. GetExternalFilesDir will always return a directory which Ultrasonic can access without any extra privileges.
return MainActivity.getInstance().getExternalFilesDir(null);
} }
public static File getDefaultMusicDirectory() public static File getDefaultMusicDirectory()
@ -372,7 +381,11 @@ public class FileUtil
{ {
String path = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, DEFAULT_MUSIC_DIR.getPath()); String path = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, DEFAULT_MUSIC_DIR.getPath());
File dir = new File(path); File dir = new File(path);
return ensureDirectoryExistsAndIsReadWritable(dir) ? dir : DEFAULT_MUSIC_DIR;
boolean hasAccess = ensureDirectoryExistsAndIsReadWritable(dir);
if (hasAccess == false) PermissionUtil.handlePermissionFailed(context, null);
return hasAccess ? dir : DEFAULT_MUSIC_DIR;
} }
public static boolean ensureDirectoryExistsAndIsReadWritable(File dir) public static boolean ensureDirectoryExistsAndIsReadWritable(File dir)
@ -406,13 +419,13 @@ public class FileUtil
if (!dir.canRead()) if (!dir.canRead())
{ {
Log.w(TAG, String.format("No read permission for directory %s", dir)); Log.w(TAG, String.format("No read permission for directory %s", dir));
return false; return false;
} }
if (!dir.canWrite()) if (!dir.canWrite())
{ {
Log.w(TAG, String.format("No write permission for directory %s", dir)); Log.w(TAG, String.format("No write permission for directory %s", dir));
return false; return false;
} }
return true; return true;

View File

@ -427,6 +427,11 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
? musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) ? musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null)
: musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null); : musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null);
if (bitmap == null) {
Log.d(TAG, "Found empty album art.");
return;
}
if (isAvatar) if (isAvatar)
addImageToCache(bitmap, username, size); addImageToCache(bitmap, username, size);
else else

View File

@ -0,0 +1,167 @@
package org.moire.ultrasonic.util;
import android.Manifest;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.PermissionChecker;
import com.karumi.dexter.Dexter;
import com.karumi.dexter.MultiplePermissionsReport;
import com.karumi.dexter.PermissionToken;
import com.karumi.dexter.listener.DexterError;
import com.karumi.dexter.listener.PermissionRequest;
import com.karumi.dexter.listener.PermissionRequestErrorListener;
import com.karumi.dexter.listener.multi.MultiplePermissionsListener;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.MainActivity;
import java.util.List;
import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
/**
* Contains static functions for Permission handling
*/
public class PermissionUtil {
private static final String TAG = FileUtil.class.getSimpleName();
public interface PermissionRequestFinishedCallback {
void onPermissionRequestFinished();
}
/**
* This function can be used to handle file access permission failures.
*
* It will check if the failure is because the necessary permissions aren't available,
* and it will request them, if necessary.
*
* @param context context for the operation
* @param callback callback function to execute after the permission request is finished
*/
public static void handlePermissionFailed(final Context context, final PermissionRequestFinishedCallback callback) {
String currentCachePath = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory().getPath());
String defaultCachePath = FileUtil.getDefaultMusicDirectory().getPath();
// Ultrasonic can do nothing about this error when the Music Directory is already set to the default.
if (currentCachePath.compareTo(defaultCachePath) == 0) return;
// We must get the context of the Main Activity for the dialogs, as this function may be called from a background thread where displaying dialogs is not available
Context mainContext = MainActivity.getInstance();
if ((PermissionChecker.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_DENIED) ||
(PermissionChecker.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PERMISSION_DENIED)) {
// While we request permission, the Music Directory is temporarily reset to its default location
setCacheLocation(context, FileUtil.getDefaultMusicDirectory().getPath());
requestPermission(mainContext, currentCachePath, callback);
} else {
setCacheLocation(context, FileUtil.getDefaultMusicDirectory().getPath());
showWarning(mainContext, context.getString(R.string.permissions_message_box_title), context.getString(R.string.permissions_access_error), null);
callback.onPermissionRequestFinished();
}
}
private static void setCacheLocation(Context context, String cacheLocation) {
Util.getPreferences(context).edit()
.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, cacheLocation)
.apply();
}
private static void requestPermission(final Context context, final String cacheLocation, final PermissionRequestFinishedCallback callback) {
Dexter.withContext(context)
.withPermissions(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE)
.withListener(new MultiplePermissionsListener() {
@Override
public void onPermissionsChecked(MultiplePermissionsReport report) {
if (report.areAllPermissionsGranted()) {
Log.i(TAG, String.format("Permission granted to use cache directory %s", cacheLocation));
setCacheLocation(context, cacheLocation);
if (callback != null) callback.onPermissionRequestFinished();
return;
}
if (report.isAnyPermissionPermanentlyDenied()) {
Log.i(TAG, String.format("Found permanently denied permission to use cache directory %s, offering settings", cacheLocation));
showSettingsDialog(context);
if (callback != null) callback.onPermissionRequestFinished();
return;
}
Log.i(TAG, String.format("At least one permission is missing to use directory %s ", cacheLocation));
setCacheLocation(context, FileUtil.getDefaultMusicDirectory().getPath());
showWarning(context, context.getString(R.string.permissions_message_box_title),
context.getString(R.string.permissions_permission_missing), null);
if (callback != null) callback.onPermissionRequestFinished();
}
@Override
public void onPermissionRationaleShouldBeShown(List<PermissionRequest> permissions, PermissionToken token) {
showWarning(context, context.getString(R.string.permissions_rationale_title),
context.getString(R.string.permissions_rationale_description), token);
}
}).withErrorListener(new PermissionRequestErrorListener() {
@Override
public void onError(DexterError error) {
Log.e(TAG, String.format("An error has occurred during checking permissions with Dexter: %s", error.toString()));
}
})
.check();
}
private static void showSettingsDialog(final Context context) {
AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Dialog);
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setTitle(context.getString(R.string.permissions_permanent_denial_title));
builder.setMessage(context.getString(R.string.permissions_permanent_denial_description));
builder.setPositiveButton(context.getString(R.string.permissions_open_settings), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
openSettings(context);
}
});
builder.setNegativeButton(context.getString(R.string.common_cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
setCacheLocation(context, FileUtil.getDefaultMusicDirectory().getPath());
dialog.cancel();
}
});
builder.show();
}
private static void openSettings(Context context) {
Intent i = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
i.addCategory(Intent.CATEGORY_DEFAULT);
i.setData(Uri.parse("package:" + context.getPackageName()));
context.startActivity(i);
}
private static void showWarning(Context context, String title, String text, final PermissionToken token) {
AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Dialog);
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setTitle(title);
builder.setMessage(text);
builder.setPositiveButton(context.getString(R.string.common_ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
if (token != null) token.continuePermissionRequest();
}
});
builder.show();
}
}

View File

@ -882,6 +882,7 @@ public class Util extends DownloadActivity
public static Bitmap scaleBitmap(Bitmap bitmap, int size) public static Bitmap scaleBitmap(Bitmap bitmap, int size)
{ {
if (bitmap == null) return null;
return Bitmap.createScaledBitmap(bitmap, size, getScaledHeight(bitmap, size), true); return Bitmap.createScaledBitmap(bitmap, size, getScaledHeight(bitmap, size), true);
} }

View File

@ -19,8 +19,8 @@
package org.moire.ultrasonic.view; package org.moire.ultrasonic.view;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import androidx.annotation.NonNull;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;

View File

@ -6,7 +6,9 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
public class AutoRepeatButton extends ImageView import androidx.appcompat.widget.AppCompatImageView;
public class AutoRepeatButton extends AppCompatImageView
{ {
private long initialRepeatDelay = 1000; private long initialRepeatDelay = 1000;

View File

@ -19,8 +19,8 @@
package org.moire.ultrasonic.view; package org.moire.ultrasonic.view;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import androidx.annotation.NonNull;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;

Some files were not shown because too many files have changed in this diff Show More