Add initial implementation of image loader.

Currently it only supports loading cover art images from network.

Signed-off-by: Yahor Berdnikau <egorr.berd@gmail.com>
This commit is contained in:
Yahor Berdnikau 2018-06-26 21:11:39 +02:00
parent e4e962faa0
commit 74591571bf
12 changed files with 260 additions and 1 deletions

View File

@ -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",
]

View File

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

View File

@ -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
// }
//}

View File

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

View File

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

View File

@ -0,0 +1,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)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

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

View File

@ -0,0 +1,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)
}
}
}

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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