diff --git a/app/src/main/java/fr/boitakub/bogadex/MainActivity.kt b/app/src/main/java/fr/boitakub/bogadex/MainActivity.kt index a77c4a5..4ebecd8 100644 --- a/app/src/main/java/fr/boitakub/bogadex/MainActivity.kt +++ b/app/src/main/java/fr/boitakub/bogadex/MainActivity.kt @@ -46,6 +46,8 @@ import dagger.hilt.InstallIn import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.components.ActivityComponent +import fr.boitakub.bogadex.WorkManagerScheduler.refreshUpdateExistingBoardGameWork +import fr.boitakub.bogadex.WorkManagerScheduler.upsetMissingBoardGame import fr.boitakub.bogadex.boardgame.BoardGameCollectionRepository import fr.boitakub.bogadex.boardgame.ui.BoardGameCollectionNavigation import fr.boitakub.bogadex.boardgame.ui.BoardGameCollectionViewModel @@ -74,6 +76,8 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + upsetMissingBoardGame(this) + refreshUpdateExistingBoardGameWork(this) setContent { val settingsState by userSettingsFlow.collectAsStateWithLifecycle( diff --git a/feature/boardgame/src/main/java/fr/boitakub/bogadex/boardgame/UpdateBoardGameIntentWorker.kt b/app/src/main/java/fr/boitakub/bogadex/UpdateExistingBoardGameWork.kt similarity index 68% rename from feature/boardgame/src/main/java/fr/boitakub/bogadex/boardgame/UpdateBoardGameIntentWorker.kt rename to app/src/main/java/fr/boitakub/bogadex/UpdateExistingBoardGameWork.kt index 190c9be..e9ba086 100644 --- a/feature/boardgame/src/main/java/fr/boitakub/bogadex/boardgame/UpdateBoardGameIntentWorker.kt +++ b/app/src/main/java/fr/boitakub/bogadex/UpdateExistingBoardGameWork.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022, Boitakub + * Copyright (c) 2021-2023, Boitakub * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -26,59 +26,52 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -package fr.boitakub.bogadex.boardgame +package fr.boitakub.bogadex import android.content.Context import android.util.Log import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import com.tickaroo.tikxml.TikXml import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import fr.boitakub.bgg.client.BggGameInfoResult import fr.boitakub.bgg.client.BggService -import fr.boitakub.bogadex.boardgame.mapper.BoardGameMapper +import fr.boitakub.bogadex.boardgame.BoardGameDao +import fr.boitakub.bogadex.boardgame.work.scheduleRetrieveMissingBoardGameWork import kotlinx.coroutines.CancellationException -import okio.buffer -import okio.sink -import java.io.File @HiltWorker -class UpdateBoardGameIntentWorker @AssistedInject constructor( +class UpdateExistingBoardGameWork @AssistedInject constructor( @Assisted val context: Context, @Assisted val workerParams: WorkerParameters, val service: BggService, val database: BoardGameDao, - val mapper: BoardGameMapper ) : CoroutineWorker(context, workerParams) { + + companion object { + const val MAX_GAME_TO_REFRESH_PER_CYCLE = 30 + } + override suspend fun doWork(): Result { - if (runAttemptCount > 2) { + if (runAttemptCount > 4) { return Result.failure() } + Log.d("UpdateExistingBoardGameWork", "Starting job") val result = try { - val bggId = inputData.getString("bggId") - val networkResult = service.boardGame(bggId) - if (BuildConfig.DEBUG) writeMockData(bggId, networkResult) - val mappedValues = networkResult?.let { mapper.map(it.games) } - mappedValues?.let { database.insertAllBoardGame(listOf(it)) } + database.getBoardGameByUpdateDate().filter { item -> item.isOutdated() }.shuffled() + .take(MAX_GAME_TO_REFRESH_PER_CYCLE) + .forEach { itemToRefresh -> + Log.d("UpdateExistingBoardGameWork", "Schedule retrieving of : " + itemToRefresh.bggId) + scheduleRetrieveMissingBoardGameWork(context, itemToRefresh.bggId) + } Result.success() } catch (error: IllegalStateException) { if (error is CancellationException) { - Log.d("UpdateBoardGameIntentWorker", "Job was Cancelled....") + Log.d("UpdateExistingBoardGameWork", "Job was Cancelled....") } Result.retry() } return result } - - private fun writeMockData(bggId: String?, result: BggGameInfoResult?) { - val parser: TikXml = TikXml.Builder() - .build() - val file = File(context.filesDir?.path + "/" + File.separator + bggId + ".xml") - file.sink().buffer().use { sink -> - parser.write(sink, result) - } - } } diff --git a/app/src/main/java/fr/boitakub/bogadex/UpsetMissingBoardGameWork.kt b/app/src/main/java/fr/boitakub/bogadex/UpsetMissingBoardGameWork.kt new file mode 100644 index 0000000..c628173 --- /dev/null +++ b/app/src/main/java/fr/boitakub/bogadex/UpsetMissingBoardGameWork.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2021-2023, Boitakub + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of mosquitto nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package fr.boitakub.bogadex + +import android.content.Context +import android.util.Log +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import fr.boitakub.bgg.client.BggService +import fr.boitakub.bogadex.boardgame.BoardGameListDao +import fr.boitakub.bogadex.boardgame.work.scheduleRetrieveMissingBoardGameWork +import kotlinx.coroutines.CancellationException + +@HiltWorker +class UpsetMissingBoardGameWork @AssistedInject constructor( + @Assisted val context: Context, + @Assisted val workerParams: WorkerParameters, + val service: BggService, + val database: BoardGameListDao, +) : CoroutineWorker(context, workerParams) { + + companion object { + const val MAX_GAME_TO_REFRESH_PER_CYCLE = 30 + } + + override suspend fun doWork(): Result { + Log.d("UpsetMissingBoardGameWork", "Starting job") + if (runAttemptCount > 4) { + return Result.failure() + } + + val result = try { + database.collectionWithDetails().filter { item -> item.details == null }.shuffled() + .take(MAX_GAME_TO_REFRESH_PER_CYCLE) + .forEach { itemToRefresh -> + Log.d("UpsetMissingBoardGameWork", "Schedule retrieving of : " + itemToRefresh.item.bggId) + scheduleRetrieveMissingBoardGameWork(context, itemToRefresh.item.bggId) + } + Result.success() + } catch (error: IllegalStateException) { + if (error is CancellationException) { + Log.d("UpsetMissingBoardGameWork", "Job was Cancelled....") + } + Result.retry() + } + return result + } +} diff --git a/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/usecase/RefreshGameDetails.kt b/app/src/main/java/fr/boitakub/bogadex/WorkManagerScheduler.kt similarity index 53% rename from feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/usecase/RefreshGameDetails.kt rename to app/src/main/java/fr/boitakub/bogadex/WorkManagerScheduler.kt index 5e64937..cad7320 100644 --- a/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/usecase/RefreshGameDetails.kt +++ b/app/src/main/java/fr/boitakub/bogadex/WorkManagerScheduler.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, Boitakub + * Copyright (c) 2023, Boitakub * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -26,45 +26,48 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -package fr.boitakub.bogadex.boardgame.usecase +package fr.boitakub.bogadex import android.content.Context import androidx.work.Constraints -import androidx.work.Data +import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager -import dagger.hilt.android.qualifiers.ApplicationContext -import fr.boitakub.bogadex.boardgame.UpdateBoardGameIntentWorker -import fr.boitakub.bogadex.boardgame.model.CollectionItemWithDetails -import javax.inject.Inject -import javax.inject.Singleton +import java.util.concurrent.TimeUnit -@Singleton -class RefreshGameDetails @Inject constructor(@ApplicationContext val context: Context) { +object WorkManagerScheduler { + fun upsetMissingBoardGame(context: Context) { + val myConstraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() - var scheduledRefresh = SCHEDULED_REFRESH_START + val work = PeriodicWorkRequest.Builder(UpsetMissingBoardGameWork::class.java, 15, TimeUnit.MINUTES) + .setConstraints(myConstraints) + .addTag("UpsetMissingBoardGameWork") + .build() - fun apply(input: CollectionItemWithDetails) { - if (scheduledRefresh < SCHEDULED_REFRESH_MAX) { - val data = Data.Builder() - data.putString("bggId", input.item.bggId) - - val request = OneTimeWorkRequestBuilder() - .addTag("Sync") - .setInputData(data.build()) - .setConstraints( - Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), - ) - .build() - - scheduledRefresh++ - WorkManager.getInstance(context).enqueue(request) - } + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + "UpsetMissingBoardGameWork", + ExistingPeriodicWorkPolicy.UPDATE, + work, + ) } + fun refreshUpdateExistingBoardGameWork(context: Context) { + val myConstraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresCharging(true) + .build() + + val work = PeriodicWorkRequest.Builder(UpdateExistingBoardGameWork::class.java, 24, TimeUnit.HOURS) + .setConstraints(myConstraints) + .addTag("UpdateExistingBoardGameWork") + .build() - companion object { - const val SCHEDULED_REFRESH_START = 0 - const val SCHEDULED_REFRESH_MAX = 20 + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + "UpdateExistingBoardGameWork", + ExistingPeriodicWorkPolicy.UPDATE, + work, + ) } } diff --git a/detekt.yml b/detekt.yml index 8dfb6c2..0e8ac72 100644 --- a/detekt.yml +++ b/detekt.yml @@ -606,7 +606,6 @@ style: ForbiddenComment: active: true values: - - 'TODO:' - 'FIXME:' - 'STOPSHIP:' allowedPatterns: '' diff --git a/feature/boardgame/src/androidTest/java/fr/boitakub/bogadex/boardgame/ui/BoardGameDetailScreenTest.kt b/feature/boardgame/src/androidTest/java/fr/boitakub/bogadex/boardgame/ui/BoardGameDetailScreenTest.kt deleted file mode 100644 index 71f5ba5..0000000 --- a/feature/boardgame/src/androidTest/java/fr/boitakub/bogadex/boardgame/ui/BoardGameDetailScreenTest.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2021-2023, Boitakub - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of mosquitto nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -package fr.boitakub.bogadex.boardgame.ui - -import androidx.compose.material3.MaterialTheme -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.navigation.NavHostController -import fr.boitakub.bgg.client.BggService -import fr.boitakub.bogadex.boardgame.BoardGameDao -import fr.boitakub.bogadex.boardgame.model.BoardGame -import fr.boitakub.bogadex.boardgame.model.BoardGameBggStatistic -import io.mockk.MockKAnnotations -import io.mockk.impl.annotations.MockK -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -class BoardGameDetailScreenTest { - - @get:Rule - val composeTestRule = createComposeRule() - - @MockK - lateinit var navHostController: NavHostController - - @MockK - lateinit var dao: BoardGameDao - - @MockK - lateinit var service: BggService - - @Before - fun setUp() { - MockKAnnotations.init(this, relaxUnitFun = true) - } - - @Test - fun boardGameDetailScreen_shouldDisplayEmptyScreen() { - composeTestRule.setContent { - MaterialTheme { - BoardGameDetailScreen( - navController = navHostController, - BoardGame() - ) - } - } - - composeTestRule.onNodeWithText("Description").assertIsDisplayed() - } - - @Test - fun boardGameDetailScreen_shouldDisplayData() { - composeTestRule.setContent { - MaterialTheme { - BoardGameDetailScreen( - navController = navHostController, - BoardGame( - title = "BoardGame Title", - yearPublished = 2005, - minPlayer = 1, - maxPlayer = 4, - statistic = BoardGameBggStatistic(average = 3.06f), - ), - ) - } - } - composeTestRule.onNodeWithText("Description").assertIsDisplayed() - composeTestRule.onNodeWithText("BoardGame Title").assertIsDisplayed() - composeTestRule.onNodeWithText("Release Date: 2005").assertIsDisplayed() - } -} diff --git a/feature/boardgame/src/main/java/fr/boitakub/bogadex/boardgame/BoardGameDao.kt b/feature/boardgame/src/main/java/fr/boitakub/bogadex/boardgame/BoardGameDao.kt index a9dced3..ccdc84e 100644 --- a/feature/boardgame/src/main/java/fr/boitakub/bogadex/boardgame/BoardGameDao.kt +++ b/feature/boardgame/src/main/java/fr/boitakub/bogadex/boardgame/BoardGameDao.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Boitakub + * Copyright (c) 2021-2023, Boitakub * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -38,6 +38,9 @@ import kotlinx.coroutines.flow.Flow @Dao interface BoardGameDao { + @Query("SELECT * FROM boardgame ORDER BY update_date DESC") + fun getBoardGameByUpdateDate(): List + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAllBoardGame(boardGames: List) diff --git a/feature/boardgame/src/main/java/fr/boitakub/bogadex/boardgame/model/BoardGame.kt b/feature/boardgame/src/main/java/fr/boitakub/bogadex/boardgame/model/BoardGame.kt index b8c49cb..9b96da9 100644 --- a/feature/boardgame/src/main/java/fr/boitakub/bogadex/boardgame/model/BoardGame.kt +++ b/feature/boardgame/src/main/java/fr/boitakub/bogadex/boardgame/model/BoardGame.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022, Boitakub + * Copyright (c) 2021-2023, Boitakub * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/feature/boardgame_list/build.gradle.kts b/feature/boardgame_list/build.gradle.kts index 6651f55..ae3aa63 100644 --- a/feature/boardgame_list/build.gradle.kts +++ b/feature/boardgame_list/build.gradle.kts @@ -59,7 +59,9 @@ dependencies { val roomVersion: String by project val workVersion: String by project val junitVersion: String by project + val testCoreVersion: String by project val espressoVersion: String by project + val mockkVersion: String by project implementation(project(":common")) implementation(project(":shared:architecture")) @@ -123,7 +125,18 @@ dependencies { //region AndroidTest + androidTestImplementation(project(":shared:tests_tools")) + + androidTestImplementation("androidx.test:core-ktx:$testCoreVersion") androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion") + androidTestImplementation("io.mockk:mockk-android:$mockkVersion") + androidTestImplementation("io.mockk:mockk-agent:$mockkVersion") + + androidTestImplementation("androidx.compose.ui:ui-test-junit4:$composeVersion") + debugImplementation("androidx.compose.ui:ui-test-manifest:$composeVersion") + + androidTestImplementation("androidx.work:work-testing:$workVersion") + //endregion } diff --git a/feature/boardgame/src/androidTest/assets/data/86246.xml b/feature/boardgame_list/src/androidTest/assets/data/86246.xml similarity index 100% rename from feature/boardgame/src/androidTest/assets/data/86246.xml rename to feature/boardgame_list/src/androidTest/assets/data/86246.xml diff --git a/feature/boardgame/src/androidTest/java/fr/boitakub/bogadex/boardgame/UpdateBoardGameIntentWorkerImplementationTest.kt b/feature/boardgame_list/src/androidTest/java/fr/boitakub/bogadex/tests/RetrieveMissingBoardGameWorkImplementationTest.kt similarity index 81% rename from feature/boardgame/src/androidTest/java/fr/boitakub/bogadex/boardgame/UpdateBoardGameIntentWorkerImplementationTest.kt rename to feature/boardgame_list/src/androidTest/java/fr/boitakub/bogadex/tests/RetrieveMissingBoardGameWorkImplementationTest.kt index 80d919c..669af93 100644 --- a/feature/boardgame/src/androidTest/java/fr/boitakub/bogadex/boardgame/UpdateBoardGameIntentWorkerImplementationTest.kt +++ b/feature/boardgame_list/src/androidTest/java/fr/boitakub/bogadex/tests/RetrieveMissingBoardGameWorkImplementationTest.kt @@ -26,7 +26,7 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -package fr.boitakub.bogadex.boardgame +package fr.boitakub.bogadex.tests import android.content.Context import androidx.test.core.app.ApplicationProvider @@ -37,7 +37,9 @@ import androidx.work.testing.TestListenableWorkerBuilder import com.tickaroo.tikxml.TikXml import fr.boitakub.bgg.client.BggGameInfoResult import fr.boitakub.bgg.client.BggService +import fr.boitakub.bogadex.boardgame.BoardGameDao import fr.boitakub.bogadex.boardgame.mapper.BoardGameMapper +import fr.boitakub.bogadex.boardgame.work.RetrieveMissingBoardGameWork import fr.boitakub.bogadex.tests.tools.FileReader.readStringFromFile import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -50,7 +52,7 @@ import org.hamcrest.MatcherAssert.assertThat import org.junit.Before import org.junit.Test -class UpdateBoardGameIntentWorkerImplementationTest { +class RetrieveMissingBoardGameWorkImplementationTest { private lateinit var context: Context @MockK @@ -59,13 +61,19 @@ class UpdateBoardGameIntentWorkerImplementationTest { @MockK lateinit var service: BggService - class UpdateBoardGameIntentWorkerFactory(private val dao: BoardGameDao, val service: BggService) : WorkerFactory() { + class RetrieveMissingBoardGameWorkFactory(private val dao: BoardGameDao, val service: BggService) : WorkerFactory() { override fun createWorker( appContext: Context, workerClassName: String, workerParameters: WorkerParameters ): ListenableWorker { - return UpdateBoardGameIntentWorker(appContext, workerParameters, service, dao, BoardGameMapper()) + return RetrieveMissingBoardGameWork( + appContext, + workerParameters, + service, + dao, + BoardGameMapper() + ) } } @@ -85,8 +93,8 @@ class UpdateBoardGameIntentWorkerImplementationTest { fun updateBoardGameIntentWorker_should_returnSuccess() { coEvery { service.boardGame(any()) } answers { readStubData("86246") } - val worker = TestListenableWorkerBuilder(context) - .setWorkerFactory(UpdateBoardGameIntentWorkerFactory(dao, service)) + val worker = TestListenableWorkerBuilder(context) + .setWorkerFactory(RetrieveMissingBoardGameWorkFactory(dao, service)) .build() runBlocking { val result = worker.doWork() @@ -101,8 +109,8 @@ class UpdateBoardGameIntentWorkerImplementationTest { fun updateBoardGameIntentWorker_shouldRetryOnce_OnError() { coEvery { service.boardGame(any()) } throws IllegalStateException() - val worker = TestListenableWorkerBuilder(context) - .setWorkerFactory(UpdateBoardGameIntentWorkerFactory(dao, service)) + val worker = TestListenableWorkerBuilder(context) + .setWorkerFactory(RetrieveMissingBoardGameWorkFactory(dao, service)) .build() runBlocking { val result = worker.doWork() @@ -117,8 +125,8 @@ class UpdateBoardGameIntentWorkerImplementationTest { fun updateBoardGameIntentWorker_shouldNotRetryTwice_OnTwosError() { coEvery { service.boardGame(any()) } throws IllegalStateException() - val worker = TestListenableWorkerBuilder(context) - .setWorkerFactory(UpdateBoardGameIntentWorkerFactory(dao, service)) + val worker = TestListenableWorkerBuilder(context) + .setWorkerFactory(RetrieveMissingBoardGameWorkFactory(dao, service)) .setRunAttemptCount(3) .build() runBlocking { diff --git a/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/BoardGameCollectionRepository.kt b/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/BoardGameCollectionRepository.kt index 1d4bd56..a6b01cc 100644 --- a/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/BoardGameCollectionRepository.kt +++ b/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/BoardGameCollectionRepository.kt @@ -38,11 +38,9 @@ import fr.boitakub.bogadex.boardgame.mapper.CollectionMapper import fr.boitakub.bogadex.boardgame.model.BoardGame import fr.boitakub.bogadex.boardgame.model.CollectionItem import fr.boitakub.bogadex.boardgame.model.CollectionItemWithDetails -import fr.boitakub.bogadex.boardgame.usecase.RefreshGameDetails import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.onEach import okio.buffer import okio.sink import java.io.File @@ -53,17 +51,10 @@ class BoardGameCollectionRepository @Inject constructor( override val local: BoardGameListDao, override val remote: BggService, private val mapper: CollectionMapper, - private val refreshGameDetails: RefreshGameDetails, ) : Repository { fun get(user: String): Flow> = - local.getCollectionWithDetails().onEach { collection -> - collection.shuffled().forEach { - if (it.details == null || it.details?.isOutdated() == true) { - refreshGameDetails.apply(it) - } - } - }.combine(getAllRemoteResorts(user)) { local, remote -> + local.collectionWithDetailsFlow().combine(getAllRemoteResorts(user)) { local, remote -> toUiModel(local, remote) } diff --git a/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/BoardGameListDao.kt b/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/BoardGameListDao.kt index d410dd9..3040d47 100644 --- a/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/BoardGameListDao.kt +++ b/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/BoardGameListDao.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Boitakub + * Copyright (c) 2021-2023, Boitakub * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -46,7 +46,11 @@ interface BoardGameListDao { @Transaction @Query("SELECT * FROM collection_item") - fun getCollectionWithDetails(): Flow> + fun collectionWithDetailsFlow(): Flow> + + @Transaction + @Query("SELECT * FROM collection_item") + fun collectionWithDetails(): List @Transaction suspend fun updateCollection(boardGames: List) { diff --git a/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/usecase/RefreshCollection.kt b/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/usecase/RefreshCollection.kt new file mode 100644 index 0000000..cbdb886 --- /dev/null +++ b/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/usecase/RefreshCollection.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023, Boitakub + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of mosquitto nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package fr.boitakub.bogadex.boardgame.usecase + +class RefreshCollection diff --git a/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/work/RetrieveMissingBoardGameWork.kt b/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/work/RetrieveMissingBoardGameWork.kt new file mode 100644 index 0000000..24a2569 --- /dev/null +++ b/feature/boardgame_list/src/main/java/fr/boitakub/bogadex/boardgame/work/RetrieveMissingBoardGameWork.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2021-2023, Boitakub + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of mosquitto nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package fr.boitakub.bogadex.boardgame.work + +import android.content.Context +import android.util.Log +import androidx.hilt.work.HiltWorker +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.tickaroo.tikxml.TikXml +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import fr.boitakub.bgg.client.BggGameInfoResult +import fr.boitakub.bgg.client.BggService +import fr.boitakub.bogadex.boardgame.BoardGameDao +import fr.boitakub.bogadex.boardgame.BuildConfig +import fr.boitakub.bogadex.boardgame.mapper.BoardGameMapper +import okio.buffer +import okio.sink +import java.io.File +import java.util.concurrent.CancellationException + +@HiltWorker +class RetrieveMissingBoardGameWork @AssistedInject constructor( + @Assisted val context: Context, + @Assisted val workerParams: WorkerParameters, + val service: BggService, + val database: BoardGameDao, + val mapper: BoardGameMapper, +) : CoroutineWorker(context, workerParams) { + + companion object { + const val MAX_RETRIEVE_REQUEST_PER_SESSION = 30 + } + + override suspend fun doWork(): Result { + Log.d("RetrieveMissingBoardGameWork", "Checking") + if (runAttemptCount > 4) { + return Result.failure() + } + Log.d("RetrieveMissingBoardGameWork", "Starting job") + + val result = try { + val bggId = inputData.getString("bggId") + val networkResult = service.boardGame(bggId) + Log.d("RetrieveMissingBoardGameWork", "Retrieving : $bggId") + val mappedValues = networkResult?.let { mapper.map(it.games) } + if (BuildConfig.DEBUG) writeMockData(bggId, networkResult) + mappedValues?.let { database.insertAllBoardGame(listOf(it)) } + Result.success() + } catch (error: IllegalStateException) { + if (error is CancellationException) { + Log.d("RetrieveMissingBoardGameWork", "Job was Cancelled....") + } + Result.retry() + } + return result + } + + private fun writeMockData(bggId: String?, result: BggGameInfoResult?) { + val parser: TikXml = TikXml.Builder() + .build() + val file = File(context.filesDir?.path + "/" + File.separator + bggId + ".xml") + file.sink().buffer().use { sink -> + parser.write(sink, result) + } + } +} + +fun scheduleRetrieveMissingBoardGameWork(context: Context, bggId: String) { + val data = Data.Builder() + data.putString("bggId", bggId) + + // define constraints + val myConstraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val refreshCpnWork = OneTimeWorkRequestBuilder() + .setConstraints(myConstraints) + .setInputData(data.build()) + .addTag("RetrieveMissingBoardGameWork$bggId") + .build() + + WorkManager.getInstance(context).enqueueUniqueWork( + "RetrieveMissingBoardGameWork$bggId", + ExistingWorkPolicy.REPLACE, + refreshCpnWork, + ) +} diff --git a/gradle.properties b/gradle.properties index 522e24a..2b26e74 100644 --- a/gradle.properties +++ b/gradle.properties @@ -56,12 +56,12 @@ navigationVersion=2.5.3 materialVersion=1.1.0-beta01 coilVersion=2.3.0 composeVersion=1.4.0-rc01 -daggerVersion=2.45 +daggerVersion=2.48.1 hiltVersion=1.0.0 okhttpVersion=4.10.0 retrofitVersion=2.9.0 roomVersion=2.5.0 -workVersion=2.8.0 +workVersion=2.8.1 firebaseVersion=31.2.3 lifecycleVersion=2.6.0