From 74591571bfe07bc8ee22533f55531288e32b673f Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Tue, 26 Jun 2018 21:11:39 +0200 Subject: [PATCH] Add initial implementation of image loader. Currently it only supports loading cover art images from network. Signed-off-by: Yahor Berdnikau --- dependencies.gradle | 10 +- settings.gradle | 1 + subsonic-api-image-loader/build.gradle | 66 ++++++++++++++ .../subsonic/loader/image/CommonFunctions.kt | 9 ++ .../image/CoverArtRequestHandlerTest.kt | 86 ++++++++++++++++++ .../loader/image/RequestCreatorTest.kt | 18 ++++ .../resources/Big_Buck_Bunny.jpeg | Bin 0 -> 10452 bytes .../src/main/AndroidManifest.xml | 4 + .../loader/image/CoverArtRequestHandler.kt | 32 +++++++ .../subsonic/loader/image/RequestCreator.kt | 14 +++ .../loader/image/SubsonicImageLoader.kt | 20 ++++ ultrasonic/build.gradle | 1 + 12 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 subsonic-api-image-loader/build.gradle create mode 100644 subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CommonFunctions.kt create mode 100644 subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandlerTest.kt create mode 100644 subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt create mode 100644 subsonic-api-image-loader/src/integrationTest/resources/Big_Buck_Bunny.jpeg create mode 100644 subsonic-api-image-loader/src/main/AndroidManifest.xml create mode 100644 subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt create mode 100644 subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt create mode 100644 subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt diff --git a/dependencies.gradle b/dependencies.gradle index 437b99a1..4282a4f8 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -21,12 +21,15 @@ ext.versions = [ semver : "1.0.0", twitterSerial : "0.1.6", koin : "0.9.3", + picasso : "2.71828", junit : "4.12", mockito : "2.16.0", mockitoKotlin : "1.5.0", kluent : "1.35", apacheCodecs : "1.10", + testRunner : "1.0.1", + robolectric : "3.8", ] ext.gradlePlugins = [ @@ -40,6 +43,7 @@ ext.gradlePlugins = [ ext.androidSupport = [ support : "com.android.support:support-v4:$versions.androidSupport", design : "com.android.support:design:$versions.androidSupport", + annotations : "com.android.support:support-annotations:$versions.androidSupport" ] ext.other = [ @@ -53,7 +57,8 @@ ext.other = [ semver : "net.swiftzer.semver:semver:$versions.semver", twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial", koinCore : "org.koin:koin-core:$versions.koin", - koinAndroid : "org.koin:koin-android:$versions.koin" + koinAndroid : "org.koin:koin-android:$versions.koin", + picasso : "com.squareup.picasso:picasso:$versions.picasso", ] ext.testing = [ @@ -63,6 +68,9 @@ ext.testing = [ mockito : "org.mockito:mockito-core:$versions.mockito", mockitoInline : "org.mockito:mockito-inline:$versions.mockito", kluent : "org.amshove.kluent:kluent:$versions.kluent", + kluentAndroid : "org.amshove.kluent:kluent-android:$versions.kluent", mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp", apacheCodecs : "commons-codec:commons-codec:$versions.apacheCodecs", + testRunner : "com.android.support.test:runner:$versions.testRunner", + robolectric : "org.robolectric:robolectric:$versions.robolectric", ] diff --git a/settings.gradle b/settings.gradle index 256032c5..eb222b3d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ include ':library' include ':domain' include ':subsonic-api' +include ':subsonic-api-image-loader' include ':cache' include ':menudrawer' include ':pulltorefresh' diff --git a/subsonic-api-image-loader/build.gradle b/subsonic-api-image-loader/build.gradle new file mode 100644 index 00000000..7fc23d69 --- /dev/null +++ b/subsonic-api-image-loader/build.gradle @@ -0,0 +1,66 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +//apply plugin: 'jacoco' +apply from: '../gradle_scripts/code_quality.gradle' + +android { + compileSdkVersion(versions.compileSdk) + + defaultConfig { + minSdkVersion(versions.minSdk) + targetSdkVersion(versions.targetSdk) + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + test.java.srcDirs += "${projectDir}/src/integrationTest/kotlin" + test.resources.srcDirs += "${projectDir}/src/integrationTest/resources" + } +} + +dependencies { + api project(':domain') + api project(':subsonic-api') + api other.kotlinStdlib + api other.picasso + + testImplementation testing.junit + testImplementation testing.kotlinJunit + testImplementation testing.mockito + testImplementation testing.mockitoInline + testImplementation testing.mockitoKotlin + testImplementation testing.kluent + testImplementation testing.robolectric +} + +jacoco { + toolVersion(versions.jacoco) +} + +//ext { +// jacocoExclude = [] +//} + +//jacocoTestReport { +// reports { +// html.enabled true +// csv.enabled false +// xml.enabled true +// } +// +// afterEvaluate { +// classDirectories = files(classDirectories.files.collect { +// fileTree(dir: it, excludes: jacocoExclude) +// }) +// } +//} +// +//test.finalizedBy jacocoTestReport +//test { +// jacoco { +// excludes += jacocoExclude +// } +//} diff --git a/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CommonFunctions.kt b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CommonFunctions.kt new file mode 100644 index 00000000..5c4be754 --- /dev/null +++ b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CommonFunctions.kt @@ -0,0 +1,9 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import okio.Okio +import java.io.InputStream + +fun Any.loadResourceStream(name: String): InputStream { + val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name))) + return source.inputStream() +} diff --git a/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandlerTest.kt b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandlerTest.kt new file mode 100644 index 00000000..a4d74733 --- /dev/null +++ b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandlerTest.kt @@ -0,0 +1,86 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import android.net.Uri +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.anyOrNull +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.whenever +import com.squareup.picasso.Picasso +import com.squareup.picasso.Request +import org.amshove.kluent.`should equal` +import org.amshove.kluent.`should not be` +import org.amshove.kluent.`should throw` +import org.amshove.kluent.shouldEqualTo +import org.junit.Test +import org.junit.runner.RunWith +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.api.subsonic.response.StreamResponse +import org.robolectric.RobolectricTestRunner +import java.io.IOException + +@RunWith(RobolectricTestRunner::class) +class CoverArtRequestHandlerTest { + private val mockSubsonicApiClientMock = mock() + private val handler = CoverArtRequestHandler(mockSubsonicApiClientMock) + + @Test + fun `Should accept only cover art request`() { + val requestUri = createLoadCoverArtRequest("some-id") + + handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo true + } + + @Test + fun `Should not accept random request uri`() { + val requestUri = Uri.Builder() + .scheme(SCHEME) + .authority(AUTHORITY) + .appendPath("random") + .build() + + handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo false + } + + @Test + fun `Should fail loading if uri doesn't contain id`() { + var requestUri = createLoadCoverArtRequest("some-id") + requestUri = requestUri.buildUpon().clearQuery().build() + + val fail = { + handler.load(requestUri.buildRequest(), 0) + } + + fail `should throw` IllegalStateException::class + } + + @Test + fun `Should throw IOException when request to api failed`() { + val streamResponse = StreamResponse(null, null, 500) + whenever(mockSubsonicApiClientMock.getCoverArt(any(), anyOrNull())) + .thenReturn(streamResponse) + + val fail = { + handler.load(createLoadCoverArtRequest("some").buildRequest(), 0) + } + + fail `should throw` IOException::class + } + + @Test + fun `Should load bitmap from network`() { + val streamResponse = StreamResponse( + loadResourceStream("Big_Buck_Bunny.jpeg"), + apiError = null, + responseHttpCode = 200 + ) + whenever(mockSubsonicApiClientMock.getCoverArt(any(), anyOrNull())) + .thenReturn(streamResponse) + + val response = handler.load(createLoadCoverArtRequest("some").buildRequest(), 0) + + response.loadedFrom `should equal` Picasso.LoadedFrom.NETWORK + response.source `should not be` null + } + + private fun Uri.buildRequest() = Request.Builder(this).build() +} diff --git a/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt new file mode 100644 index 00000000..09927850 --- /dev/null +++ b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt @@ -0,0 +1,18 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import android.net.Uri +import org.amshove.kluent.shouldEqualTo +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class RequestCreatorTest { + @Test + fun `Should create valid load cover art request`() { + val entityId = "299" + val expectedUri = Uri.parse("$SCHEME://$AUTHORITY/$COVER_ART_PATH?id=$entityId") + + createLoadCoverArtRequest(entityId).compareTo(expectedUri).shouldEqualTo(0) + } +} diff --git a/subsonic-api-image-loader/src/integrationTest/resources/Big_Buck_Bunny.jpeg b/subsonic-api-image-loader/src/integrationTest/resources/Big_Buck_Bunny.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..758dfa64294976541eb73221aa5a6d43b6c456c2 GIT binary patch literal 10452 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 + + diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt new file mode 100644 index 00000000..7d898437 --- /dev/null +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt @@ -0,0 +1,32 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import com.squareup.picasso.Picasso.LoadedFrom.NETWORK +import com.squareup.picasso.Request +import com.squareup.picasso.RequestHandler +import okio.Okio +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import java.io.IOException + +/** + * Loads cover arts from subsonic api. + */ +class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : RequestHandler() { + override fun canHandleRequest(data: Request): Boolean { + return with(data.uri) { + scheme == SCHEME && + authority == AUTHORITY && + path == "/$COVER_ART_PATH" + } + } + + override fun load(request: Request, networkPolicy: Int): Result { + val id = request.uri.getQueryParameter("id") + + val response = apiClient.getCoverArt(id) + if (response.hasError()) { + throw IOException("${response.apiError}") + } else { + return Result(Okio.source(response.stream), NETWORK) + } + } +} diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt new file mode 100644 index 00000000..eb300fe5 --- /dev/null +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt @@ -0,0 +1,14 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import android.net.Uri + +internal const val SCHEME = "subsonic_api" +internal const val AUTHORITY = BuildConfig.APPLICATION_ID +internal const val COVER_ART_PATH = "cover_art" + +internal fun createLoadCoverArtRequest(entityId: String): Uri = Uri.Builder() + .scheme(SCHEME) + .authority(AUTHORITY) + .appendPath(COVER_ART_PATH) + .appendQueryParameter("id", entityId) + .build() diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt new file mode 100644 index 00000000..6a3225b4 --- /dev/null +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt @@ -0,0 +1,20 @@ +package org.moire.ultrasonic.subsonic.loader.image + +import android.content.Context +import android.widget.ImageView +import com.squareup.picasso.Picasso +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient + +class SubsonicImageLoader( + private val context: Context, + apiClient: SubsonicAPIClient +) { + private val picasso = Picasso.Builder(context) + .addRequestHandler(CoverArtRequestHandler(apiClient)) + .build().apply { setIndicatorsEnabled(BuildConfig.DEBUG) } + + fun loadCoverArt(entityId: String, view: ImageView) { + picasso.load(createLoadCoverArtRequest(entityId)) + .into(view) + } +} diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index bf224674..c2d49505 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -55,6 +55,7 @@ dependencies { implementation project(':library') implementation project(':domain') implementation project(':subsonic-api') + implementation project(':subsonic-api-image-loader') implementation project(':cache') implementation androidSupport.support