From aca31b8c98cfca626d61bde0882ffd418507902a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Tue, 28 Nov 2023 14:07:32 +0100 Subject: [PATCH] Refactoring service image (#25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description The goal of this PR is to enhance the integrator experience with image url handling. The new api allow to write with ease image url like the SRG SSR required. ### Examples ```kotlin val media: Media = ... media.imageUrl.url(ImageWidth.W1920) media.imageUrl.url(ImageWidth.W1920, IlHost.TEST) val chapter: Chapter = ... chapter.imageUrl.url(ImageSize.MEDIUM) val defaultDecorator = DefaultImageUrlDecorator(IlHost.PROD) // or custom decorator media.imageUrl.url(defaultDecorator, ImageSize.MEDIUM) ``` ## Changes - `ImageUrl.rawUrl` is private now - Add `ImageUrlDecorator` to decorate image url with a width - Remove `ImageProvider` replaced by `DefaultImageUrlDecorator` --------- Co-authored-by: Loïc Dumas Co-authored-by: Gaëtan Muller --- buildSrc/src/main/kotlin/Config.kt | 4 +- .../integrationlayer/data/IlImage.kt | 37 ------- .../integrationlayer/data/ImageUrl.kt | 27 +++-- .../data/ImageUrlDecorator.kt | 15 +++ .../TestDefaultImageUrlDecorator.kt | 46 ++++++++ .../TestIlHostImageUrlDecorator.kt | 73 +++++++++++++ .../integrationlayer/TestIlUrn.java | 4 +- .../TestScaleWidthImageUrlDecorator.kt | 37 +++++++ .../integrationlayer/request/ImageProvider.kt | 101 ------------------ .../request/image/DefaultImageUrlDecorator.kt | 31 ++++++ .../request/image/IlHostImageUrlDecorator.kt | 37 +++++++ .../request/image/ImageSize.kt | 22 ++++ .../request/image/ImageUrlExtension.kt | 36 +++++++ .../request/image/ImageWidth.kt | 62 +++++++++++ .../image/ScaleWidthImageUrlDecorator.kt | 23 ++++ .../integrationlayer/TestImageSize.kt | 24 +++++ .../integrationlayer/TestImageWidth.kt | 98 +++++++++++++++++ 17 files changed, 524 insertions(+), 153 deletions(-) delete mode 100644 data/src/main/java/ch/srg/dataProvider/integrationlayer/data/IlImage.kt create mode 100644 data/src/main/java/ch/srg/dataProvider/integrationlayer/data/ImageUrlDecorator.kt create mode 100644 dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestDefaultImageUrlDecorator.kt create mode 100644 dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestIlHostImageUrlDecorator.kt create mode 100644 dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestScaleWidthImageUrlDecorator.kt delete mode 100644 dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/ImageProvider.kt create mode 100644 dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/DefaultImageUrlDecorator.kt create mode 100644 dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/IlHostImageUrlDecorator.kt create mode 100644 dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ImageSize.kt create mode 100644 dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ImageUrlExtension.kt create mode 100644 dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ImageWidth.kt create mode 100644 dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ScaleWidthImageUrlDecorator.kt create mode 100644 dataprovider-retrofit/src/test/java/ch/srg/dataProvider/integrationlayer/TestImageSize.kt create mode 100644 dataprovider-retrofit/src/test/java/ch/srg/dataProvider/integrationlayer/TestImageWidth.kt diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 02d646d..2964694 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -4,8 +4,8 @@ object Config { const val minSdk = 21 const val major = 0 - const val minor = 6 - const val patch = 3 + const val minor = 7 + const val patch = 2 const val versionName = "$major.$minor.$patch" const val maven_group = "ch.srg.data.provider" diff --git a/data/src/main/java/ch/srg/dataProvider/integrationlayer/data/IlImage.kt b/data/src/main/java/ch/srg/dataProvider/integrationlayer/data/IlImage.kt deleted file mode 100644 index 1307f55..0000000 --- a/data/src/main/java/ch/srg/dataProvider/integrationlayer/data/IlImage.kt +++ /dev/null @@ -1,37 +0,0 @@ -package ch.srg.dataProvider.integrationlayer.data - -/** - * Copyright (c) SRG SSR. All rights reserved. - * - * - * License information is available from the LICENSE file. - */ -data class IlImage @JvmOverloads constructor(val url: String) { - - @Suppress("MagicNumber") - enum class Size(val sizePixels: Int) { - W240(240), W320(320), W480(480), W960(960), W1920(1920); - - companion object { - fun getClosest(pixels: Int): Size { - if (pixels >= W1920.sizePixels) { - return W1920 - } - if (pixels <= W240.sizePixels) { - return W240 - } - val sizes = values() - var closestSize = 0 - var minDist = Int.MAX_VALUE - for (i in sizes.indices) { - val dist = Math.abs(sizes[i].sizePixels - pixels) - if (dist <= minDist) { - minDist = dist - closestSize = i - } - } - return sizes[closestSize] - } - } - } -} diff --git a/data/src/main/java/ch/srg/dataProvider/integrationlayer/data/ImageUrl.kt b/data/src/main/java/ch/srg/dataProvider/integrationlayer/data/ImageUrl.kt index 17a3b62..b2b6714 100644 --- a/data/src/main/java/ch/srg/dataProvider/integrationlayer/data/ImageUrl.kt +++ b/data/src/main/java/ch/srg/dataProvider/integrationlayer/data/ImageUrl.kt @@ -1,3 +1,7 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ @file:Suppress("MemberVisibilityCanBePrivate") package ch.srg.dataProvider.integrationlayer.data @@ -6,10 +10,9 @@ import ch.srg.dataProvider.integrationlayer.data.serializer.ImageUrlSerializer import java.io.Serializable /** - * Copyright (c) SRG SSR. All rights reserved. - * + * Image url * - * License information is available from the LICENSE file. + * @property rawUrl Internal image url, to retrieve the url use [ImageUrl.decorated]. */ @Suppress("SerialVersionUIDInSerializableClass") @kotlinx.serialization.Serializable(with = ImageUrlSerializer::class) @@ -19,15 +22,17 @@ data class ImageUrl( * * @return the undecorated url */ - val rawUrl: String + internal val rawUrl: String ) : Serializable { - @JvmOverloads - fun getIlImage(): IlImage { - return IlImage(rawUrl) - } - - override fun toString(): String { - return rawUrl + /** + * Decorated + * + * @param decorator The [ImageUrlDecorator] used to decorate the [rawUrl]. + * @param widthPixels The width of the image. + * @return The decorated [rawUrl]. + */ + fun decorated(decorator: ImageUrlDecorator, widthPixels: Int): String { + return decorator.decorate(rawUrl, widthPixels) } } diff --git a/data/src/main/java/ch/srg/dataProvider/integrationlayer/data/ImageUrlDecorator.kt b/data/src/main/java/ch/srg/dataProvider/integrationlayer/data/ImageUrlDecorator.kt new file mode 100644 index 0000000..4562f08 --- /dev/null +++ b/data/src/main/java/ch/srg/dataProvider/integrationlayer/data/ImageUrlDecorator.kt @@ -0,0 +1,15 @@ +package ch.srg.dataProvider.integrationlayer.data + +/** + * Image url decorator + */ +interface ImageUrlDecorator { + /** + * Decorate [sourceUrl] with [widthPixels]. + * + * @param sourceUrl The source url. + * @param widthPixels The width size in pixels. + * @return decorated url. + */ + fun decorate(sourceUrl: String, widthPixels: Int): String +} diff --git a/dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestDefaultImageUrlDecorator.kt b/dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestDefaultImageUrlDecorator.kt new file mode 100644 index 0000000..fcc073e --- /dev/null +++ b/dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestDefaultImageUrlDecorator.kt @@ -0,0 +1,46 @@ +package ch.srg.dataProvider.integrationlayer + +import android.net.Uri +import ch.srg.dataProvider.integrationlayer.data.ImageUrl +import ch.srg.dataProvider.integrationlayer.request.IlHost +import ch.srg.dataProvider.integrationlayer.request.image.DefaultImageUrlDecorator +import org.junit.Assert +import org.junit.Test + +class TestDefaultImageUrlDecorator { + private val decorator = DefaultImageUrlDecorator(ilHost = IlHost.PROD) + + @Test + fun testNonRtsUrl() { + val input = ImageUrl("https://ws.srf.ch/asset/image/audio/123") + val encodedInput = Uri.encode("https://ws.srf.ch/asset/image/audio/123") + val expected = "https://il.srgssr.ch/images/?imageUrl=${encodedInput}&format=webp&width=480" + Assert.assertEquals(expected, input.decorated(decorator, 480)) + } + + @Test + fun testRtsUrlWithoutImage() { + val input = ImageUrl("https://ws.rts.ch/asset/image/audio/123") + val encodedInput = Uri.encode("https://ws.rts.ch/asset/image/audio/123") + val expected = "https://il.srgssr.ch/images/?imageUrl=${encodedInput}&format=webp&width=480" + Assert.assertEquals(expected, input.decorated(decorator, 480)) + } + + @Test + fun testUrlWithImageOnly() { + val input = ImageUrl("https://ws.srf.ch/asset/image/audio/123.image") + val encodedInput = Uri.encode("https://ws.srf.ch/asset/image/audio/123.image") + val expected = "https://il.srgssr.ch/images/?imageUrl=${encodedInput}&format=webp&width=480" + Assert.assertEquals(expected, input.decorated(decorator, 480)) + } + + @Test + fun testRtsUrlWithImage() { + val input = ImageUrl("https://ws.rts.ch/asset/image/audio/123.image") + val expected = "https://ws.rts.ch/asset/image/audio/123.image/scale/width/460" + Assert.assertEquals(expected, input.decorated(decorator, 460)) + } + + + +} diff --git a/dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestIlHostImageUrlDecorator.kt b/dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestIlHostImageUrlDecorator.kt new file mode 100644 index 0000000..ab1e9a8 --- /dev/null +++ b/dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestIlHostImageUrlDecorator.kt @@ -0,0 +1,73 @@ +package ch.srg.dataProvider.integrationlayer + +import android.net.Uri +import ch.srg.dataProvider.integrationlayer.data.ImageUrl +import ch.srg.dataProvider.integrationlayer.request.IlHost +import ch.srg.dataProvider.integrationlayer.request.image.IlHostImageUrlDecorator +import ch.srg.dataProvider.integrationlayer.request.image.ImageSize +import ch.srg.dataProvider.integrationlayer.request.image.ImageWidth +import ch.srg.dataProvider.integrationlayer.request.image.url +import ch.srg.dataProvider.integrationlayer.request.image.decorated +import org.junit.Assert.assertEquals +import org.junit.Test + +class TestIlHostImageUrlDecorator { + + private val decorator = IlHostImageUrlDecorator(ilHost = IlHost.PROD) + + @Test + fun testPixelValid() { + val input = ImageUrl("https://ws.srf.ch/asset/image/audio/123") + val encodedInput = Uri.encode("https://ws.srf.ch/asset/image/audio/123") + val expected = "https://il.srgssr.ch/images/?imageUrl=${encodedInput}&format=webp&width=480" + assertEquals(expected, input.decorated(decorator, 480)) + } + + @Test + fun testPixelWidthInvalid() { + val input = ImageUrl("https://ws.srf.ch/asset/image/audio/123") + val encodedInput = Uri.encode("https://ws.srf.ch/asset/image/audio/123") + val expected = "https://il.srgssr.ch/images/?imageUrl=${encodedInput}&format=webp&width=480" + assertEquals(expected, input.decorated(decorator, 460)) + } + + @Test + fun testImageSize() { + val input = ImageUrl("https://ws.srf.ch/asset/image/audio/123") + val encodedInput = Uri.encode("https://ws.srf.ch/asset/image/audio/123") + val expected = "https://il.srgssr.ch/images/?imageUrl=${encodedInput}&format=webp&width=480" + assertEquals(expected, input.decorated(decorator, ImageSize.MEDIUM)) + } + + @Test + fun testImageWidth() { + val input = ImageUrl("https://ws.srf.ch/asset/image/audio/123") + val encodedInput = Uri.encode("https://ws.srf.ch/asset/image/audio/123") + val expected = "https://il.srgssr.ch/images/?imageUrl=${encodedInput}&format=webp&width=1920" + assertEquals(expected, input.url(decorator, ImageWidth.W1920)) + } + + @Test + fun testOtherIlHost() { + val input = ImageUrl("https://ws.srf.ch/asset/image/audio/123") + val encodedInput = Uri.encode("https://ws.srf.ch/asset/image/audio/123") + val expected = "https://il-stage.srgssr.ch/images/?imageUrl=${encodedInput}&format=webp&width=1920" + assertEquals(expected, input.url(decorator = IlHostImageUrlDecorator(IlHost.STAGE), width = ImageWidth.W1920)) + } + + @Test + fun testExtensionImageWidthWithIlHost() { + val input = ImageUrl("https://ws.srf.ch/asset/image/audio/123") + val encodedInput = Uri.encode("https://ws.srf.ch/asset/image/audio/123") + val expected = "https://il-stage.srgssr.ch/images/?imageUrl=${encodedInput}&format=webp&width=1920" + assertEquals(expected, input.decorated(ilHost = IlHost.STAGE, width = ImageWidth.W1920)) + } + + @Test + fun testExtensionImageSizeWithIlHost() { + val input = ImageUrl("https://ws.srf.ch/asset/image/audio/123") + val encodedInput = Uri.encode("https://ws.srf.ch/asset/image/audio/123") + val expected = "https://il-test.srgssr.ch/images/?imageUrl=${encodedInput}&format=webp&width=480" + assertEquals(expected, input.decorated(ilHost = IlHost.TEST, imageSize = ImageSize.MEDIUM)) + } +} diff --git a/dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestIlUrn.java b/dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestIlUrn.java index 3ab8223..6f17b2c 100644 --- a/dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestIlUrn.java +++ b/dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestIlUrn.java @@ -40,7 +40,7 @@ public void createFromValidUrn() { } @Test - public void testIsAudioValideUrn() { + public void testIsAudioValidUrn() { Assert.assertFalse(IlUrn.isAudio("urn:rts:video:123456")); Assert.assertTrue(IlUrn.isAudio("urn:rts:audio:123456")); Assert.assertFalse(IlUrn.isAudio("urn:a:b:12345")); @@ -58,7 +58,7 @@ public void testIsAudioNullUrn() { @Test - public void testIsVideoValideUrn() { + public void testIsVideoValidUrn() { Assert.assertTrue(IlUrn.isVideo("urn:rts:video:123456")); Assert.assertFalse(IlUrn.isVideo("urn:rts:audio:123456")); Assert.assertFalse(IlUrn.isVideo("urn:a:b:12345")); diff --git a/dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestScaleWidthImageUrlDecorator.kt b/dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestScaleWidthImageUrlDecorator.kt new file mode 100644 index 0000000..436117f --- /dev/null +++ b/dataprovider-retrofit/src/androidTest/java/ch/srg/dataProvider/integrationlayer/TestScaleWidthImageUrlDecorator.kt @@ -0,0 +1,37 @@ +package ch.srg.dataProvider.integrationlayer + +import ch.srg.dataProvider.integrationlayer.data.ImageUrl +import ch.srg.dataProvider.integrationlayer.request.image.ImageSize +import ch.srg.dataProvider.integrationlayer.request.image.ImageWidth +import ch.srg.dataProvider.integrationlayer.request.image.ScaleWidthImageUrlDecorator +import ch.srg.dataProvider.integrationlayer.request.image.url +import ch.srg.dataProvider.integrationlayer.request.image.decorated +import org.junit.Assert.assertEquals +import org.junit.Test + +class TestScaleWidthImageUrlDecorator { + + private val decorator = ScaleWidthImageUrlDecorator + + @Test + fun testScaleWidth() { + val input = ImageUrl("https://www.data.com/images/images.png") + val width = 458 + val expected = "https://www.data.com/images/images.png/scale/width/458" + assertEquals(expected, input.decorated(decorator, width)) + } + + @Test + fun testScaleWidthImageSize() { + val input = ImageUrl("https://www.data.com/images/images.png") + val expected = "https://www.data.com/images/images.png/scale/width/480" + assertEquals(expected, input.decorated(decorator, ImageSize.MEDIUM)) + } + + @Test + fun testScaleWidthImageWidth() { + val input = ImageUrl("https://www.data.com/images/images.png") + val expected = "https://www.data.com/images/images.png/scale/width/1920" + assertEquals(expected, input.url(decorator, ImageWidth.W1920)) + } +} diff --git a/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/ImageProvider.kt b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/ImageProvider.kt deleted file mode 100644 index 41ca10f..0000000 --- a/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/ImageProvider.kt +++ /dev/null @@ -1,101 +0,0 @@ -package ch.srg.dataProvider.integrationlayer.request - -import android.net.Uri -import android.text.TextUtils -import androidx.annotation.IntDef -import ch.srg.dataProvider.integrationlayer.SRGUrlFactory -import ch.srg.dataProvider.integrationlayer.data.IlImage - -/** - * Copyright (c) SRG SSR. All rights reserved. - * - * - * License information is available from the LICENSE file. - */ -class ImageProvider(factory: SRGUrlFactory) { - @IntDef(SIZE_SMALL, SIZE_MEDIUM, SIZE_LARGE) - annotation class ImageSize - - private val srgImageServiceUri: Uri - - init { - srgImageServiceUri = factory.hostUri.buildUpon().appendEncodedPath("images/").build() - } - - fun decorateImageWithSize(image: IlImage, @ImageSize size: Int): Uri? { - return decorateImageWithSize(image, getImageSize(size)) - } - - fun decorateImageWithSize(image: IlImage, size: IlImage.Size): Uri? { - return decorateImageWithSizeInPixel(image, size.sizePixels) - } - - /** - * @param widthInPixels 160,240,320,480,640,960,1280,1920 - */ - private fun decorateImageWithSizeInPixel(image: IlImage, widthInPixels: Int): Uri? { - return decorateImageUrlWithSizeInPixel(image.url, widthInPixels) - } - - fun decorateImageUrlWithSize(imageUrl: String, @ImageSize size: Int): Uri? { - return decorateImageUrlWithSize(imageUrl, getImageSize(size)) - } - - fun decorateImageUrlWithSize(imageUrl: String, size: IlImage.Size): Uri? { - return decorateImageUrlWithSizeInPixel(imageUrl, size.sizePixels) - } - - /** - * Fixme https://github.com/SRGSSR/srgdataprovider-apple/issues/47 once RTS image service is well connected to Il Play image service. - * - * @param widthInPixels 160,240,320,480,640,960,1280,1920 - */ - private fun decorateImageUrlWithSizeInPixel(imageUrl: String, widthInPixels: Int): Uri? { - return if (TextUtils.isEmpty(imageUrl)) { - null - } else { - if (imageUrl.contains("rts.ch") && imageUrl.contains(".image")) { - return createBusinessUnitImageServiceUrl(imageUrl, widthInPixels) - } else { - return createPlaySrgImageServiceUrl(imageUrl, widthInPixels) - } - } - } - - private fun createPlaySrgImageServiceUrl(imageUrl: String?, width: Int): Uri { - return srgImageServiceUri.buildUpon() - .appendQueryParameter("imageUrl", imageUrl) - .appendQueryParameter("format", FORMAT_WEBP) - .appendQueryParameter("width", width.toString()) - .build() - } - - private fun addScaleWith(uri: Uri.Builder, width: Int): Uri.Builder { - return uri.appendPath(Scale).appendPath(Width).appendPath(width.toString()) - } - - private fun createBusinessUnitImageServiceUrl(url: String?, width: Int): Uri { - return addScaleWith(Uri.parse(url).buildUpon(), width).build() - } - - private fun getImageSize(@ImageSize size: Int): IlImage.Size { - return DIMENSIONS_PX[size] - } - - companion object { - const val SIZE_SMALL = 0 - const val SIZE_MEDIUM = 1 - const val SIZE_LARGE = 2 - - /** - * Dimension to use for each size depending of screen density - * (SRGImageSizeSmall) : @(SRGImageWidth320), - * (SRGImageSizeMedium) : @(SRGImageWidth480), - * (SRGImageSizeLarge) : @(SRGImageWidth960) - */ - private val DIMENSIONS_PX = arrayOf(IlImage.Size.W320, IlImage.Size.W480, IlImage.Size.W960) - private const val Scale = "scale" - private const val Width = "width" - private const val FORMAT_WEBP = "webp" // webp, jpg, png - } -} diff --git a/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/DefaultImageUrlDecorator.kt b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/DefaultImageUrlDecorator.kt new file mode 100644 index 0000000..ac209c8 --- /dev/null +++ b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/DefaultImageUrlDecorator.kt @@ -0,0 +1,31 @@ +package ch.srg.dataProvider.integrationlayer.request.image + +import ch.srg.dataProvider.integrationlayer.data.ImageUrlDecorator +import ch.srg.dataProvider.integrationlayer.request.IlHost + +/** + * Copyright (c) SRG SSR. All rights reserved. + * + * + * License information is available from the LICENSE file. + */ + +/** + * Default image url decorator + * + * For specific RTS image url, the old [ScaleWidthImageUrlDecorator] is used, but it should be fixed sooner or later. + * * + * @param ilHost The [IlHost] to use with [ilHostImageUrlDecorator]. + */ +class DefaultImageUrlDecorator(ilHost: IlHost = IlHost.PROD) : ImageUrlDecorator { + private val ilHostImageUrlDecorator = IlHostImageUrlDecorator(ilHost) + + override fun decorate(imageUrl: String, widthPixels: Int): String { + // FIXME https://github.com/SRGSSR/srgdataprovider-apple/issues/47 once RTS image service is well connected to Il Play image service. + return if (imageUrl.contains("rts.ch") && imageUrl.contains(".image")) { + ScaleWidthImageUrlDecorator.decorate(imageUrl, widthPixels) + } else { + ilHostImageUrlDecorator.decorate(imageUrl, widthPixels) + } + } +} diff --git a/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/IlHostImageUrlDecorator.kt b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/IlHostImageUrlDecorator.kt new file mode 100644 index 0000000..185b337 --- /dev/null +++ b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/IlHostImageUrlDecorator.kt @@ -0,0 +1,37 @@ +package ch.srg.dataProvider.integrationlayer.request.image + +import android.net.Uri +import ch.srg.dataProvider.integrationlayer.data.ImageUrlDecorator +import ch.srg.dataProvider.integrationlayer.request.IlHost + +/** + * Il host image url decorator + * + * @param ilHost The [IlHost] of the integration layer image service. + */ +class IlHostImageUrlDecorator(ilHost: IlHost) : ImageUrlDecorator { + private val imageServiceUri: Uri + + init { + imageServiceUri = ilHost.hostUri.buildUpon().appendEncodedPath(IMAGES_SEGMENT).build() + } + + override fun decorate(sourceUrl: String, widthPixels: Int): String { + // Il image service only support a limited image size! + val imageWidth = ImageWidth.getFromPixels(widthPixels) + return imageServiceUri.buildUpon() + .appendQueryParameter(PARAM_IMAGE_URL, sourceUrl) + .appendQueryParameter(PARAM_FORMAT, FORMAT_WEBP) + .appendQueryParameter(PARAM_WIDTH, imageWidth.widthPixels.toString()) + .build() + .toString() + } + + companion object { + private const val FORMAT_WEBP = "webp" // webp, jpg, png + private const val IMAGES_SEGMENT = "images/" + private const val PARAM_IMAGE_URL = "imageUrl" + private const val PARAM_FORMAT = "format" + private const val PARAM_WIDTH = "width" + } +} diff --git a/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ImageSize.kt b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ImageSize.kt new file mode 100644 index 0000000..c33a06a --- /dev/null +++ b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ImageSize.kt @@ -0,0 +1,22 @@ +package ch.srg.dataProvider.integrationlayer.request.image + +/** + * Image size + * @property width [ImageWidth]. + */ +enum class ImageSize(val width: ImageWidth) { + /** + * Small [ImageWidth.W320] + */ + SMALL(ImageWidth.W320), + + /** + * Medium [ImageWidth.W480] + */ + MEDIUM(ImageWidth.W480), + + /** + * Large [ImageWidth.W960] + */ + LARGE(ImageWidth.W960), +} diff --git a/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ImageUrlExtension.kt b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ImageUrlExtension.kt new file mode 100644 index 0000000..88c7477 --- /dev/null +++ b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ImageUrlExtension.kt @@ -0,0 +1,36 @@ +package ch.srg.dataProvider.integrationlayer.request.image + +import ch.srg.dataProvider.integrationlayer.data.ImageUrl +import ch.srg.dataProvider.integrationlayer.data.ImageUrlDecorator +import ch.srg.dataProvider.integrationlayer.request.IlHost + +fun ImageUrl.decorated(widthPixels: Int, ilHost: IlHost = IlHost.PROD): String { + return decorated(ImageUrlDecoratorInstances.getOrCreate(ilHost), widthPixels) +} + +fun ImageUrl.decorated(width: ImageWidth, ilHost: IlHost = IlHost.PROD): String { + return decorated(widthPixels = width.widthPixels, ilHost = ilHost) +} + +fun ImageUrl.decorated(imageSize: ImageSize, ilHost: IlHost = IlHost.PROD): String { + return decorated(width = imageSize.width, ilHost = ilHost) +} + +fun ImageUrl.decorated(decorator: ImageUrlDecorator, width: ImageWidth): String { + return decorated(decorator, width.widthPixels) +} + +fun ImageUrl.decorated(decorator: ImageUrlDecorator, imageSize: ImageSize): String { + return decorated(decorator, imageSize.width) +} + +/** + * Optimization for extensions to reuse DefaultImageUrlDecorator based on IlHost. + */ +private object ImageUrlDecoratorInstances { + private val instances = mutableMapOf() + + fun getOrCreate(ilHost: IlHost): ImageUrlDecorator { + return instances.getOrPut(ilHost) { DefaultImageUrlDecorator(ilHost) } + } +} diff --git a/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ImageWidth.kt b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ImageWidth.kt new file mode 100644 index 0000000..c1c38c8 --- /dev/null +++ b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ImageWidth.kt @@ -0,0 +1,62 @@ +package ch.srg.dataProvider.integrationlayer.request.image + +import kotlin.math.abs + +/** + * Image width supported by the integration layer + * + * @property widthPixels The width in pixels. + */ +@Suppress("MagicNumber") +enum class ImageWidth(val widthPixels: Int) { + W240(240), + W320(320), + W480(480), + W960(960), + W1920(1920), + ; + + companion object { + + /** + * Get the [ImageWidth] that matches [pixels] or the closest one. + * + * @param pixels The width in pixels. + */ + fun getFromPixels(pixels: Int): ImageWidth { + return when (pixels) { + W240.widthPixels -> W240 + W320.widthPixels -> W320 + W480.widthPixels -> W480 + W960.widthPixels -> W960 + W1920.widthPixels -> W1920 + else -> getClosest(pixels) + } + } + + /** + * Get closest [ImageWidth] + * + * @param widthPixels The width in pixels to get the closest [ImageWidth]. + */ + private fun getClosest(widthPixels: Int): ImageWidth { + if (widthPixels >= W1920.widthPixels) { + return W1920 + } + if (widthPixels <= W240.widthPixels) { + return W240 + } + val sizes = entries + var closestSize = 0 + var minDist = Int.MAX_VALUE + for (i in sizes.indices) { + val dist = abs(sizes[i].widthPixels - widthPixels) + if (dist <= minDist) { + minDist = dist + closestSize = i + } + } + return sizes[closestSize] + } + } +} diff --git a/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ScaleWidthImageUrlDecorator.kt b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ScaleWidthImageUrlDecorator.kt new file mode 100644 index 0000000..074c5cb --- /dev/null +++ b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/image/ScaleWidthImageUrlDecorator.kt @@ -0,0 +1,23 @@ +package ch.srg.dataProvider.integrationlayer.request.image + +import android.net.Uri +import ch.srg.dataProvider.integrationlayer.data.ImageUrlDecorator + +/** + * Scale width image url decorator + * + * @constructor Create empty Scale width image url decorator + */ +object ScaleWidthImageUrlDecorator : ImageUrlDecorator { + private const val Scale = "scale" + private const val Width = "width" + + override fun decorate(sourceUrl: String, widthPixels: Int): String { + return Uri.parse(sourceUrl).buildUpon() + .appendPath(Scale) + .appendPath(Width) + .appendPath(widthPixels.toString()) + .build() + .toString() + } +} diff --git a/dataprovider-retrofit/src/test/java/ch/srg/dataProvider/integrationlayer/TestImageSize.kt b/dataprovider-retrofit/src/test/java/ch/srg/dataProvider/integrationlayer/TestImageSize.kt new file mode 100644 index 0000000..ce9251d --- /dev/null +++ b/dataprovider-retrofit/src/test/java/ch/srg/dataProvider/integrationlayer/TestImageSize.kt @@ -0,0 +1,24 @@ +package ch.srg.dataProvider.integrationlayer + +import ch.srg.dataProvider.integrationlayer.request.image.ImageSize +import ch.srg.dataProvider.integrationlayer.request.image.ImageWidth +import org.junit.Assert +import org.junit.Test + +class TestImageSize { + + @Test + fun testSmallSizeIsW320() { + Assert.assertEquals(ImageWidth.W320, ImageSize.SMALL.width) + } + + @Test + fun testMediumSizeIsW480() { + Assert.assertEquals(ImageWidth.W480, ImageSize.MEDIUM.width) + } + + @Test + fun testLargeSizeIsW960() { + Assert.assertEquals(ImageWidth.W960, ImageSize.LARGE.width) + } +} diff --git a/dataprovider-retrofit/src/test/java/ch/srg/dataProvider/integrationlayer/TestImageWidth.kt b/dataprovider-retrofit/src/test/java/ch/srg/dataProvider/integrationlayer/TestImageWidth.kt new file mode 100644 index 0000000..25f9cc6 --- /dev/null +++ b/dataprovider-retrofit/src/test/java/ch/srg/dataProvider/integrationlayer/TestImageWidth.kt @@ -0,0 +1,98 @@ +package ch.srg.dataProvider.integrationlayer + +import ch.srg.dataProvider.integrationlayer.request.image.ImageWidth +import org.junit.Assert.assertEquals +import org.junit.Test + +class TestImageWidth { + + @Test + fun testGetFromPixelMatchingWidth() { + assertEquals(ImageWidth.W240, ImageWidth.getFromPixels(240)) + assertEquals(ImageWidth.W320, ImageWidth.getFromPixels(320)) + assertEquals(ImageWidth.W480, ImageWidth.getFromPixels(480)) + assertEquals(ImageWidth.W960, ImageWidth.getFromPixels(960)) + assertEquals(ImageWidth.W1920, ImageWidth.getFromPixels(1920)) + } + + @Test + fun testClosestUnder240Pixels() { + assertEquals(ImageWidth.W240, ImageWidth.getFromPixels(0)) + assertEquals(ImageWidth.W240, ImageWidth.getFromPixels(239)) + } + + @Test + fun testClosestGreaterThan1920Pixels() { + assertEquals(ImageWidth.W1920, ImageWidth.getFromPixels(1921)) + assertEquals(ImageWidth.W1920, ImageWidth.getFromPixels(4000)) + } + + @Test + fun testClosestW240() { + assertEquals(ImageWidth.W240, ImageWidth.getFromPixels(239)) + assertEquals(ImageWidth.W240, ImageWidth.getFromPixels(240)) + assertEquals(ImageWidth.W240, ImageWidth.getFromPixels(241)) + } + + @Test + fun testClosestW320() { + assertEquals(ImageWidth.W320, ImageWidth.getFromPixels(319)) + assertEquals(ImageWidth.W320, ImageWidth.getFromPixels(320)) + assertEquals(ImageWidth.W320, ImageWidth.getFromPixels(321)) + } + + @Test + fun testClosestW480() { + assertEquals(ImageWidth.W480, ImageWidth.getFromPixels(479)) + assertEquals(ImageWidth.W480, ImageWidth.getFromPixels(480)) + assertEquals(ImageWidth.W480, ImageWidth.getFromPixels(481)) + } + + @Test + fun testClosestW960() { + assertEquals(ImageWidth.W960, ImageWidth.getFromPixels(959)) + assertEquals(ImageWidth.W960, ImageWidth.getFromPixels(960)) + assertEquals(ImageWidth.W960, ImageWidth.getFromPixels(961)) + } + + @Test + fun testClosestW1920() { + assertEquals(ImageWidth.W1920, ImageWidth.getFromPixels(1919)) + assertEquals(ImageWidth.W1920, ImageWidth.getFromPixels(1920)) + assertEquals(ImageWidth.W1920, ImageWidth.getFromPixels(1921)) + } + + @Test + fun testClosestBetween240_320() { + assertEquals(ImageWidth.W240, ImageWidth.getFromPixels(279)) + assertEquals(ImageWidth.W320, ImageWidth.getFromPixels(280)) // Middle + assertEquals(ImageWidth.W320, ImageWidth.getFromPixels(281)) + } + + @Test + fun testClosestBetween320_480() { + assertEquals(ImageWidth.W320, ImageWidth.getFromPixels(399)) + assertEquals(ImageWidth.W480, ImageWidth.getFromPixels(400)) // Middle + assertEquals(ImageWidth.W480, ImageWidth.getFromPixels(401)) + } + + @Test + fun testClosestBetween480_960() { + assertEquals(ImageWidth.W480, ImageWidth.getFromPixels(719)) + assertEquals(ImageWidth.W960, ImageWidth.getFromPixels(720)) // Middle + assertEquals(ImageWidth.W960, ImageWidth.getFromPixels(721)) + } + + @Test + fun testClosestBetween960_1920() { + assertEquals(ImageWidth.W960, ImageWidth.getFromPixels(1439)) + assertEquals(ImageWidth.W1920, ImageWidth.getFromPixels(1440)) // Middle + assertEquals(ImageWidth.W1920, ImageWidth.getFromPixels(1441)) + } + + @Test + fun testNegativeWidthPixels() { + assertEquals(ImageWidth.W240, ImageWidth.getFromPixels(-100)) + } + +}