diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/HttpResultException.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/HttpResultException.kt new file mode 100644 index 000000000..e5814598d --- /dev/null +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/HttpResultException.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business + +import io.ktor.client.plugins.ClientRequestException +import java.io.IOException + +/** + * Http result exception + * + * @constructor + * + * @param message Message for the IOException, constructor used by PlaybackException to rebuild this exception. + */ +class HttpResultException internal constructor(message: String) : IOException(message) { + constructor(throwable: ClientRequestException) : this( + "${throwable.response.status.description} (${throwable.response.status.value})" + ) +} diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/MediaCompositionMediaItemSource.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/MediaCompositionMediaItemSource.kt index 97309d73b..423402754 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/MediaCompositionMediaItemSource.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/MediaCompositionMediaItemSource.kt @@ -8,13 +8,14 @@ import android.net.Uri import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import ch.srgssr.pillarbox.core.business.integrationlayer.data.BlockReasonException +import ch.srgssr.pillarbox.core.business.exception.BlockReasonException +import ch.srgssr.pillarbox.core.business.exception.DataParsingException +import ch.srgssr.pillarbox.core.business.exception.ResourceNotFoundException import ch.srgssr.pillarbox.core.business.integrationlayer.data.Chapter import ch.srgssr.pillarbox.core.business.integrationlayer.data.Drm import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaComposition import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaUrn import ch.srgssr.pillarbox.core.business.integrationlayer.data.Resource -import ch.srgssr.pillarbox.core.business.integrationlayer.data.ResourceNotFoundException import ch.srgssr.pillarbox.core.business.integrationlayer.service.MediaCompositionDataSource import ch.srgssr.pillarbox.core.business.tracker.SRGEventLoggerTracker import ch.srgssr.pillarbox.core.business.tracker.commandersact.CommandersActTracker @@ -22,6 +23,9 @@ import ch.srgssr.pillarbox.core.business.tracker.comscore.ComScoreTracker import ch.srgssr.pillarbox.player.data.MediaItemSource import ch.srgssr.pillarbox.player.getMediaItemTrackerData import ch.srgssr.pillarbox.player.setTrackerData +import io.ktor.client.plugins.ClientRequestException +import kotlinx.serialization.SerializationException +import java.io.IOException /** * Load [MediaItem] playable from a [ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaComposition] @@ -65,7 +69,21 @@ class MediaCompositionMediaItemSource( require(MediaUrn.isValid(mediaItem.mediaId)) { "Invalid urn=${mediaItem.mediaId}" } val mediaUri = mediaItem.localConfiguration?.uri require(!MediaUrn.isValid(mediaUri.toString())) { "Uri can't be a urn" } - val result = mediaCompositionDataSource.getMediaCompositionByUrn(mediaItem.mediaId).getOrThrow() + val result = mediaCompositionDataSource.getMediaCompositionByUrn(mediaItem.mediaId).getOrElse { + when (it) { + is ClientRequestException -> { + throw HttpResultException(it) + } + + is SerializationException -> { + throw DataParsingException(it) + } + + else -> { + throw IOException(it.message) + } + } + } val chapter = result.mainChapter chapter.blockReason?.let { throw BlockReasonException(it) @@ -74,7 +92,7 @@ class MediaCompositionMediaItemSource( throw BlockReasonException(it) } - val resource = resourceSelector.selectResourceFromChapter(chapter) ?: throw ResourceNotFoundException + val resource = resourceSelector.selectResourceFromChapter(chapter) ?: throw ResourceNotFoundException() var uri = Uri.parse(resource.url) if (resource.tokenType == Resource.TokenType.AKAMAI) { uri = appendTokenQueryToUri(uri) diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/SRGErrorMessageProvider.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/SRGErrorMessageProvider.kt index e4af70681..25e83e5ed 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/SRGErrorMessageProvider.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/SRGErrorMessageProvider.kt @@ -4,38 +4,45 @@ */ package ch.srgssr.pillarbox.core.business -import android.os.RemoteException +import android.content.Context import android.util.Pair import androidx.media3.common.ErrorMessageProvider import androidx.media3.common.PlaybackException -import ch.srgssr.pillarbox.core.business.integrationlayer.data.BlockReasonException -import ch.srgssr.pillarbox.core.business.integrationlayer.data.ResourceNotFoundException -import io.ktor.client.plugins.ClientRequestException +import ch.srgssr.pillarbox.core.business.exception.BlockReasonException +import ch.srgssr.pillarbox.core.business.exception.DataParsingException +import ch.srgssr.pillarbox.core.business.exception.ResourceNotFoundException +import ch.srgssr.pillarbox.core.business.extension.getString +import java.io.IOException /** * Process error message from [PlaybackException] */ -class SRGErrorMessageProvider : ErrorMessageProvider { +class SRGErrorMessageProvider(private val context: Context) : ErrorMessageProvider { override fun getErrorMessage(throwable: PlaybackException): Pair { return when (val cause = throwable.cause) { is BlockReasonException -> { - Pair.create(0, cause.blockReason.name) + Pair.create(0, context.getString(cause.blockReason)) } - // When using MediaController, RemoteException is send instead of HttpException. - is RemoteException -> - Pair.create(throwable.errorCode, cause.message) - is ClientRequestException -> { - Pair.create(cause.response.status.value, cause.response.status.description) + is ResourceNotFoundException -> { + Pair.create(0, context.getString(R.string.noPlayableResourceFound)) } - is ResourceNotFoundException -> { - Pair.create(0, "Can't find Resource to play") + is DataParsingException -> { + Pair.create(0, context.getString(R.string.invalidDataError)) + } + + is HttpResultException -> { + Pair.create(0, cause.message) + } + + is IOException -> { + Pair.create(0, context.getString(R.string.NoInternet)) } else -> { - Pair.create(throwable.errorCode, "${throwable.localizedMessage} (${throwable.errorCodeName})") + Pair.create(throwable.errorCode, context.getString(R.string.unknownError)) } } } diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/BlockReasonException.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/BlockReasonException.kt new file mode 100644 index 000000000..8759e5c90 --- /dev/null +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/BlockReasonException.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.exception + +import ch.srgssr.pillarbox.core.business.integrationlayer.data.BlockReason +import java.io.IOException + +/** + * Block reason exception + * + * @property blockReason the reason a [Chapter] or a [Segment] is blocked. + */ +class BlockReasonException(val blockReason: BlockReason) : IOException(blockReason.name) { + /* + * ExoPlaybackException bundles cause exception with class name and message. + * In order to recreate the cause of the throwable, it needs a throwable class with constructor(string). + */ + internal constructor(message: String) : this(parseMessage(message)) + + companion object { + @Suppress("SwallowedException") + private fun parseMessage(message: String): BlockReason { + return try { + BlockReason.valueOf(message) + } catch (e: IllegalArgumentException) { + BlockReason.UNKNOWN + } + } + } +} diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/DataParsingException.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/DataParsingException.kt new file mode 100644 index 000000000..3c1fd39dc --- /dev/null +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/DataParsingException.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.exception + +import java.io.IOException + +/** + * Data parsing exception + * + * @constructor + * + * @param message Message for the IOException, constructor used by PlaybackException to rebuild this exception. + */ +class DataParsingException internal constructor(message: String? = "Data parsing error") : IOException(message) { + constructor(throwable: Throwable) : this(throwable.message) +} diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/ResourceNotFoundException.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/ResourceNotFoundException.kt new file mode 100644 index 000000000..f579eb7c9 --- /dev/null +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/ResourceNotFoundException.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.exception + +import java.io.IOException + +/** + * Resource not found exception is throw when : + * - [Chapter] doesn't have a playable resource + * - [Chapter.listResource] is empty or null + */ +class ResourceNotFoundException internal constructor(message: String) : IOException(message) { + constructor() : this("Unable to find suitable resources") +} diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/extension/BlockReason.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/extension/BlockReason.kt new file mode 100644 index 000000000..db6e290f4 --- /dev/null +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/extension/BlockReason.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.extension + +import android.content.Context +import ch.srgssr.pillarbox.core.business.R +import ch.srgssr.pillarbox.core.business.integrationlayer.data.BlockReason + +/** + * Get string + * + * @param blockReason The [BlockReason] to get the string of. + * @return The string message of [blockReason] + */ +fun Context.getString(blockReason: BlockReason): String { + return getString(blockReason.getStringResId()) +} + +/** + * Get string resource id + * + * @return The android string resource id of a [BlockReason] + */ +fun BlockReason.getStringResId(): Int { + return when (this) { + BlockReason.AGERATING12 -> R.string.blockReason_ageRating12 + BlockReason.GEOBLOCK -> R.string.blockReason_geoBlock + BlockReason.LEGAL -> R.string.blockReason_legal + BlockReason.COMMERCIAL -> R.string.blockReason_commercial + BlockReason.AGERATING18 -> R.string.blockReason_ageRating18 + BlockReason.STARTDATE -> R.string.blockReason_startDate + BlockReason.ENDDATE -> R.string.blockReason_endDate + BlockReason.UNKNOWN -> R.string.blockReason_unknown + } +} diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/BlockReasonException.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/BlockReasonException.kt deleted file mode 100644 index cabb07b31..000000000 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/BlockReasonException.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) 2022. SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.core.business.integrationlayer.data - -/** - * Block reason exception - * - * @property blockReason the reason a [Chapter] or a [Segment] is blocked. - */ -class BlockReasonException(val blockReason: BlockReason) : RuntimeException(blockReason.name) diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/ResourceNotFoundException.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/ResourceNotFoundException.kt deleted file mode 100644 index e5ad9a1e1..000000000 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/ResourceNotFoundException.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) 2022. SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.core.business.integrationlayer.data - -/** - * Resource not found exception is throw when : - * - [Chapter] doesn't have a playable resource - * - [Chapter.listResource] is empty or null - */ -object ResourceNotFoundException : RuntimeException("Unable to find suitable resources") diff --git a/pillarbox-core-business/src/main/res/values-de/strings.xml b/pillarbox-core-business/src/main/res/values-de/strings.xml new file mode 100644 index 000000000..843e2ef42 --- /dev/null +++ b/pillarbox-core-business/src/main/res/values-de/strings.xml @@ -0,0 +1,14 @@ + + + Dieser Inhalt ist ausserhalb der Schweiz nicht verfügbar. + Dieser Inhalt ist aus rechtlichen Gründen nicht verfügbar. + Dieser Werbe-Inhalt ist nicht verfügbar. + Dieser Inhalt ist aus Gründen des Jugendschutzes nur zwischen 22:00 und 5:00 Uhr verfügbar. + Dieser Inhalt ist aus Gründen des Jugendschutzes nur zwischen 20:00 und 6:00 Uhr verfügbar. + Dieser Inhalt ist noch nicht verfügbar. + Dieser Inhalt ist nicht mehr verfügbar. + Dieser Inhalt ist nicht verfügbar. + diff --git a/pillarbox-core-business/src/main/res/values-en/strings.xml b/pillarbox-core-business/src/main/res/values-en/strings.xml new file mode 100644 index 000000000..0ce7ab99c --- /dev/null +++ b/pillarbox-core-business/src/main/res/values-en/strings.xml @@ -0,0 +1,18 @@ + + + + Unknown error. + The data is invalid. + No playable resources could be found. + This content is not available outside Switzerland. + This content is not available due to legal restrictions. + This commercial content is not available. + To protect children this content is only available between 10PM and 5AM. + To protect children this content is only available between 8PM and 6AM. + This content is not available yet. + This content is not available anymore. + This content is not available. + diff --git a/pillarbox-core-business/src/main/res/values-fr/strings.xml b/pillarbox-core-business/src/main/res/values-fr/strings.xml new file mode 100644 index 000000000..d6b277858 --- /dev/null +++ b/pillarbox-core-business/src/main/res/values-fr/strings.xml @@ -0,0 +1,17 @@ + + + Les données sont invalides. + Aucune ressource jouable n\'a pu être trouvée. + Ce contenu n\'est pas disponible hors de Suisse. + Ce contenu a été retiré par décision de justice. + Ce contenu n’est actuellement pas disponible. + Ce contenu n\'est disponible qu\'entre 22h et 5h afin de protéger le jeune public. + Ce contenu n\'est disponible qu\'entre 20h et 6h afin de protéger le jeune public. + Ce contenu n’est pas encore disponible. + Ce contenu n’est plus disponible. + Ce contenu n’est pas disponible. + Erreur inconnue. + diff --git a/pillarbox-core-business/src/main/res/values-it/strings.xml b/pillarbox-core-business/src/main/res/values-it/strings.xml new file mode 100644 index 000000000..69bab0780 --- /dev/null +++ b/pillarbox-core-business/src/main/res/values-it/strings.xml @@ -0,0 +1,12 @@ + + + Questo media non è disponibile fuori dalla Svizzera. + Questo media non è disponibile a causa di restrizioni legali. + Questo contenuto commerciale non è disponibile. + Questo media non è ancora disponibile. + Questo media non è più disponibile. + Questo media non è disponibile. + diff --git a/pillarbox-core-business/src/main/res/values-rm/strings.xml b/pillarbox-core-business/src/main/res/values-rm/strings.xml new file mode 100644 index 000000000..8aa1b4262 --- /dev/null +++ b/pillarbox-core-business/src/main/res/values-rm/strings.xml @@ -0,0 +1,12 @@ + + + Quest medium n\'è betg disponibel ordaifer la Svizra. + Quest medium n\'è betg disponibel perquei ch\'el è scadì. + Quest medium commerzial n\'è betg disponibel. + Quest medium n\'è betg anc disponibel. + Quest medium n\'è betg pli disponibel. + Quest medium n\'è betg disponibel. + diff --git a/pillarbox-core-business/src/main/res/values/strings.xml b/pillarbox-core-business/src/main/res/values/strings.xml new file mode 100644 index 000000000..dd1c438e8 --- /dev/null +++ b/pillarbox-core-business/src/main/res/values/strings.xml @@ -0,0 +1,18 @@ + + + Unknown error. + The data is invalid. + No playable resources could be found. + It seems your device is not connected to internet. + This content is not available outside Switzerland. + This content is not available due to legal restrictions. + This commercial content is not available. + To protect children this content is only available between 10PM and 5AM. + To protect children this content is only available between 8PM and 6AM. + This content is not available yet. + This content is not available anymore. + This content is not available. + diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/MediaCompositionMediaItemSourceTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/MediaCompositionMediaItemSourceTest.kt index d8e5d9554..b3ceaa487 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/MediaCompositionMediaItemSourceTest.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/MediaCompositionMediaItemSourceTest.kt @@ -7,12 +7,12 @@ package ch.srgssr.pillarbox.core.business import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata +import ch.srgssr.pillarbox.core.business.exception.BlockReasonException +import ch.srgssr.pillarbox.core.business.exception.ResourceNotFoundException import ch.srgssr.pillarbox.core.business.integrationlayer.data.BlockReason -import ch.srgssr.pillarbox.core.business.integrationlayer.data.BlockReasonException import ch.srgssr.pillarbox.core.business.integrationlayer.data.Chapter import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaComposition import ch.srgssr.pillarbox.core.business.integrationlayer.data.Resource -import ch.srgssr.pillarbox.core.business.integrationlayer.data.ResourceNotFoundException import ch.srgssr.pillarbox.core.business.integrationlayer.data.Segment import ch.srgssr.pillarbox.core.business.integrationlayer.service.MediaCompositionDataSource import kotlinx.coroutines.runBlocking @@ -35,13 +35,6 @@ class MediaCompositionMediaItemSourceTest { Unit } - @Test(expected = IllegalArgumentException::class) - fun testUrnAsUri() = runBlocking { - val urn = "urn:rts:video:1234" - mediaItemSource.loadMediaItem(MediaItem.Builder().setMediaId(urn).setUri(urn).build()) - Unit - } - @Test(expected = ResourceNotFoundException::class) fun testNoResource() = runBlocking { mediaItemSource.loadMediaItem(createMediaItem(DummyMediaCompositionProvider.URN_NO_RESOURCES)) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerError.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerError.kt index 3cedcdcb9..f26a31e6c 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerError.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerError.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontStyle import androidx.media3.common.PlaybackException import ch.srgssr.pillarbox.core.business.SRGErrorMessageProvider @@ -29,8 +30,9 @@ import ch.srgssr.pillarbox.core.business.SRGErrorMessageProvider */ @Composable fun PlayerError(playerError: PlaybackException, modifier: Modifier = Modifier, onRetry: () -> Unit) { - val errorMessageProvider = remember { - SRGErrorMessageProvider() + val context = LocalContext.current + val errorMessageProvider = remember(context) { + SRGErrorMessageProvider(context) } Surface(modifier, color = Color.Black) { Box(