diff --git a/library/initial-content/src/main/kotlin/org/cru/godtools/init/content/InitialContentImporter.kt b/library/initial-content/src/main/kotlin/org/cru/godtools/init/content/InitialContentImporter.kt index 4efa37f208..07ee788ba9 100644 --- a/library/initial-content/src/main/kotlin/org/cru/godtools/init/content/InitialContentImporter.kt +++ b/library/initial-content/src/main/kotlin/org/cru/godtools/init/content/InitialContentImporter.kt @@ -2,23 +2,38 @@ package org.cru.godtools.init.content import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.launch import org.cru.godtools.init.content.task.Tasks @Singleton -class InitialContentImporter @Inject internal constructor(tasks: Tasks) { - private val coroutineScope = CoroutineScope(Dispatchers.IO) +class InitialContentImporter internal constructor(tasks: Tasks, dispatcher: CoroutineDispatcher) { + @Inject + internal constructor(tasks: Tasks) : this(tasks, Dispatchers.IO) + + private val coroutineScope = CoroutineScope(dispatcher) init { coroutineScope.launch { - launch { tasks.loadBundledLanguages() } + val tools = async { tasks.loadBundledTools() } + val languages = launch { tasks.loadBundledLanguages() } - tasks.loadBundledResources() - launch { tasks.initFavoriteTools() } - launch { tasks.importBundledAttachments() } - launch { tasks.importBundledTranslations() } + launch { + tools.join() + tasks.initFavoriteTools() + } + launch { + tasks.loadBundledAttachments(tools.await()) + tasks.importBundledAttachments() + } + launch { + languages.join() + tasks.loadBundledTranslations(tools.await()) + tasks.importBundledTranslations() + } } } } diff --git a/library/initial-content/src/main/kotlin/org/cru/godtools/init/content/task/Tasks.kt b/library/initial-content/src/main/kotlin/org/cru/godtools/init/content/task/Tasks.kt index 67e9a794c9..7cd6c218fc 100644 --- a/library/initial-content/src/main/kotlin/org/cru/godtools/init/content/task/Tasks.kt +++ b/library/initial-content/src/main/kotlin/org/cru/godtools/init/content/task/Tasks.kt @@ -62,17 +62,22 @@ internal class Tasks @Inject constructor( // endregion Language Initial Content Tasks // region Tool Initial Content Tasks - suspend fun loadBundledResources() = withContext(Dispatchers.IO) { + suspend fun loadBundledTools(): List { // short-circuit if we already have any resources loaded - if (toolsRepository.getAllTools().isNotEmpty()) return@withContext - - bundledTools.let { resources -> - toolsRepository.storeInitialTools(resources) - translationsRepository.storeInitialTranslations( - resources.flatMap { it.translations.orEmpty().filter { it.isValid } } - ) - attachmentsRepository.storeInitialAttachments(resources.flatMap { it.attachments.orEmpty() }) - } + if (toolsRepository.getAllTools().isNotEmpty()) return emptyList() + + return readBundledTools() + .also { toolsRepository.storeInitialTools(it) } + } + + suspend fun loadBundledAttachments(tools: List) { + attachmentsRepository.storeInitialAttachments(tools.flatMap { it.attachments.orEmpty() }) + } + + suspend fun loadBundledTranslations(tools: List) { + translationsRepository.storeInitialTranslations( + tools.flatMap { it.translations.orEmpty().filter { it.isValid } } + ) } suspend fun initFavoriteTools() { @@ -82,7 +87,7 @@ internal class Tasks @Inject constructor( coroutineScope { val preferred = async { - bundledTools.sortedBy { it.initialFavoritesPriority ?: Int.MAX_VALUE }.mapNotNull { it.code } + readBundledTools().sortedBy { it.initialFavoritesPriority ?: Int.MAX_VALUE }.mapNotNull { it.code } } val available = translationsRepository.getTranslationsForLanguages(listOf(settings.appLanguage)) .mapNotNullTo(mutableSetOf()) { it.toolCode } @@ -97,8 +102,8 @@ internal class Tasks @Inject constructor( lastSyncTimeRepository.updateLastSyncTime(SYNC_TIME_DEFAULT_TOOLS) } - private val bundledTools: List - get() = try { + private suspend fun readBundledTools(): List = withContext(Dispatchers.IO) { + try { context.assets.open("tools.json").reader().use { it.readText() } .let { jsonApiConverter.fromJson(it, Tool::class.java) } .data @@ -108,6 +113,7 @@ internal class Tasks @Inject constructor( Timber.tag(TAG).e(e, "Error parsing bundled tools") emptyList() } + } // endregion Tool Initial Content Tasks suspend fun importBundledAttachments() = withContext(Dispatchers.IO) { @@ -118,7 +124,7 @@ internal class Tasks @Inject constructor( attachmentsRepository.getAttachments() .filter { !it.isDownloaded && it.localFilename in files } .forEach { attachment -> - launch(Dispatchers.IO) { + launch { context.assets.open("attachments/${attachment.localFilename}").use { downloadManager.importAttachment(attachment.id, data = it) } @@ -129,23 +135,24 @@ internal class Tasks @Inject constructor( } } - suspend fun importBundledTranslations() = try { - withContext(Dispatchers.IO) { - context.assets.list("translations")?.forEach { file -> - launch { - // load the translation unless it's downloaded already - val id = file.substring(0, file.lastIndexOf('.')).toLongOrNull() - val translation = id?.let { translationsRepository.findTranslation(id) } - ?.takeUnless { it.isDownloaded } ?: return@launch - - // short-circuit if a newer translation is already downloaded - val toolCode = translation.toolCode ?: return@launch - val languageCode = translation.languageCode - val latestTranslation = - translationsRepository.findLatestTranslation(toolCode, languageCode, downloadedOnly = true) - if (latestTranslation != null && latestTranslation.version >= translation.version) return@launch - - withContext(Dispatchers.IO) { + suspend fun importBundledTranslations() { + try { + withContext(Dispatchers.IO) { + context.assets.list("translations")?.forEach { file -> + launch { + // load the translation unless it's downloaded already + val id = file.substring(0, file.lastIndexOf('.')).toLongOrNull() + val translation = id?.let { translationsRepository.findTranslation(id) } + ?.takeUnless { it.isDownloaded } ?: return@launch + + // short-circuit if a newer translation is already downloaded + val toolCode = translation.toolCode ?: return@launch + val languageCode = translation.languageCode + val latestTranslation = + translationsRepository.findLatestTranslation(toolCode, languageCode, downloadedOnly = true) + if (latestTranslation != null && latestTranslation.version >= translation.version) return@launch + + // actually open and import the translation try { context.assets.open("translations/$file") .use { downloadManager.importTranslation(translation, it, -1) } @@ -162,8 +169,8 @@ internal class Tasks @Inject constructor( } } } + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Error importing bundled translations") } - } catch (e: Exception) { - Timber.tag(TAG).e(e, "Error importing bundled translations") } } diff --git a/library/initial-content/src/test/kotlin/org/cru/godtools/init/content/InitialContentImporterTest.kt b/library/initial-content/src/test/kotlin/org/cru/godtools/init/content/InitialContentImporterTest.kt new file mode 100644 index 0000000000..97d670c591 --- /dev/null +++ b/library/initial-content/src/test/kotlin/org/cru/godtools/init/content/InitialContentImporterTest.kt @@ -0,0 +1,130 @@ +package org.cru.godtools.init.content + +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyAll +import io.mockk.coVerifyOrder +import io.mockk.excludeRecords +import io.mockk.just +import io.mockk.mockk +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.cru.godtools.init.content.task.Tasks + +@OptIn(ExperimentalCoroutinesApi::class) +class InitialContentImporterTest { + private val toolsSemaphore = Semaphore(1, 1) + private val languagesSemaphore = Semaphore(1, 1) + + private val tasks: Tasks = mockk { + coEvery { loadBundledTools() } coAnswers { + toolsSemaphore.acquire() + emptyList() + } + coEvery { loadBundledLanguages() } coAnswers { languagesSemaphore.acquire() } + coEvery { loadBundledAttachments(any()) } just Runs + coEvery { loadBundledTranslations(any()) } just Runs + coEvery { initFavoriteTools() } just Runs + coEvery { importBundledAttachments() } just Runs + coEvery { importBundledTranslations() } just Runs + + excludeRecords { this@mockk.toString() } + } + + @Test + fun `Verify All Tasks run`() = runTest { + toolsSemaphore.release() + languagesSemaphore.release() + InitialContentImporter(tasks, UnconfinedTestDispatcher(testScheduler)) + + coVerifyAll { + tasks.loadBundledLanguages() + tasks.loadBundledTools() + tasks.initFavoriteTools() + tasks.loadBundledAttachments(any()) + tasks.loadBundledTranslations(any()) + tasks.importBundledAttachments() + tasks.importBundledTranslations() + } + } + + @Test + fun `Favorite Tools - Dependent on Tools being loaded`() = runTest { + InitialContentImporter(tasks, UnconfinedTestDispatcher(testScheduler)) + + coVerify { tasks.loadBundledTools() } + coVerify(exactly = 0) { tasks.initFavoriteTools() } + + toolsSemaphore.release() + coVerifyOrder { + tasks.loadBundledTools() + tasks.initFavoriteTools() + } + } + + @Test + fun `Bundled Attachments - Dependent on Tools being loaded`() = runTest { + InitialContentImporter(tasks, UnconfinedTestDispatcher(testScheduler)) + + coVerify { tasks.loadBundledTools() } + coVerify(exactly = 0) { + tasks.loadBundledAttachments(any()) + tasks.importBundledAttachments() + } + + toolsSemaphore.release() + coVerifyOrder { + tasks.loadBundledTools() + tasks.loadBundledAttachments(any()) + tasks.importBundledAttachments() + } + } + + @Test + fun `Bundled Translations - Dependent on Tools being loaded`() = runTest { + languagesSemaphore.release() + InitialContentImporter(tasks, UnconfinedTestDispatcher(testScheduler)) + + coVerify { + tasks.loadBundledTools() + tasks.loadBundledLanguages() + } + coVerify(exactly = 0) { + tasks.loadBundledTranslations(any()) + tasks.importBundledTranslations() + } + + toolsSemaphore.release() + coVerifyOrder { + tasks.loadBundledTools() + tasks.loadBundledTranslations(any()) + tasks.importBundledTranslations() + } + } + + @Test + fun `Bundled Translations - Dependent on Languages being loaded`() = runTest { + toolsSemaphore.release() + InitialContentImporter(tasks, UnconfinedTestDispatcher(testScheduler)) + + coVerify { + tasks.loadBundledTools() + tasks.loadBundledLanguages() + } + coVerify(exactly = 0) { + tasks.loadBundledTranslations(any()) + tasks.importBundledTranslations() + } + + languagesSemaphore.release() + coVerifyOrder { + tasks.loadBundledTools() + tasks.loadBundledTranslations(any()) + tasks.importBundledTranslations() + } + } +}