mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-02 18:26:49 +01:00
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:
parent
e4e962faa0
commit
74591571bf
@ -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",
|
||||
]
|
||||
|
@ -1,6 +1,7 @@
|
||||
include ':library'
|
||||
include ':domain'
|
||||
include ':subsonic-api'
|
||||
include ':subsonic-api-image-loader'
|
||||
include ':cache'
|
||||
include ':menudrawer'
|
||||
include ':pulltorefresh'
|
||||
|
66
subsonic-api-image-loader/build.gradle
Normal file
66
subsonic-api-image-loader/build.gradle
Normal 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
|
||||
// }
|
||||
//}
|
@ -0,0 +1,9 @@
|
||||
package org.moire.ultrasonic.subsonic.loader.image
|
||||
|
||||
import okio.Okio
|
||||
import java.io.InputStream
|
||||
|
||||
fun Any.loadResourceStream(name: String): InputStream {
|
||||
val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
|
||||
return source.inputStream()
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package org.moire.ultrasonic.subsonic.loader.image
|
||||
|
||||
import android.net.Uri
|
||||
import com.nhaarman.mockito_kotlin.any
|
||||
import com.nhaarman.mockito_kotlin.anyOrNull
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.squareup.picasso.Request
|
||||
import org.amshove.kluent.`should equal`
|
||||
import org.amshove.kluent.`should not be`
|
||||
import org.amshove.kluent.`should throw`
|
||||
import org.amshove.kluent.shouldEqualTo
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import java.io.IOException
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class CoverArtRequestHandlerTest {
|
||||
private val mockSubsonicApiClientMock = mock<SubsonicAPIClient>()
|
||||
private val handler = CoverArtRequestHandler(mockSubsonicApiClientMock)
|
||||
|
||||
@Test
|
||||
fun `Should accept only cover art request`() {
|
||||
val requestUri = createLoadCoverArtRequest("some-id")
|
||||
|
||||
handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should not accept random request uri`() {
|
||||
val requestUri = Uri.Builder()
|
||||
.scheme(SCHEME)
|
||||
.authority(AUTHORITY)
|
||||
.appendPath("random")
|
||||
.build()
|
||||
|
||||
handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should fail loading if uri doesn't contain id`() {
|
||||
var requestUri = createLoadCoverArtRequest("some-id")
|
||||
requestUri = requestUri.buildUpon().clearQuery().build()
|
||||
|
||||
val fail = {
|
||||
handler.load(requestUri.buildRequest(), 0)
|
||||
}
|
||||
|
||||
fail `should throw` IllegalStateException::class
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should throw IOException when request to api failed`() {
|
||||
val streamResponse = StreamResponse(null, null, 500)
|
||||
whenever(mockSubsonicApiClientMock.getCoverArt(any(), anyOrNull()))
|
||||
.thenReturn(streamResponse)
|
||||
|
||||
val fail = {
|
||||
handler.load(createLoadCoverArtRequest("some").buildRequest(), 0)
|
||||
}
|
||||
|
||||
fail `should throw` IOException::class
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should load bitmap from network`() {
|
||||
val streamResponse = StreamResponse(
|
||||
loadResourceStream("Big_Buck_Bunny.jpeg"),
|
||||
apiError = null,
|
||||
responseHttpCode = 200
|
||||
)
|
||||
whenever(mockSubsonicApiClientMock.getCoverArt(any(), anyOrNull()))
|
||||
.thenReturn(streamResponse)
|
||||
|
||||
val response = handler.load(createLoadCoverArtRequest("some").buildRequest(), 0)
|
||||
|
||||
response.loadedFrom `should equal` Picasso.LoadedFrom.NETWORK
|
||||
response.source `should not be` null
|
||||
}
|
||||
|
||||
private fun Uri.buildRequest() = Request.Builder(this).build()
|
||||
}
|
@ -0,0 +1,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 |
4
subsonic-api-image-loader/src/main/AndroidManifest.xml
Normal file
4
subsonic-api-image-loader/src/main/AndroidManifest.xml
Normal 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>
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user