diff --git a/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandlerTest.kt b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandlerTest.kt new file mode 100644 index 00000000..4dbc5f33 --- /dev/null +++ b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandlerTest.kt @@ -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() + 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() +} diff --git a/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt index 09927850..6572028d 100644 --- a/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt +++ b/subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreatorTest.kt @@ -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) + } } diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt new file mode 100644 index 00000000..afce254e --- /dev/null +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt @@ -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) + } + } +} diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt index 7d898437..8c6f4cd3 100644 --- a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt @@ -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()) { diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt index eb300fe5..9cecb7e3 100644 --- a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/RequestCreator.kt @@ -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() diff --git a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt index 0f73ae2f..630bbc4a 100644 --- a/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt +++ b/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/SubsonicImageLoader.kt @@ -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 + ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt index 83fb4d83..4ed49943 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/SubsonicImageLoaderProxy.kt @@ -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) + } + } }