From 02467cb05beb4aab40a8cc773720344a44b41f7d Mon Sep 17 00:00:00 2001
From: Yahor Berdnikau <egorr.berd@gmail.com>
Date: Sat, 14 Jul 2018 20:55:45 +0200
Subject: [PATCH] Add loading user avatars.

Signed-off-by: Yahor Berdnikau <egorr.berd@gmail.com>
---
 .../loader/image/AvatarRequestHandlerTest.kt  | 73 +++++++++++++++++++
 .../loader/image/RequestCreatorTest.kt        | 10 ++-
 .../loader/image/AvatarRequestHandler.kt      | 34 +++++++++
 .../loader/image/CoverArtRequestHandler.kt    |  2 +-
 .../subsonic/loader/image/RequestCreator.kt   | 12 ++-
 .../loader/image/SubsonicImageLoader.kt       | 53 +++++++++++---
 .../subsonic/SubsonicImageLoaderProxy.kt      | 21 ++++++
 7 files changed, 190 insertions(+), 15 deletions(-)
 create mode 100644 subsonic-api-image-loader/src/integrationTest/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandlerTest.kt
 create mode 100644 subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt

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