Add loading user avatars.
Signed-off-by: Yahor Berdnikau <egorr.berd@gmail.com>
This commit is contained in:
parent
c0b6500b47
commit
02467cb05b
|
@ -0,0 +1,73 @@
|
|||
package org.moire.ultrasonic.subsonic.loader.image
|
||||
|
||||
import android.net.Uri
|
||||
import com.nhaarman.mockito_kotlin.any
|
||||
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 org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE)
|
||||
class AvatarRequestHandlerTest {
|
||||
private val mockSubsonicApiClient = mock<SubsonicAPIClient>()
|
||||
private val handler = AvatarRequestHandler(mockSubsonicApiClient)
|
||||
|
||||
@Test
|
||||
fun `Should accept only cover art request`() {
|
||||
val requestUri = createLoadAvatarRequest("some-username")
|
||||
|
||||
handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should not accept random request uri`() {
|
||||
val requestUri = Uri.Builder()
|
||||
.scheme(SCHEME)
|
||||
.authority(AUTHORITY)
|
||||
.appendPath("something")
|
||||
.build()
|
||||
|
||||
handler.canHandleRequest(requestUri.buildRequest()) shouldEqualTo false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should fail loading if uri doesn't contain username`() {
|
||||
var requestUri = createLoadAvatarRequest("some-username")
|
||||
requestUri = requestUri.buildUpon().clearQuery().build()
|
||||
|
||||
val fail = {
|
||||
handler.load(requestUri.buildRequest(), 0)
|
||||
}
|
||||
|
||||
fail `should throw` IllegalStateException::class
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should load avatar from network`() {
|
||||
val streamResponse = StreamResponse(
|
||||
loadResourceStream("Big_Buck_Bunny.jpeg"),
|
||||
apiError = null,
|
||||
responseHttpCode = 200
|
||||
)
|
||||
whenever(mockSubsonicApiClient.getAvatar(any()))
|
||||
.thenReturn(streamResponse)
|
||||
|
||||
val response = handler.load(createLoadAvatarRequest("some-username").buildRequest(), 0)
|
||||
|
||||
response.loadedFrom `should equal` Picasso.LoadedFrom.NETWORK
|
||||
response.source `should not be` null
|
||||
}
|
||||
|
||||
private fun Uri.buildRequest() = Request.Builder(this).build()
|
||||
}
|
|
@ -11,8 +11,16 @@ 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")
|
||||
val expectedUri = Uri.parse("$SCHEME://$AUTHORITY/$COVER_ART_PATH?$QUERY_ID=$entityId")
|
||||
|
||||
createLoadCoverArtRequest(entityId).compareTo(expectedUri).shouldEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should create valid avatar request`() {
|
||||
val username = "some-username"
|
||||
val expectedUri = Uri.parse("$SCHEME://$AUTHORITY/$AVATAR_PATH?$QUERY_USERNAME=$username")
|
||||
|
||||
createLoadAvatarRequest(username).compareTo(expectedUri).shouldEqualTo(0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package org.moire.ultrasonic.subsonic.loader.image
|
||||
|
||||
import com.squareup.picasso.Picasso
|
||||
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 avatars from subsonic api.
|
||||
*/
|
||||
class AvatarRequestHandler(
|
||||
private val apiClient: SubsonicAPIClient
|
||||
) : RequestHandler() {
|
||||
override fun canHandleRequest(data: Request): Boolean {
|
||||
return with(data.uri) {
|
||||
scheme == SCHEME &&
|
||||
authority == AUTHORITY &&
|
||||
path == "/$AVATAR_PATH"
|
||||
}
|
||||
}
|
||||
|
||||
override fun load(request: Request, networkPolicy: Int): Result {
|
||||
val username = request.uri.getQueryParameter(QUERY_USERNAME)
|
||||
|
||||
val response = apiClient.getAvatar(username)
|
||||
if (response.hasError()) {
|
||||
throw IOException("${response.apiError}")
|
||||
} else {
|
||||
return Result(Okio.source(response.stream), Picasso.LoadedFrom.NETWORK)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request
|
|||
}
|
||||
|
||||
override fun load(request: Request, networkPolicy: Int): Result {
|
||||
val id = request.uri.getQueryParameter("id")
|
||||
val id = request.uri.getQueryParameter(QUERY_ID)
|
||||
|
||||
val response = apiClient.getCoverArt(id)
|
||||
if (response.hasError()) {
|
||||
|
|
|
@ -5,10 +5,20 @@ 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 const val AVATAR_PATH = "avatar"
|
||||
internal const val QUERY_ID = "id"
|
||||
internal const val QUERY_USERNAME = "username"
|
||||
|
||||
internal fun createLoadCoverArtRequest(entityId: String): Uri = Uri.Builder()
|
||||
.scheme(SCHEME)
|
||||
.authority(AUTHORITY)
|
||||
.appendPath(COVER_ART_PATH)
|
||||
.appendQueryParameter("id", entityId)
|
||||
.appendQueryParameter(QUERY_ID, entityId)
|
||||
.build()
|
||||
|
||||
internal fun createLoadAvatarRequest(username: String): Uri = Uri.Builder()
|
||||
.scheme(SCHEME)
|
||||
.authority(AUTHORITY)
|
||||
.appendPath(AVATAR_PATH)
|
||||
.appendQueryParameter(QUERY_USERNAME, username)
|
||||
.build()
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.moire.ultrasonic.subsonic.loader.image
|
|||
import android.content.Context
|
||||
import android.widget.ImageView
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.squareup.picasso.RequestCreator
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||
|
||||
class SubsonicImageLoader(
|
||||
|
@ -10,27 +11,44 @@ class SubsonicImageLoader(
|
|||
apiClient: SubsonicAPIClient
|
||||
) {
|
||||
private val picasso = Picasso.Builder(context)
|
||||
.addRequestHandler(CoverArtRequestHandler(apiClient))
|
||||
.build().apply { setIndicatorsEnabled(BuildConfig.DEBUG) }
|
||||
.addRequestHandler(CoverArtRequestHandler(apiClient))
|
||||
.addRequestHandler(AvatarRequestHandler(apiClient))
|
||||
.build().apply { setIndicatorsEnabled(BuildConfig.DEBUG) }
|
||||
|
||||
fun load(request: ImageRequest) = when (request) {
|
||||
is ImageRequest.CoverArt -> loadCoverArt(request)
|
||||
is ImageRequest.Avatar -> loadAvatar(request)
|
||||
}
|
||||
|
||||
private fun loadCoverArt(request: ImageRequest.CoverArt) {
|
||||
picasso.load(createLoadCoverArtRequest(request.entityId))
|
||||
.apply {
|
||||
if (request.placeHolderDrawableRes != null) {
|
||||
placeholder(request.placeHolderDrawableRes)
|
||||
}
|
||||
}
|
||||
.apply {
|
||||
if (request.errorDrawableRes != null) {
|
||||
error(request.errorDrawableRes)
|
||||
}
|
||||
}
|
||||
.addPlaceholder(request)
|
||||
.addError(request)
|
||||
.into(request.imageView)
|
||||
}
|
||||
|
||||
private fun loadAvatar(request: ImageRequest.Avatar) {
|
||||
picasso.load(createLoadAvatarRequest(request.username))
|
||||
.addPlaceholder(request)
|
||||
.addError(request)
|
||||
.into(request.imageView)
|
||||
}
|
||||
|
||||
private fun RequestCreator.addPlaceholder(request: ImageRequest): RequestCreator {
|
||||
if (request.placeHolderDrawableRes != null) {
|
||||
placeholder(request.placeHolderDrawableRes)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun RequestCreator.addError(request: ImageRequest): RequestCreator {
|
||||
if (request.errorDrawableRes != null) {
|
||||
error(request.errorDrawableRes)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ImageRequest(
|
||||
|
@ -48,4 +66,15 @@ sealed class ImageRequest(
|
|||
errorDrawableRes,
|
||||
imageView
|
||||
)
|
||||
|
||||
class Avatar(
|
||||
val username: String,
|
||||
imageView: ImageView,
|
||||
placeHolderDrawableRes: Int? = null,
|
||||
errorDrawableRes: Int? = null
|
||||
) : ImageRequest(
|
||||
placeHolderDrawableRes,
|
||||
errorDrawableRes,
|
||||
imageView
|
||||
)
|
||||
}
|
||||
|
|
|
@ -41,4 +41,25 @@ class SubsonicImageLoaderProxy(
|
|||
subsonicImageLoader.load(request)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadAvatarImage(
|
||||
view: View?,
|
||||
username: String?,
|
||||
large: Boolean,
|
||||
size: Int,
|
||||
crossFade: Boolean,
|
||||
highQuality: Boolean
|
||||
) {
|
||||
if (username != null &&
|
||||
view != null &&
|
||||
view is ImageView) {
|
||||
val request = ImageRequest.Avatar(
|
||||
username,
|
||||
view,
|
||||
placeHolderDrawableRes = R.drawable.ic_contact_picture,
|
||||
errorDrawableRes = R.drawable.ic_contact_picture
|
||||
)
|
||||
subsonicImageLoader.load(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue