From 301bc0e145436294efd91f319effea53e0f499ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 21 Jun 2023 15:39:13 +0200 Subject: [PATCH] Add paging helper (#7) ## Description Add `androidx.paging` [v3 library](https://developer.android.com/topic/libraries/architecture/paging/v3-overview) helper to handle integration layer content with pagination. There is two use cases 1. Requesting a amount of content with a list of urns that exceed the integration layer page limit (50) 2. Requesting a result with a nextUrl Both are supported with 1. `UrnsPagingSource` 2. `NexturlPagingSource` ### Recommended We recommend to use `DataProviderPaging` that simplify the usage and retrieve directly the content needed with `PagingDataAdapter`. ## Usage ```kotlin val dataProvider = DataProviderPaging(...) val pagingAdapter = PagingDataAdapter(...) dataProvider.getMediaRecommendedByUrn(urn, pageSize = 15).collectLatest { pagingData -> pagingAdapter.submitData(pagingData) } ``` --- buildSrc/src/main/kotlin/Versions.kt | 9 +- data/build.gradle.kts | 2 +- dataprovider-paging/.gitignore | 1 + dataprovider-paging/build.gradle.kts | 79 +++++ dataprovider-paging/consumer-rules.pro | 0 dataprovider-paging/proguard-rules.pro | 21 ++ .../paging/ExampleInstrumentedTest.kt | 24 ++ .../src/main/AndroidManifest.xml | 4 + .../dataprovider/paging/DataProviderPaging.kt | 290 ++++++++++++++++++ .../paging/datasource/NextUrlPagingSource.kt | 37 +++ .../paging/datasource/UrnsPagingSource.kt | 53 ++++ .../DataProviderPagingComponent.kt | 20 ++ .../dependencies/DataProviderPagingScope.java | 16 + .../dataprovider/paging/ExampleUnitTest.kt | 17 + dataprovider-retrofit/build.gradle.kts | 8 +- .../components/DataProviderDependencies.java | 22 ++ .../request/parameters/IlPaging.kt | 2 + dataproviderdemo/build.gradle.kts | 1 + .../srgssr/dataprovider/demo/MainActivity.kt | 21 ++ settings.gradle | 1 + 20 files changed, 619 insertions(+), 9 deletions(-) create mode 100644 dataprovider-paging/.gitignore create mode 100644 dataprovider-paging/build.gradle.kts create mode 100644 dataprovider-paging/consumer-rules.pro create mode 100644 dataprovider-paging/proguard-rules.pro create mode 100644 dataprovider-paging/src/androidTest/java/ch/srgssr/dataprovider/paging/ExampleInstrumentedTest.kt create mode 100644 dataprovider-paging/src/main/AndroidManifest.xml create mode 100644 dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/DataProviderPaging.kt create mode 100644 dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/datasource/NextUrlPagingSource.kt create mode 100644 dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/datasource/UrnsPagingSource.kt create mode 100644 dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/dependencies/DataProviderPagingComponent.kt create mode 100644 dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/dependencies/DataProviderPagingScope.java create mode 100644 dataprovider-paging/src/test/java/ch/srgssr/dataprovider/paging/ExampleUnitTest.kt create mode 100644 dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/dependencies/components/DataProviderDependencies.java diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 298675b..f1ec00b 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -1,9 +1,10 @@ object Versions { const val coreKtx = "1.8.0" - const val lifecycle_version = "2.5.1" + const val lifecycle = "2.5.1" const val dagger = "2.44.2" - const val retrofit_version = "2.9.0" - const val okHttp_version = "4.9.1" - const val gsonVersion = "2.10.1" + const val retrofit = "2.9.0" + const val okHttp = "4.9.1" + const val gson = "2.10.1" const val detekt = "1.22.0" + const val paging = "3.1.1" } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 68d7188..e871f9d 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -38,7 +38,7 @@ android { dependencies { implementation("androidx.core:core-ktx:${Versions.coreKtx}") - implementation("com.google.code.gson:gson:${Versions.gsonVersion}") + implementation("com.google.code.gson:gson:${Versions.gson}") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") diff --git a/dataprovider-paging/.gitignore b/dataprovider-paging/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/dataprovider-paging/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/dataprovider-paging/build.gradle.kts b/dataprovider-paging/build.gradle.kts new file mode 100644 index 0000000..b68bf73 --- /dev/null +++ b/dataprovider-paging/build.gradle.kts @@ -0,0 +1,79 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.kapt") + `maven-publish` +} + +android { + namespace = "ch.srgssr.dataprovider.paging" + compileSdk = Config.compileSdk + + defaultConfig { + minSdk = Config.minSdk + targetSdk = Config.targetSdk + group = Config.maven_group + version = Config.versionName + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + lint { + // https://developer.android.com/reference/tools/gradle-api/4.1/com/android/build/api/dsl/LintOptions + abortOnError = false + } +} + +dependencies { + api(project(mapOf("path" to ":dataprovider-retrofit"))) + implementation("androidx.core:core-ktx:${Versions.coreKtx}") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycle}") + api("androidx.paging:paging-runtime-ktx:${Versions.paging}") + + implementation("com.google.dagger:dagger:${Versions.dagger}") + kapt("com.google.dagger:dagger-compiler:${Versions.dagger}") + + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} + +publishing { + publications { + register("gpr") { + afterEvaluate { + from(components["release"]) + } + } + } + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/SRGSSR/srgdataprovider-android") + credentials { + username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") + } + } + maven { + url = uri("https://maven.ecetest.rts.ch/content/repositories/srg-letterbox-releases/") + credentials { + username = project.findProperty("sonatypeUsername") as String? ?: System.getenv("SONATYPE_USERNAME") + password = project.findProperty("sonatypePassword") as String? ?: System.getenv("SONATYPE_PASSWORD") + } + } + } +} diff --git a/dataprovider-paging/consumer-rules.pro b/dataprovider-paging/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/dataprovider-paging/proguard-rules.pro b/dataprovider-paging/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/dataprovider-paging/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/dataprovider-paging/src/androidTest/java/ch/srgssr/dataprovider/paging/ExampleInstrumentedTest.kt b/dataprovider-paging/src/androidTest/java/ch/srgssr/dataprovider/paging/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..287cbd6 --- /dev/null +++ b/dataprovider-paging/src/androidTest/java/ch/srgssr/dataprovider/paging/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package ch.srgssr.dataprovider.paging + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("ch.srgssr.dataprovider.paging.test", appContext.packageName) + } +} diff --git a/dataprovider-paging/src/main/AndroidManifest.xml b/dataprovider-paging/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8bdb7e1 --- /dev/null +++ b/dataprovider-paging/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/DataProviderPaging.kt b/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/DataProviderPaging.kt new file mode 100644 index 0000000..b2be577 --- /dev/null +++ b/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/DataProviderPaging.kt @@ -0,0 +1,290 @@ +package ch.srgssr.dataprovider.paging + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import ch.srg.dataProvider.integrationlayer.data.Episode +import ch.srg.dataProvider.integrationlayer.data.ListResult +import ch.srg.dataProvider.integrationlayer.data.LiveCenterType +import ch.srg.dataProvider.integrationlayer.data.Media +import ch.srg.dataProvider.integrationlayer.data.MediaFilter +import ch.srg.dataProvider.integrationlayer.data.MediaType +import ch.srg.dataProvider.integrationlayer.data.Show +import ch.srg.dataProvider.integrationlayer.data.Song +import ch.srg.dataProvider.integrationlayer.data.search.SearchParams +import ch.srg.dataProvider.integrationlayer.data.search.SearchResultWithMediaList +import ch.srg.dataProvider.integrationlayer.data.search.SearchResultWithShowList +import ch.srg.dataProvider.integrationlayer.request.IlService +import ch.srg.dataProvider.integrationlayer.request.SearchProvider +import ch.srg.dataProvider.integrationlayer.request.parameters.Bu +import ch.srg.dataProvider.integrationlayer.request.parameters.IlDate +import ch.srg.dataProvider.integrationlayer.request.parameters.IlDateTime +import ch.srg.dataProvider.integrationlayer.request.parameters.IlMediaType +import ch.srg.dataProvider.integrationlayer.request.parameters.IlPaging.Unlimited.toIlPaging +import ch.srg.dataProvider.integrationlayer.request.parameters.IlUrns +import ch.srgssr.dataprovider.paging.datasource.NextUrlPagingSource +import ch.srgssr.dataprovider.paging.datasource.UrnsPagingSource +import ch.srgssr.dataprovider.paging.dependencies.DataProviderPagingScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import java.util.Date +import javax.inject.Inject + +/** + * Paging data source + */ +@DataProviderPagingScope +class DataProviderPaging @Inject constructor( + private val ilService: IlService, + private val searchProvider: SearchProvider +) { + + fun getShowListFromUrns(tabUrns: List, pageSize: Int = DefaultPageSize): Flow> { + return Pager(config = pageSize.toPagingConfig(), pagingSourceFactory = { + UrnsPagingSource(urns = tabUrns, call = { urns -> + ilService.getShowListFromUrns(IlUrns(urns)) + }) + }).flow + } + + fun getMediaListFromUrns(tabUrns: List, pageSize: Int = DefaultPageSize): Flow> { + return Pager(config = pageSize.toPagingConfig(), pagingSourceFactory = { + UrnsPagingSource(urns = tabUrns, call = { urns -> + ilService.getMediaListFromUrns(IlUrns(urns)) + }) + }).flow + } + + fun getLatestMediaByShowUrn(showUrn: String, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getLatestMediaByShowUrn(showUrn, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getLatestMediaByShowUrn( + listShowUrns: List, + filter: MediaFilter? = null, + maxPublishDate: Date? = null, + minPublishDate: Date? = null, + types: String? = null, + pageSize: Int = DefaultPageSize + ): Flow> { + return Pager(config = pageSize.toPagingConfig(), pagingSourceFactory = { + UrnsPagingSource(urns = listShowUrns, call = { urns -> + ilService.getLatestMediaByShowUrns( + showUrns = IlUrns(urns), + onlyEpisodes = if (filter == MediaFilter.EPISODE_ONLY) true else null, + excludeEpisodes = if (filter == MediaFilter.EPISODE_EXCLUDED) true else null, + maxPublishedDate = maxPublishDate?.let { IlDateTime(it) }, + minPublishedDate = minPublishDate?.let { IlDateTime(it) }, + types = types + ) + }) + }).flow + } + + fun getMediaRecommendedByUrn(urn: String, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getMediaRecommendedByUrn(urn, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getLatestMediaByTopicUrn(topicUrn: String, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getLatestMediaByTopicUrn(topicUrn, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getMostClickedMediaByTopicUrn(topicUrn: String, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getMostClickedMediaByTopicUrn(topicUrn, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getTvMostClickedMedias(bu: Bu, topicId: String? = null, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getTvMostClickedMedias(bu, topicId, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getTvSoonExpiringMedias(bu: Bu, topicId: String? = null, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getTvSoonExpiringMedias(bu, topicId, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getTvSoonExpiringMedias(bu: Bu, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getTvWebFirstMedias(bu, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getTrendingMedias(bu: Bu, type: IlMediaType, onlyEpisodes: Boolean = false, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getTrendingMedias(bu, type, onlyEpisodes, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getLatestMediaByChannelId(bu: Bu, type: IlMediaType, channelId: String, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getLatestMediaByChannelId(bu, type, channelId, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getTvEpisodesByDate(bu: Bu, date: IlDate, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getTvEpisodesByDate(bu, date, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getEpisodeCompositionByUrn(showUrn: String, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getEpisodeCompositionByUrn(showUrn, it.toIlPaging()) }, + nextCall = { ilService.getEpisodeCompositionByUrn(it) } + ) + } + + fun getLiveCenterVideos( + bu: Bu, + type: LiveCenterType, + onlyEventsWithResult: Boolean = true, + pageSize: Int = DefaultPageSize + ): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getLiveCenterVideos(bu, type, onlyEventsWithResult, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getScheduledLiveStreamVideos(bu: Bu, signLanguageOnly: Boolean = false, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getScheduledLiveStreamVideos(bu, signLanguageOnly, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getRadioEpisodesByDateByChannelId(bu: Bu, date: IlDate, channelId: String, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getRadioEpisodesByDateByChannelId(bu, date, channelId, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getRadioMostClickedMediasByChannelId( + bu: Bu, + channelId: String, + onlyEpisodes: Boolean? = null, + pageSize: Int = DefaultPageSize + ): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getRadioMostClickedMediasByChannelId(bu, channelId, onlyEpisodes, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getRadioMostClickedMediasByChannelId(bu: Bu, onlyEpisodes: Boolean? = null, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getRadioMostClickedMedias(bu, onlyEpisodes, it.toIlPaging()) }, + nextCall = { ilService.getMediaListNextUrl(it) } + ) + } + + fun getRadioSongListByChannelId(bu: Bu, channelId: String, pageSize: Int = DefaultPageSize): Flow> { + return createNextUrlPagingData( + pageSize = pageSize, + initialCall = { ilService.getRadioSongListByChannelId(bu, channelId, it.toIlPaging()) }, + nextCall = { ilService.getSongListNextUrl(it) } + ) + } + + /** + * Search media. + * + * @param searchTerm search term (can be empty) + * @param queryParameters list of query parameters to send to the server. + * @param lastResult First server response (with aggregations and suggestions) + * + * Note that SWI does not support any query parameters (May 2019) + */ + fun searchMedia( + bu: Bu, + searchTerm: String, + queryParameters: SearchParams.MediaParams, + lastResult: MutableSharedFlow? = null, + pageSize: Int = DefaultPageSize + ): Flow> { + return createNextUrlPagingData(pageSize, initialCall = { + val result = searchProvider.searchMedias(bu, searchTerm, queryParameters) + lastResult?.emit(result) + result + }, nextCall = { + searchProvider.searchMediaWithNextUrl(it) + }) + } + + /** + * Search show. + * + * @param searchTerm search term + * @param queryParameters list of query parameters to send to the server. + * @param lastResult First server response (with total) + * @return PagingDataSource with paginated results. + */ + fun searchShow( + bu: Bu, + searchTerm: String, + queryParameters: SearchParams.ShowParams, + lastResult: MutableSharedFlow? = null, + pageSize: Int = DefaultPageSize + ): Flow> { + return createNextUrlPagingData(pageSize, initialCall = { + val result = searchProvider.searchShows(bu, searchTerm, IlMediaType(queryParameters.mediaType ?: MediaType.VIDEO)) + lastResult?.emit(result) + result + }, nextCall = { + searchProvider.searchShowWithNextUrl(it) + }) + } + + companion object { + private const val DefaultPageSize = 10 + + private fun Int.toPagingConfig() = PagingConfig(pageSize = this, prefetchDistance = 1) + + private fun createNextUrlPagingData( + pageSize: Int, + initialCall: suspend (pageSize: Int) -> ListResult?, + nextCall: suspend (next: String) -> ListResult? + ): Flow> = Pager(config = pageSize.toPagingConfig(), pagingSourceFactory = { + NextUrlPagingSource( + initialCall = initialCall, nextCall = nextCall + ) + }).flow + } +} diff --git a/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/datasource/NextUrlPagingSource.kt b/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/datasource/NextUrlPagingSource.kt new file mode 100644 index 0000000..a2dd1a5 --- /dev/null +++ b/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/datasource/NextUrlPagingSource.kt @@ -0,0 +1,37 @@ +package ch.srgssr.dataprovider.paging.datasource + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import ch.srg.dataProvider.integrationlayer.data.ListResult + +/** + * Copyright (c) SRG SSR. All rights reserved. + *

+ * License information is available from the LICENSE file. + */ +@Suppress("TooGenericExceptionCaught") +class NextUrlPagingSource( + private val initialCall: suspend (pageSize: Int) -> ListResult?, + private val nextCall: suspend (next: String) -> ListResult? +) : PagingSource() { + + override fun getRefreshKey(state: PagingState): String? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.nextKey + } + } + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val nextUrl = params.key // PageSize+PrefetchDistance + val listResult = if (nextUrl == null) + initialCall.invoke(params.loadSize) + else + nextCall.invoke(nextUrl) + val data = listResult?.list + LoadResult.Page(data.orEmpty(), prevKey = null, nextKey = listResult?.next) + } catch (e: Exception) { + LoadResult.Error(e) + } + } +} diff --git a/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/datasource/UrnsPagingSource.kt b/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/datasource/UrnsPagingSource.kt new file mode 100644 index 0000000..ad866cf --- /dev/null +++ b/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/datasource/UrnsPagingSource.kt @@ -0,0 +1,53 @@ +package ch.srgssr.dataprovider.paging.datasource + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import ch.srg.dataProvider.integrationlayer.data.ListResult +import java.io.IOException +import kotlin.math.min + +/** + * Copyright (c) SRG SSR. All rights reserved. + *

+ * License information is available from the LICENSE file. + */ +class UrnsPagingSource( + private val urns: List, + private val call: suspend (urns: List) -> ListResult?, +) : PagingSource() { + private val dataCount: Int = urns.size + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.nextKey + } + } + + override suspend fun load(params: LoadParams): LoadResult { + if (urns.isEmpty()) { + return LoadResult.Page(listOf(), null, null) + } + return try { + val loadSize = min(params.loadSize, MAX_IL_URNS_PER_CALL) + val startPosition: Int = min(dataCount - 1, params.key ?: 0) // inclusive subList + val endPosition = min(dataCount, startPosition + loadSize) // exclusive subList + val subUrnList: List = urns.subList(startPosition, endPosition) + if (subUrnList.isEmpty()) { + return LoadResult.Page(listOf(), null, null) + } + val listResult = call.invoke(subUrnList) + val data = listResult?.list.orEmpty() + val nextPosition = if (endPosition >= dataCount) null else endPosition + LoadResult.Page(data, null, nextPosition) + } catch (e: IOException) { + LoadResult.Error(e) + } + } + + companion object { + /** + * Integration layer hard limitation urns list query + */ + const val MAX_IL_URNS_PER_CALL = 50 + } +} diff --git a/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/dependencies/DataProviderPagingComponent.kt b/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/dependencies/DataProviderPagingComponent.kt new file mode 100644 index 0000000..9a45a0b --- /dev/null +++ b/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/dependencies/DataProviderPagingComponent.kt @@ -0,0 +1,20 @@ +package ch.srgssr.dataprovider.paging.dependencies + +import ch.srg.dataProvider.integrationlayer.dependencies.components.IlDataProviderComponent +import ch.srgssr.dataprovider.paging.DataProviderPaging +import dagger.Component + +@DataProviderPagingScope +@Component(dependencies = [IlDataProviderComponent::class]) +interface DataProviderPagingComponent { + + val dataProviderPaging: DataProviderPaging +} + +object DataProviderPagingDependencies { + fun create(ilDataProviderComponent: IlDataProviderComponent): DataProviderPagingComponent { + return DaggerDataProviderPagingComponent.builder() + .ilDataProviderComponent(ilDataProviderComponent) + .build() + } +} diff --git a/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/dependencies/DataProviderPagingScope.java b/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/dependencies/DataProviderPagingScope.java new file mode 100644 index 0000000..19071f7 --- /dev/null +++ b/dataprovider-paging/src/main/java/ch/srgssr/dataprovider/paging/dependencies/DataProviderPagingScope.java @@ -0,0 +1,16 @@ +package ch.srgssr.dataprovider.paging.dependencies; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Scope; + +/** + * Copyright (c) SRG SSR. All rights reserved. + *

+ * License information is available from the LICENSE file. + */ +@Scope +@Retention(RetentionPolicy.RUNTIME) +public @interface DataProviderPagingScope { +} diff --git a/dataprovider-paging/src/test/java/ch/srgssr/dataprovider/paging/ExampleUnitTest.kt b/dataprovider-paging/src/test/java/ch/srgssr/dataprovider/paging/ExampleUnitTest.kt new file mode 100644 index 0000000..fe9d638 --- /dev/null +++ b/dataprovider-paging/src/test/java/ch/srgssr/dataprovider/paging/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package ch.srgssr.dataprovider.paging + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/dataprovider-retrofit/build.gradle.kts b/dataprovider-retrofit/build.gradle.kts index ae0ecda..00d874d 100644 --- a/dataprovider-retrofit/build.gradle.kts +++ b/dataprovider-retrofit/build.gradle.kts @@ -40,16 +40,16 @@ android { dependencies { api(project(mapOf("path" to ":data"))) implementation("androidx.core:core-ktx:${Versions.coreKtx}") - api("androidx.lifecycle:lifecycle-livedata-ktx:${Versions.lifecycle_version}") + api("androidx.lifecycle:lifecycle-livedata-ktx:${Versions.lifecycle}") implementation("com.google.dagger:dagger:${Versions.dagger}") kapt("com.google.dagger:dagger-compiler:${Versions.dagger}") //retrofit implementation - api("com.squareup.retrofit2:retrofit:${Versions.retrofit_version}") - api("com.squareup.retrofit2:converter-gson:${Versions.retrofit_version}") + api("com.squareup.retrofit2:retrofit:${Versions.retrofit}") + api("com.squareup.retrofit2:converter-gson:${Versions.retrofit}") //noinspection GradleDependency - implementation("com.squareup.okhttp3:logging-interceptor:${Versions.okHttp_version}") + implementation("com.squareup.okhttp3:logging-interceptor:${Versions.okHttp}") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") diff --git a/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/dependencies/components/DataProviderDependencies.java b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/dependencies/components/DataProviderDependencies.java new file mode 100644 index 0000000..292c887 --- /dev/null +++ b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/dependencies/components/DataProviderDependencies.java @@ -0,0 +1,22 @@ +package ch.srg.dataProvider.integrationlayer.dependencies.components; + +import android.app.Application; + +import androidx.annotation.NonNull; + +import ch.srg.dataProvider.integrationlayer.data.IlHost; +import ch.srg.dataProvider.integrationlayer.dependencies.modules.IlAppModule; +import ch.srg.dataProvider.integrationlayer.dependencies.modules.SRGConfigModule; + +final public class DataProviderDependencies { + + private DataProviderDependencies() { + } + + public static IlDataProviderComponent create(@NonNull Application application, @NonNull IlHost ilHost) { + return DaggerIlDataProviderComponent.builder() + .ilAppModule(new IlAppModule(application)) + .sRGConfigModule(new SRGConfigModule(ilHost)) + .build(); + } +} diff --git a/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/parameters/IlPaging.kt b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/parameters/IlPaging.kt index 7e8d597..eb0f9fa 100644 --- a/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/parameters/IlPaging.kt +++ b/dataprovider-retrofit/src/main/java/ch/srg/dataProvider/integrationlayer/request/parameters/IlPaging.kt @@ -8,4 +8,6 @@ package ch.srg.dataProvider.integrationlayer.request.parameters sealed class IlPaging(value: String) : IlParam(value) { object Unlimited : IlPaging("unlimited") class Size(pageSize: Int) : IlPaging(pageSize.toString()) + + fun Int.toIlPaging() = Size(this) } diff --git a/dataproviderdemo/build.gradle.kts b/dataproviderdemo/build.gradle.kts index e03963e..e3ce5b0 100644 --- a/dataproviderdemo/build.gradle.kts +++ b/dataproviderdemo/build.gradle.kts @@ -52,6 +52,7 @@ android { dependencies { implementation(project(mapOf("path" to ":dataprovider-retrofit"))) + implementation(project(mapOf("path" to ":dataprovider-paging"))) implementation("androidx.core:core-ktx:${Versions.coreKtx}") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.9.0") diff --git a/dataproviderdemo/src/main/java/ch/srgssr/dataprovider/demo/MainActivity.kt b/dataproviderdemo/src/main/java/ch/srgssr/dataprovider/demo/MainActivity.kt index 340069b..f815a55 100644 --- a/dataproviderdemo/src/main/java/ch/srgssr/dataprovider/demo/MainActivity.kt +++ b/dataproviderdemo/src/main/java/ch/srgssr/dataprovider/demo/MainActivity.kt @@ -1,11 +1,32 @@ package ch.srgssr.dataprovider.demo import android.os.Bundle +import android.util.Log import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import ch.srg.dataProvider.integrationlayer.data.IlHost +import ch.srg.dataProvider.integrationlayer.dependencies.components.DataProviderDependencies +import ch.srg.dataProvider.integrationlayer.dependencies.components.IlDataProviderComponent +import ch.srg.dataProvider.integrationlayer.request.parameters.Bu +import ch.srgssr.dataprovider.paging.dependencies.DataProviderPagingComponent +import ch.srgssr.dataprovider.paging.dependencies.DataProviderPagingDependencies +import kotlinx.coroutines.flow.collectLatest class MainActivity : AppCompatActivity() { + + private lateinit var ilDataComponent: IlDataProviderComponent + private lateinit var dataProviderComponent: DataProviderPagingComponent + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + ilDataComponent = DataProviderDependencies.create(application, IlHost.PROD) + dataProviderComponent = DataProviderPagingDependencies.create(ilDataComponent) + + lifecycleScope.launchWhenCreated { + dataProviderComponent.dataProviderPaging.getTvSoonExpiringMedias(Bu.RTS).collectLatest { + Log.d("Coucou", "Data loaded ") + } + } } } diff --git a/settings.gradle b/settings.gradle index 248c9ac..bf18c24 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,3 +16,4 @@ rootProject.name = "SRGDataProvider" include ':dataproviderdemo' include ':data' include ':dataprovider-retrofit' +include ':dataprovider-paging'