Add loading user avatars.

Signed-off-by: Yahor Berdnikau <egorr.berd@gmail.com>
This commit is contained in:
Yahor Berdnikau 2018-07-14 20:55:45 +02:00
parent c0b6500b47
commit 02467cb05b
7 changed files with 190 additions and 15 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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