mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-09 00:08:43 +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",
|
semver : "1.0.0",
|
||||||
twitterSerial : "0.1.6",
|
twitterSerial : "0.1.6",
|
||||||
koin : "0.9.3",
|
koin : "0.9.3",
|
||||||
|
picasso : "2.71828",
|
||||||
|
|
||||||
junit : "4.12",
|
junit : "4.12",
|
||||||
mockito : "2.16.0",
|
mockito : "2.16.0",
|
||||||
mockitoKotlin : "1.5.0",
|
mockitoKotlin : "1.5.0",
|
||||||
kluent : "1.35",
|
kluent : "1.35",
|
||||||
apacheCodecs : "1.10",
|
apacheCodecs : "1.10",
|
||||||
|
testRunner : "1.0.1",
|
||||||
|
robolectric : "3.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
ext.gradlePlugins = [
|
ext.gradlePlugins = [
|
||||||
@ -40,6 +43,7 @@ ext.gradlePlugins = [
|
|||||||
ext.androidSupport = [
|
ext.androidSupport = [
|
||||||
support : "com.android.support:support-v4:$versions.androidSupport",
|
support : "com.android.support:support-v4:$versions.androidSupport",
|
||||||
design : "com.android.support:design:$versions.androidSupport",
|
design : "com.android.support:design:$versions.androidSupport",
|
||||||
|
annotations : "com.android.support:support-annotations:$versions.androidSupport"
|
||||||
]
|
]
|
||||||
|
|
||||||
ext.other = [
|
ext.other = [
|
||||||
@ -53,7 +57,8 @@ ext.other = [
|
|||||||
semver : "net.swiftzer.semver:semver:$versions.semver",
|
semver : "net.swiftzer.semver:semver:$versions.semver",
|
||||||
twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial",
|
twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial",
|
||||||
koinCore : "org.koin:koin-core:$versions.koin",
|
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 = [
|
ext.testing = [
|
||||||
@ -63,6 +68,9 @@ ext.testing = [
|
|||||||
mockito : "org.mockito:mockito-core:$versions.mockito",
|
mockito : "org.mockito:mockito-core:$versions.mockito",
|
||||||
mockitoInline : "org.mockito:mockito-inline:$versions.mockito",
|
mockitoInline : "org.mockito:mockito-inline:$versions.mockito",
|
||||||
kluent : "org.amshove.kluent:kluent:$versions.kluent",
|
kluent : "org.amshove.kluent:kluent:$versions.kluent",
|
||||||
|
kluentAndroid : "org.amshove.kluent:kluent-android:$versions.kluent",
|
||||||
mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp",
|
mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp",
|
||||||
apacheCodecs : "commons-codec:commons-codec:$versions.apacheCodecs",
|
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 ':library'
|
||||||
include ':domain'
|
include ':domain'
|
||||||
include ':subsonic-api'
|
include ':subsonic-api'
|
||||||
|
include ':subsonic-api-image-loader'
|
||||||
include ':cache'
|
include ':cache'
|
||||||
include ':menudrawer'
|
include ':menudrawer'
|
||||||
include ':pulltorefresh'
|
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(':library')
|
||||||
implementation project(':domain')
|
implementation project(':domain')
|
||||||
implementation project(':subsonic-api')
|
implementation project(':subsonic-api')
|
||||||
|
implementation project(':subsonic-api-image-loader')
|
||||||
implementation project(':cache')
|
implementation project(':cache')
|
||||||
|
|
||||||
implementation androidSupport.support
|
implementation androidSupport.support
|
||||||
|
Loading…
x
Reference in New Issue
Block a user