From 62c9e89ddb5fea2470d0d907aaa19ef9fa1d7ba3 Mon Sep 17 00:00:00 2001 From: Elly Kitoto Date: Fri, 20 Sep 2024 16:08:07 +0300 Subject: [PATCH] Refactor register filter with REL tags (#3488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor register filter with REL tags Signed-off-by: Elly Kitoto * Update default pageSize to 15 Signed-off-by: Elly Kitoto * Fix loading locations on map Signed-off-by: Elly Kitoto * Refactor data structure used on base resource search results Signed-off-by: Elly Kitoto * Refactor code Signed-off-by: Elly Kitoto * Delete unnecessary code Signed-off-by: Elly Kitoto * Refactor retrieving related resources Signed-off-by: Elly Kitoto * Optimize data structures and perform parallel processing Signed-off-by: Elly Kitoto * Use recent version of rules engine library Signed-off-by: Elly Kitoto * Refactor implementation for decoding image resources to bitmap Signed-off-by: Elly Kitoto * Fix related resource count on register Signed-off-by: Elly Kitoto * Fix loading related resources This fix ensures all the nested related resources are loaded too. Signed-off-by: Elly Kitoto * Batch related resource queries Signed-off-by: Elly Kitoto * Map resources to RepositoryResourceData with async map Signed-off-by: Elly Kitoto * Make infinite scroll the default register behavior Signed-off-by: Elly Kitoto * Disable automatic intialization of emoji2 A lot of memory was used in heap during the allocation. Emojis are not used in the app so intializing them automatically is unnecessary. Signed-off-by: Elly Kitoto * :arrow_up: Update the map box and kujaku versions * Run spotlessApply Signed-off-by: Elly Kitoto * Update tests for displaying images (#3506) * Refactor load images tests for different views. Signed-off-by: Lentumunai-Mark * Remove unutilized imports. Signed-off-by: Lentumunai-Mark * Run spotlessApply Signed-off-by: Elly Kitoto Signed-off-by: Lentumunai-Mark * Refactor load images tests for different views. Signed-off-by: Lentumunai-Mark * Resolve conflicts. Signed-off-by: Lentumunai-Mark --------- Signed-off-by: Lentumunai-Mark Signed-off-by: Elly Kitoto * Refactor navigation to GeowidgetLauncher workflow Signed-off-by: Elly Kitoto * Load map data in batches (#3511) * Load map data in batches Signed-off-by: Elly Kitoto * Update observer Signed-off-by: Elly Kitoto * Deactivate infinite scroll by default Signed-off-by: Elly Kitoto --------- Signed-off-by: Elly Kitoto * Fix failing tests Signed-off-by: Elly Kitoto * Batch searching when list might be large or when count is not defined (#3456) * Fetch search results in batches for when loading all * Fix infinite loop for mocks with FhirEngine#search in tests * Add comparable FhirEngine#search vs batchedSearch integration tests * Run spotlessApply Signed-off-by: Elly Kitoto * Fix Geowidget tests Signed-off-by: Elly Kitoto * Fix spotlessCheck Signed-off-by: Elly Kitoto * Add missing import Signed-off-by: Elly Kitoto * Fix rules execution before map render Signed-off-by: Elly Kitoto * - Update android manifest to use exported values * Prevent leaking map features via viewmodel Signed-off-by: Elly Kitoto * Format code Signed-off-by: Elly Kitoto * Only search map features via keyboard action We need to reload all features when the search term is reset Signed-off-by: Elly Kitoto --------- Signed-off-by: Elly Kitoto Signed-off-by: Lentumunai-Mark Co-authored-by: Benjamin Mwalimu Co-authored-by: Lentumunai Mark <90028422+Lentumunai-Mark@users.noreply.github.com> Co-authored-by: L≡ZRS <12814349+LZRS@users.noreply.github.com> --- android/engine/build.gradle.kts | 6 +- .../extension/FhirEngineExtensionKtTest.kt | 101 +++ .../configuration/ConfigurationRegistry.kt | 26 +- .../navigation/NavigationMenuConfig.kt | 6 +- .../register/RegisterConfiguration.kt | 1 + .../configuration/view/ButtonProperties.kt | 4 +- .../configuration/view/ViewProperties.kt | 16 +- .../view/ViewPropertiesSerializer.kt | 8 +- .../engine/data/local/DefaultRepository.kt | 749 ++++++++++-------- .../data/local/register/RegisterRepository.kt | 42 +- .../data/remote/shared/TokenAuthenticator.kt | 4 +- .../fhircore/engine/di/NetworkModule.kt | 8 +- .../engine/p2p/dao/BaseP2PTransferDao.kt | 3 +- .../fhircore/engine/pdf/HtmlPopulator.kt | 4 +- .../engine/rulesengine/ConfigRulesExecutor.kt | 4 +- .../rulesengine/ResourceDataRulesExecutor.kt | 4 +- .../engine/rulesengine/RulesFactory.kt | 11 +- .../engine/task/FhirCarePlanGenerator.kt | 8 +- .../engine/task/FhirCompleteCarePlanWorker.kt | 4 +- .../fhircore/engine/task/FhirResourceUtil.kt | 8 +- .../fhircore/engine/ui/base/AlertDialogue.kt | 4 +- .../fhircore/engine/ui/components/Pin.kt | 4 +- .../ui/components/register/RegisterFooter.kt | 17 +- .../engine/ui/multiselect/MultiSelectView.kt | 13 +- .../util/extension/AndroidExtensions.kt | 7 - .../engine/util/extension/BitmapExtension.kt | 4 +- .../util/extension/FhirEngineExtension.kt | 37 +- .../util/extension/MeasureExtensions.kt | 5 +- .../util/extension/QuestionnaireExtension.kt | 4 +- .../util/extension/ReferenceExtension.kt | 3 +- .../util/extension/ResourceExtension.kt | 7 +- .../data/local/DefaultRepositoryTest.kt | 14 +- .../engine/task/FhirCarePlanGeneratorTest.kt | 12 +- .../geowidget/src/main/AndroidManifest.xml | 4 +- .../src/main/assets/conversion_config.json | 8 - .../geowidget/screens/GeoWidgetFragment.kt | 152 ++-- .../geowidget/screens/GeoWidgetViewModel.kt | 33 +- .../geowidget/rule/CoroutineTestRule.kt | 40 +- .../screens/GeoWidgetViewModelTest.kt | 15 +- android/gradle/libs.versions.toml | 13 +- .../main/components/TopScreenSectionTest.kt | 4 +- android/quest/src/main/AndroidManifest.xml | 8 + .../fhircore/quest/data/DataMigration.kt | 4 +- .../data/geowidget/GeoWidgetPagingSource.kt | 114 +++ .../data/register/RegisterPagingSource.kt | 17 +- .../ui/appsetting/AppSettingViewModel.kt | 3 +- .../quest/ui/geowidget/GeoWidgetEvent.kt | 27 + .../ui/geowidget/GeoWidgetLauncherFragment.kt | 118 ++- .../ui/geowidget/GeoWidgetLauncherScreen.kt | 18 +- .../geowidget/GeoWidgetLauncherViewModel.kt | 170 ++-- .../fhircore/quest/ui/login/LoginActivity.kt | 4 +- .../fhircore/quest/ui/main/AppMainActivity.kt | 4 +- .../quest/ui/main/AppMainViewModel.kt | 36 +- .../quest/ui/main/components/AppDrawer.kt | 8 +- .../ui/main/components/TopScreenSection.kt | 44 +- .../ui/multiselect/MultiSelectViewModel.kt | 14 +- .../quest/ui/profile/ProfileScreen.kt | 4 +- .../quest/ui/profile/ProfileViewModel.kt | 43 +- .../components/ChangeManagingEntityView.kt | 8 +- .../questionnaire/QuestionnaireViewModel.kt | 8 +- .../quest/ui/register/RegisterFragment.kt | 8 +- .../quest/ui/register/RegisterScreen.kt | 19 +- .../quest/ui/register/RegisterViewModel.kt | 75 +- .../register/components/RegisterCardList.kt | 33 +- .../measure/worker/MeasureReportWorker.kt | 4 +- .../EditTextQrCodeItemViewHolderFactory.kt | 3 +- .../qrCode/EditTextQrCodeViewHolderFactory.kt | 4 +- .../ui/shared/components/ActionableButton.kt | 4 +- .../ui/shared/components/SyncStatusView.kt | 16 +- .../ui/shared/components/ViewRenderer.kt | 4 +- .../quest/util/extensions/ConfigExtensions.kt | 133 ++-- .../fhircore/quest/CqlContentTest.kt | 4 +- .../fhircore/quest/event/EventBusTest.kt | 51 +- .../GeoWidgetLauncherViewModelTest.kt | 16 +- .../ui/register/RegisterViewModelTest.kt | 2 +- .../util/extensions/ConfigExtensionsKtTest.kt | 112 ++- 76 files changed, 1480 insertions(+), 1077 deletions(-) create mode 100644 android/engine/src/androidTest/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtensionKtTest.kt delete mode 100644 android/geowidget/src/main/assets/conversion_config.json create mode 100644 android/quest/src/main/java/org/smartregister/fhircore/quest/data/geowidget/GeoWidgetPagingSource.kt create mode 100644 android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetEvent.kt diff --git a/android/engine/build.gradle.kts b/android/engine/build.gradle.kts index 7393796830..6616981e4a 100644 --- a/android/engine/build.gradle.kts +++ b/android/engine/build.gradle.kts @@ -176,11 +176,7 @@ dependencies { api(libs.timber) api(libs.converter.gson) api(libs.json.path) - api(libs.commons.jexl3) { exclude(group = "commons-logging", module = "commons-logging") } - api(libs.easy.rules.jexl) { - exclude(group = "commons-logging", module = "commons-logging") - exclude(group = "org.apache.commons", module = "commons-jexl3") - } + api(libs.easy.rules.jexl) { exclude(group = "commons-logging", module = "commons-logging") } api(libs.data.capture) { isTransitive = true exclude(group = "ca.uhn.hapi.fhir") diff --git a/android/engine/src/androidTest/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtensionKtTest.kt b/android/engine/src/androidTest/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtensionKtTest.kt new file mode 100644 index 0000000000..8862f5e123 --- /dev/null +++ b/android/engine/src/androidTest/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtensionKtTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.engine.util.extension + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.MediumTest +import com.google.android.fhir.FhirEngine +import com.google.android.fhir.FhirEngineConfiguration +import com.google.android.fhir.FhirEngineProvider +import com.google.android.fhir.search.search +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.Resource +import org.hl7.fhir.r4.model.ResourceType +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +@MediumTest +class FhirEngineExtensionKtTest { + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var fhirEngine: FhirEngine + + @Before + fun setUp() { + FhirEngineProvider.init(FhirEngineConfiguration(testMode = true)) + fhirEngine = FhirEngineProvider.getInstance(context) + + val patients = (0..1000).map { Patient().apply { id = "test-patient-$it" } } + val questionnaires = (0..3).map { Questionnaire().apply { id = "test-questionnaire-$it" } } + runBlocking { fhirEngine.create(*patients.toTypedArray(), *questionnaires.toTypedArray()) } + } + + @After + fun tearDown() { + runBlocking { fhirEngine.clearDatabase() } + FhirEngineProvider.cleanup() + } + + @Test + fun test_search_time_searches_sequentially_and_short_running_query_waits() { + val fetchedResources = mutableListOf() + runBlocking { + launch { + val patients = fhirEngine.search {}.map { it.resource } + fetchedResources += patients + } + + launch { + val questionnaires = fhirEngine.search {}.map { it.resource } + fetchedResources += questionnaires + } + } + val indexOfResultOfShortQuery = + fetchedResources.indexOfFirst { it.resourceType == ResourceType.Questionnaire } + val indexOfResultOfLongQuery = + fetchedResources.indexOfFirst { it.resourceType == ResourceType.Patient } + Assert.assertTrue(indexOfResultOfShortQuery > indexOfResultOfLongQuery) + } + + @Test + fun test_batchedSearch_returns_short_running_query_and_long_running_does_not_block() { + val fetchedResources = mutableListOf() + runBlocking { + launch { + val patients = fhirEngine.batchedSearch {}.map { it.resource } + fetchedResources += patients + } + + launch { + val questionnaires = fhirEngine.search {} + fetchedResources + questionnaires + } + } + + val indexOfResultOfShortQuery = + fetchedResources.indexOfFirst { it.resourceType == ResourceType.Questionnaire } + val indexOfResultOfLongQuery = + fetchedResources.indexOfFirst { it.resourceType == ResourceType.Patient } + Assert.assertTrue(indexOfResultOfShortQuery < indexOfResultOfLongQuery) + } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt index 6dc039191f..82c126faf4 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt @@ -33,7 +33,6 @@ import java.io.File import java.io.FileNotFoundException import java.io.InputStreamReader import java.net.UnknownHostException -import java.util.LinkedList import java.util.Locale import java.util.PropertyResourceBundle import java.util.ResourceBundle @@ -61,8 +60,6 @@ import org.smartregister.fhircore.engine.configuration.app.ApplicationConfigurat import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource import org.smartregister.fhircore.engine.di.NetworkModule -import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig -import org.smartregister.fhircore.engine.domain.model.ResourceConfig import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper @@ -376,19 +373,23 @@ constructor( * @return A list of strings of config files. */ private fun retrieveAssetConfigs(context: Context, appId: String): MutableList { - val filesQueue = LinkedList() + val filesQueue = ArrayDeque() val configFiles = mutableListOf() context.assets.list(String.format(BASE_CONFIG_PATH, appId))?.onEach { if (!supportedFileExtensions.contains(it.fileExtension)) { filesQueue.addLast(String.format(BASE_CONFIG_PATH, appId) + "/$it") - } else configFiles.add(String.format(BASE_CONFIG_PATH, appId) + "/$it") + } else { + configFiles.add(String.format(BASE_CONFIG_PATH, appId) + "/$it") + } } while (filesQueue.isNotEmpty()) { val currentPath = filesQueue.removeFirst() context.assets.list(currentPath)?.onEach { if (!supportedFileExtensions.contains(it.fileExtension)) { filesQueue.addLast("$currentPath/$it") - } else configFiles.add("$currentPath/$it") + } else { + configFiles.add("$currentPath/$it") + } } } return configFiles @@ -510,13 +511,14 @@ constructor( val resultBundle = if (isNonProxy()) { fhirResourceDataSourceGetBundle(resourceType, resourceIdList) - } else + } else { fhirResourceDataSource.post( requestBody = generateRequestBundle(resourceType, resourceIdList) .encodeResourceToString() .toRequestBody(NetworkModule.JSON_MEDIA_TYPE), ) + } processResultBundleEntries(resultBundle.entry) @@ -730,16 +732,6 @@ constructor( } } - private fun FhirResourceConfig.dependentResourceTypes(target: MutableList) { - this.baseResource.dependentResourceTypes(target) - this.relatedResources.forEach { it.dependentResourceTypes(target) } - } - - private fun ResourceConfig.dependentResourceTypes(target: MutableList) { - target.add(resource) - relatedResources.forEach { it.dependentResourceTypes(target) } - } - suspend fun loadResourceSearchParams(): Pair>, ResourceSearchParams> { val syncConfig = retrieveResourceConfiguration(ConfigType.Sync) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt index b2d6ae374a..f545c4481e 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt @@ -22,6 +22,9 @@ import kotlinx.serialization.Serializable import org.smartregister.fhircore.engine.domain.model.ActionConfig import org.smartregister.fhircore.engine.util.extension.interpolate +const val ICON_TYPE_LOCAL = "local" +const val ICON_TYPE_REMOTE = "remote" + @Serializable @Parcelize data class NavigationMenuConfig( @@ -53,9 +56,6 @@ data class ImageConfig( } } -const val ICON_TYPE_LOCAL = "local" -const val ICON_TYPE_REMOTE = "remote" - enum class ImageType { JPEG, PNG, diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/register/RegisterConfiguration.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/register/RegisterConfiguration.kt index a22be805fb..7f6d5c7ddd 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/register/RegisterConfiguration.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/register/RegisterConfiguration.kt @@ -50,6 +50,7 @@ data class RegisterConfiguration( val filterDataByRelatedEntityLocation: Boolean = false, val topScreenSection: TopScreenSectionConfig? = null, val onSearchByQrSingleResultActions: List? = null, + val infiniteScroll: Boolean = false, ) : Configuration() { val onSearchByQrSingleResultValidActions = onSearchByQrSingleResultActions?.filter { it.trigger == ActionTrigger.ON_SEARCH_SINGLE_RESULT } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ButtonProperties.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ButtonProperties.kt index 8996c628f5..5e89b02a09 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ButtonProperties.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ButtonProperties.kt @@ -91,7 +91,9 @@ data class ButtonProperties( val interpolated = this.status.interpolate(computedValuesMap) return if (ServiceStatus.values().map { it.name }.contains(interpolated)) { ServiceStatus.valueOf(interpolated) - } else ServiceStatus.UPCOMING + } else { + ServiceStatus.UPCOMING + } } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewProperties.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewProperties.kt index 13cc346747..30c1059b21 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewProperties.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewProperties.kt @@ -16,7 +16,6 @@ package org.smartregister.fhircore.engine.configuration.view -import java.util.LinkedList import kotlinx.serialization.Serializable import org.smartregister.fhircore.engine.domain.model.ViewType @@ -47,18 +46,17 @@ abstract class ViewProperties : java.io.Serializable { */ fun List.retrieveListProperties(): List { val listProperties = mutableListOf() - val viewPropertiesLinkedList: LinkedList = LinkedList(this) - while (viewPropertiesLinkedList.isNotEmpty()) { - val properties = viewPropertiesLinkedList.removeFirst() + val viewPropertiesQueue: ArrayDeque = ArrayDeque(this) + while (viewPropertiesQueue.isNotEmpty()) { + val properties = viewPropertiesQueue.removeFirst() if (properties.viewType == ViewType.LIST) { listProperties.add(properties as ListProperties) } when (properties.viewType) { - ViewType.COLUMN -> viewPropertiesLinkedList.addAll((properties as ColumnProperties).children) - ViewType.ROW -> viewPropertiesLinkedList.addAll((properties as RowProperties).children) - ViewType.CARD -> viewPropertiesLinkedList.addAll((properties as CardViewProperties).content) - ViewType.LIST -> - viewPropertiesLinkedList.addAll((properties as ListProperties).registerCard.views) + ViewType.COLUMN -> viewPropertiesQueue.addAll((properties as ColumnProperties).children) + ViewType.ROW -> viewPropertiesQueue.addAll((properties as RowProperties).children) + ViewType.CARD -> viewPropertiesQueue.addAll((properties as CardViewProperties).content) + ViewType.LIST -> viewPropertiesQueue.addAll((properties as ListProperties).registerCard.views) else -> {} } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewPropertiesSerializer.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewPropertiesSerializer.kt index 7dec5bc376..7c3a47ab6f 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewPropertiesSerializer.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ViewPropertiesSerializer.kt @@ -29,12 +29,14 @@ object ViewPropertiesSerializer : JsonContentPolymorphicSerializer(ViewProperties::class) { override fun selectDeserializer( element: JsonElement, - ): DeserializationStrategy { + ): DeserializationStrategy { val jsonObject = element.jsonObject val viewType = jsonObject[VIEW_TYPE]?.jsonPrimitive?.content - require(viewType != null && ViewType.values().contains(ViewType.valueOf(viewType))) { + require( + viewType != null && ViewType.entries.toTypedArray().contains(ViewType.valueOf(viewType)), + ) { """Ensure that supported `viewType` property is included in your register view properties configuration. - Supported types: ${ViewType.values()} + Supported types: ${ViewType.entries.toTypedArray()} Parsed JSON: $jsonObject """ .trimMargin() diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt index fd6310ec06..8ce645822c 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/DefaultRepository.kt @@ -26,6 +26,7 @@ import ca.uhn.fhir.rest.gclient.ReferenceClientParam import ca.uhn.fhir.rest.gclient.StringClientParam import ca.uhn.fhir.rest.gclient.TokenClientParam import com.google.android.fhir.FhirEngine +import com.google.android.fhir.SearchResult import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.get @@ -36,7 +37,6 @@ import com.google.android.fhir.search.filter.TokenParamFilterCriterion import com.google.android.fhir.search.has import com.google.android.fhir.search.include import com.google.android.fhir.search.revInclude -import com.google.android.fhir.search.search import com.jayway.jsonpath.Configuration import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.Option @@ -45,7 +45,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import java.util.LinkedList import java.util.UUID import javax.inject.Inject -import kotlinx.coroutines.runBlocking +import kotlin.math.min import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement @@ -56,7 +56,6 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import org.hl7.fhir.instance.model.api.IBaseResource -import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.Group import org.hl7.fhir.r4.model.IdType @@ -86,6 +85,7 @@ import org.smartregister.fhircore.engine.rulesengine.ConfigRulesExecutor import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.asReference +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.encodeResourceToString import org.smartregister.fhircore.engine.util.extension.extractId import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid @@ -97,6 +97,7 @@ import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyn import org.smartregister.fhircore.engine.util.extension.updateFrom import org.smartregister.fhircore.engine.util.extension.updateLastUpdated import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor +import org.smartregister.fhircore.engine.util.pmap import timber.log.Timber open class DefaultRepository @@ -136,7 +137,7 @@ constructor( ): List = withContext(dispatcherProvider.io()) { fhirEngine - .search { + .batchedSearch { filterByResourceTypeId(token, subjectType, subjectId) dataQueries.forEach { filterBy( @@ -149,7 +150,7 @@ constructor( } suspend inline fun search(search: Search) = - fhirEngine.search(search).map { it.resource } + fhirEngine.batchedSearch(search).map { it.resource } suspend inline fun count(search: Search) = fhirEngine.count(search) @@ -265,14 +266,14 @@ constructor( suspend fun loadManagingEntity(group: Group) = group.managingEntity?.let { reference -> fhirEngine - .search { + .batchedSearch { filter(RelatedPerson.RES_ID, { value = of(reference.extractId()) }) } .map { it.resource } .firstOrNull() ?.let { relatedPerson -> fhirEngine - .search { + .batchedSearch { filter( Patient.RES_ID, { value = of(relatedPerson.patient.extractId()) }, @@ -474,94 +475,116 @@ constructor( } protected suspend fun retrieveRelatedResources( - resources: List, + resource: Resource, relatedResourcesConfigs: List?, - relatedResourceWrapper: RelatedResourceWrapper, configComputedRuleValues: Map, ): RelatedResourceWrapper { - val countResourceConfigs = relatedResourcesConfigs?.filter { it.resultAsCount } - countResourceConfigs?.forEach { resourceConfig -> - if (resourceConfig.searchParameter.isNullOrEmpty()) { - Timber.e("Search parameter require to perform count query. Current config: $resourceConfig") + val relatedResourceWrapper = RelatedResourceWrapper() + val relatedResourcesQueue = + ArrayDeque, List?>>().apply { + addFirst(Pair(listOf(resource), relatedResourcesConfigs)) } + while (relatedResourcesQueue.isNotEmpty()) { + val (currentResources, currentRelatedResourceConfigs) = relatedResourcesQueue.removeFirst() + val relatedResourceCountConfigs = + currentRelatedResourceConfigs + ?.asSequence() + ?.filter { it.resultAsCount && !it.searchParameter.isNullOrEmpty() } + ?.toList() + + relatedResourceCountConfigs?.forEach { resourceConfig -> + val search = + Search(resourceConfig.resource).apply { + val filters = + currentResources.map { + val apply: ReferenceParamFilterCriterion.() -> Unit = { + value = it.logicalId.asReference(it.resourceType).reference + } + apply + } + filter( + ReferenceClientParam(resourceConfig.searchParameter), + *filters.toTypedArray(), + ) + applyConfiguredSortAndFilters( + resourceConfig = resourceConfig, + sortData = true, + configComputedRuleValues = configComputedRuleValues, + ) + } - // Count for each related resource or aggregate total count in one query; as configured - if (resourceConfig.resultAsCount && !resourceConfig.searchParameter.isNullOrEmpty()) { + val key = resourceConfig.id ?: resourceConfig.resource.name if (resourceConfig.countResultConfig?.sumCounts == true) { - val search = - Search(resourceConfig.resource).apply { - val filters = - resources.map { - val apply: ReferenceParamFilterCriterion.() -> Unit = { - value = it.logicalId.asReference(it.resourceType).reference - } - apply - } - filter( - ReferenceClientParam(resourceConfig.searchParameter), - *filters.toTypedArray(), - ) - applyConfiguredSortAndFilters( - resourceConfig = resourceConfig, - sortData = true, - configComputedRuleValues = configComputedRuleValues, - ) - } - val key = resourceConfig.id ?: resourceConfig.resource.name search.count( onSuccess = { - relatedResourceWrapper.relatedResourceCountMap[key] = - LinkedList().apply { - add( - RelatedResourceCount( - count = it, - ), - ) - } + relatedResourceWrapper.relatedResourceCountMap + .getOrPut(key) { mutableListOf() } + .apply { add(RelatedResourceCount(count = it)) } }, onFailure = { Timber.e( it, - "Error retrieving total count for all related resourced identified by $key", + "Error retrieving total count for all related resources identified by $key", ) }, ) } else { computeCountForEachRelatedResource( - resources = resources, + resources = currentResources, resourceConfig = resourceConfig, relatedResourceWrapper = relatedResourceWrapper, configComputedRuleValues = configComputedRuleValues, ) } } - } - searchIncludedResources( - relatedResourcesConfigs = relatedResourcesConfigs, - resources = resources, - relatedResourceWrapper = relatedResourceWrapper, - configComputedRuleValues = configComputedRuleValues, - ) + val searchResults = + searchIncludedResources( + relatedResourcesConfigs = currentRelatedResourceConfigs, + resources = currentResources, + configComputedRuleValues = configComputedRuleValues, + ) + val fwdIncludedRelatedConfigsMap = + currentRelatedResourceConfigs + ?.revIncludeRelatedResourceConfigs(false) + ?.groupBy { it.searchParameter!! } + ?.mapValues { it.value.first() } + + val revIncludedRelatedConfigsMap = + currentRelatedResourceConfigs + ?.revIncludeRelatedResourceConfigs(true) + ?.groupBy { "${it.resource.name}_${it.searchParameter}".lowercase() } + ?.mapValues { it.value.first() } + + searchResults.forEach { searchResult -> + searchResult.included?.forEach { entry -> + updateResourceWrapperAndQueue( + key = entry.key, + defaultKey = entry.value.firstOrNull()?.resourceType?.name, + resources = entry.value, + relatedResourcesConfigsMap = fwdIncludedRelatedConfigsMap, + relatedResourceWrapper = relatedResourceWrapper, + relatedResourcesQueue = relatedResourcesQueue, + ) + } + searchResult.revIncluded?.forEach { entry -> + val (resourceType, searchParam) = entry.key + val key = "${resourceType.name}_$searchParam".lowercase() + updateResourceWrapperAndQueue( + key = key, + defaultKey = entry.value.firstOrNull()?.resourceType?.name, + resources = entry.value, + relatedResourcesConfigsMap = revIncludedRelatedConfigsMap, + relatedResourceWrapper = relatedResourceWrapper, + relatedResourcesQueue = relatedResourcesQueue, + ) + } + } + } return relatedResourceWrapper } - protected suspend fun Search.count( - onSuccess: (Long) -> Unit = {}, - onFailure: (Throwable) -> Unit = { throwable -> - Timber.e( - throwable, - "Error counting data", - ) - }, - ): Long = - kotlin - .runCatching { withContext(dispatcherProvider.io()) { fhirEngine.count(this@count) } } - .onSuccess { count -> onSuccess(count) } - .onFailure { throwable -> onFailure(throwable) } - .getOrDefault(0) - private suspend fun computeCountForEachRelatedResource( resources: List, resourceConfig: ResourceConfig, @@ -610,6 +633,46 @@ constructor( relatedResourceWrapper.relatedResourceCountMap[key] = relatedResourceCountLinkedList } + private fun updateResourceWrapperAndQueue( + key: String, + defaultKey: String?, + resources: List, + relatedResourcesConfigsMap: Map?, + relatedResourceWrapper: RelatedResourceWrapper, + relatedResourcesQueue: ArrayDeque, List?>>, + ) { + val resourceConfigs = relatedResourcesConfigsMap?.get(key) + val id = resourceConfigs?.id ?: defaultKey + if (!id.isNullOrBlank()) { + relatedResourceWrapper.relatedResourceMap[id] = + relatedResourceWrapper.relatedResourceMap + .getOrPut(id) { mutableListOf() } + .apply { addAll(resources.distinctBy { it.logicalId }) } + resources.chunked(DEFAULT_BATCH_SIZE) { item -> + with(resourceConfigs?.relatedResources) { + if (!this.isNullOrEmpty()) { + relatedResourcesQueue.addLast(Pair(item, this)) + } + } + } + } + } + + protected suspend fun Search.count( + onSuccess: (Long) -> Unit = {}, + onFailure: (Throwable) -> Unit = { throwable -> + Timber.e( + throwable, + "Error counting data", + ) + }, + ): Long = + kotlin + .runCatching { fhirEngine.count(this@count) } + .onSuccess { count -> onSuccess(count) } + .onFailure { throwable -> onFailure(throwable) } + .getOrDefault(0) + /** * This function searches for reverse/forward included resources as per the configuration; * [RelatedResourceWrapper] data class is then used to wrap the maps used to store Search Query @@ -618,132 +681,57 @@ constructor( private suspend fun searchIncludedResources( relatedResourcesConfigs: List?, resources: List, - relatedResourceWrapper: RelatedResourceWrapper, configComputedRuleValues: Map, - ) { - val relatedResourcesConfigsMap = relatedResourcesConfigs?.groupBy { it.resource } - - if (!relatedResourcesConfigsMap.isNullOrEmpty()) { - if (resources.isEmpty()) return - - val firstResourceType = resources.first().resourceType - val search = - Search(firstResourceType).apply { - val filters = - resources.map { - val apply: TokenParamFilterCriterion.() -> Unit = { value = of(it.logicalId) } - apply - } - filter(Resource.RES_ID, *filters.toTypedArray()) - } - - // Forward include related resources e.g. Members (Patient) referenced in Group resource - val forwardIncludeResourceConfigs = - relatedResourcesConfigs.revIncludeRelatedResourceConfigs(false) - - // Reverse include related resources e.g. All CarePlans, Immunization for Patient resource - val reverseIncludeResourceConfigs = - relatedResourcesConfigs.revIncludeRelatedResourceConfigs(true) - - search.apply { - reverseIncludeResourceConfigs.forEach { resourceConfig -> - revInclude( - resourceConfig.resource, - ReferenceClientParam(resourceConfig.searchParameter), - ) { - (this as Search).applyConfiguredSortAndFilters( - resourceConfig = resourceConfig, - sortData = true, - configComputedRuleValues = configComputedRuleValues, - ) - } - } - - forwardIncludeResourceConfigs.forEach { resourceConfig -> - include( - resourceConfig.resource, - ReferenceClientParam(resourceConfig.searchParameter), - ) { - (this as Search).applyConfiguredSortAndFilters( - resourceConfig = resourceConfig, - sortData = true, - configComputedRuleValues = configComputedRuleValues, - ) + ): List> { + val search = + Search(resources.first().resourceType).apply { + val filters = + resources.map { + val apply: TokenParamFilterCriterion.() -> Unit = { value = of(it.logicalId) } + apply } - } + filter(Resource.RES_ID, *filters.toTypedArray()) } - searchRelatedResources( - search = search, - relatedResourcesConfigsMap = relatedResourcesConfigsMap, - relatedResourceWrapper = relatedResourceWrapper, - configComputedRuleValues = configComputedRuleValues, - ) - } - } + // Forward include related resources e.g. a member or managingEntity of a Group resource + val forwardIncludeResourceConfigs = + relatedResourcesConfigs?.revIncludeRelatedResourceConfigs(false) - private suspend fun searchRelatedResources( - search: Search, - relatedResourcesConfigsMap: Map>, - relatedResourceWrapper: RelatedResourceWrapper, - configComputedRuleValues: Map, - ) { - kotlin - .runCatching { fhirEngine.search(search) } - .onSuccess { searchResult -> - searchResult.forEach { currentSearchResult -> - val includedResources: Map>? = - currentSearchResult.included - ?.values - ?.flatten() - ?.distinctBy { it.id } - ?.groupBy { it.resourceType } - val reverseIncludedResources: Map>? = - currentSearchResult.revIncluded - ?.values - ?.flatten() - ?.distinctBy { it.id } - ?.groupBy { it.resourceType } - val theRelatedResourcesMap = - mutableMapOf>().apply { - includedResources?.let { putAll(it) } - reverseIncludedResources?.let { putAll(it) } - } - theRelatedResourcesMap.forEach { entry -> - val currentResourceConfigs = relatedResourcesConfigsMap[entry.key] - - val key = // Use configured id as key otherwise default to ResourceType - if (relatedResourcesConfigsMap.containsKey(entry.key)) { - currentResourceConfigs?.firstOrNull()?.id ?: entry.key.name - } else { - entry.key.name - } + // Reverse include related resources e.g. all CarePlans, Immunizations for Patient resource + val reverseIncludeResourceConfigs = + relatedResourcesConfigs?.revIncludeRelatedResourceConfigs(true) - // All nested resources flattened to one map by adding to existing list - relatedResourceWrapper.relatedResourceMap[key] = - relatedResourceWrapper.relatedResourceMap - .getOrPut(key) { LinkedList() } - .plus(entry.value) - - currentResourceConfigs?.forEach { resourceConfig -> - if (resourceConfig.relatedResources.isNotEmpty()) { - retrieveRelatedResources( - resources = entry.value, - relatedResourcesConfigs = resourceConfig.relatedResources, - relatedResourceWrapper = relatedResourceWrapper, - configComputedRuleValues = configComputedRuleValues, - ) - } - } - } + search.apply { + reverseIncludeResourceConfigs?.forEach { resourceConfig -> + revInclude( + resourceConfig.resource, + ReferenceClientParam(resourceConfig.searchParameter), + ) { + (this as Search).applyConfiguredSortAndFilters( + resourceConfig = resourceConfig, + sortData = true, + configComputedRuleValues = configComputedRuleValues, + ) } } - .onFailure { - Timber.e( - it, - "Error fetching configured related resources: $relatedResourcesConfigsMap", - ) + + forwardIncludeResourceConfigs?.forEach { resourceConfig -> + include( + resourceConfig.resource, + ReferenceClientParam(resourceConfig.searchParameter), + ) { + (this as Search).applyConfiguredSortAndFilters( + resourceConfig = resourceConfig, + sortData = true, + configComputedRuleValues = configComputedRuleValues, + ) + } } + } + return kotlin + .runCatching { fhirEngine.batchedSearch(search) } + .onFailure { Timber.e(it, "Error fetching related resources") } + .getOrDefault(emptyList()) } private fun List.revIncludeRelatedResourceConfigs(isRevInclude: Boolean) = @@ -779,7 +767,6 @@ constructor( } } - Timber.i("Computed values map = ${computedValuesMap.values}") val search = Search(resourceConfig.resource).apply { applyConfiguredSortAndFilters( @@ -789,7 +776,7 @@ constructor( configComputedRuleValues = computedValuesMap, ) } - val resources = fhirEngine.search(search).map { it.resource } + val resources = fhirEngine.batchedSearch(search).map { it.resource } val filteredResources = filterResourcesByFhirPathExpression( resourceFilterExpressions = eventWorkflow.resourceFilterExpressions, @@ -800,27 +787,27 @@ constructor( closeResource(resource = it, eventWorkflow = eventWorkflow) } - val retrievedRelatedResources = - retrieveRelatedResources( - resources = resources, - relatedResourcesConfigs = resourceConfig.relatedResources, - relatedResourceWrapper = RelatedResourceWrapper(), - configComputedRuleValues = computedValuesMap, - ) - - retrievedRelatedResources.relatedResourceMap.forEach { resourcesMap -> - val filteredRelatedResources = - filterResourcesByFhirPathExpression( - resourceFilterExpressions = eventWorkflow.resourceFilterExpressions, - resources = resourcesMap.value, + resources.forEach { resource -> + val retrievedRelatedResources = + retrieveRelatedResources( + resource = resource, + relatedResourcesConfigs = resourceConfig.relatedResources, + configComputedRuleValues = computedValuesMap, ) + retrievedRelatedResources.relatedResourceMap.forEach { resourcesMap -> + val filteredRelatedResources = + filterResourcesByFhirPathExpression( + resourceFilterExpressions = eventWorkflow.resourceFilterExpressions, + resources = resourcesMap.value, + ) - filteredRelatedResources.forEach { resource -> - Timber.i( - "Closing related Resource type ${resource.resourceType.name} and id ${resource.id}", - ) - if (filterRelatedResource(resource, resourceConfig)) { - closeResource(resource = resource, eventWorkflow = eventWorkflow) + filteredRelatedResources.forEach { resource -> + Timber.i( + "Closing related Resource type ${resource.resourceType.name} and id ${resource.id}", + ) + if (filterRelatedResource(resource, resourceConfig)) { + closeResource(resource = resource, eventWorkflow = eventWorkflow) + } } } } @@ -959,6 +946,81 @@ constructor( } } + suspend fun countResources( + filterByRelatedEntityLocation: Boolean, + baseResourceConfig: ResourceConfig, + filterActiveResources: List, + configComputedRuleValues: Map, + ) = + if (filterByRelatedEntityLocation) { + val syncLocationIds = context.retrieveRelatedEntitySyncLocationIds() + val locationIds = + syncLocationIds + .map { retrieveFlattenedSubLocations(it).map { subLocation -> subLocation.logicalId } } + .asSequence() + .flatten() + .toHashSet() + val countSearch = + Search(baseResourceConfig.resource).apply { + applyConfiguredSortAndFilters( + resourceConfig = baseResourceConfig, + sortData = false, + filterActiveResources = filterActiveResources, + configComputedRuleValues = configComputedRuleValues, + ) + } + val totalCount = fhirEngine.count(countSearch) + var searchResultsCount = 0L + var pageNumber = 0 + var count = 0 + while (count < totalCount) { + val baseResourceSearch = + createSearch( + baseResourceConfig = baseResourceConfig, + filterActiveResources = filterActiveResources, + configComputedRuleValues = configComputedRuleValues, + currentPage = pageNumber, + count = DEFAULT_BATCH_SIZE, + ) + searchResultsCount += + fhirEngine + .search(baseResourceSearch) + .asSequence() + .map { it.resource } + .filter { resource -> + when (resource.resourceType) { + ResourceType.Location -> locationIds.contains(resource.logicalId) + else -> + resource.meta.tag.any { + it.system == + context.getString(R.string.sync_strategy_related_entity_location_system) && + locationIds.contains(it.code) + } + } + } + .count() + .toLong() + count += DEFAULT_BATCH_SIZE + pageNumber++ + } + searchResultsCount + } else { + val search = + Search(baseResourceConfig.resource).apply { + applyConfiguredSortAndFilters( + resourceConfig = baseResourceConfig, + sortData = false, + filterActiveResources = filterActiveResources, + configComputedRuleValues = configComputedRuleValues, + ) + } + search.count( + onFailure = { + Timber.e(it, "Error counting resources ${baseResourceConfig.resource.name}") + }, + ) + } + suspend fun searchResourcesRecursively( filterByRelatedEntityLocationMetaTag: Boolean, filterActiveResources: List?, @@ -972,61 +1034,158 @@ constructor( val baseResourceConfig = fhirResourceConfig.baseResource val relatedResourcesConfig = fhirResourceConfig.relatedResources val configComputedRuleValues = configRules.configRulesComputedValues() - val search = - Search(type = baseResourceConfig.resource).apply { - applyConfiguredSortAndFilters( - resourceConfig = baseResourceConfig, - filterActiveResources = filterActiveResources, - sortData = true, - configComputedRuleValues = configComputedRuleValues, - ) - applyFilterByRelatedEntityLocationMetaTag( - baseResourceConfig.resource, - filterByRelatedEntityLocationMetaTag, + + if (filterByRelatedEntityLocationMetaTag) { + val syncLocationIds = context.retrieveRelatedEntitySyncLocationIds() + val locationIds = + syncLocationIds + .map { retrieveFlattenedSubLocations(it).map { subLocation -> subLocation.logicalId } } + .flatten() + .toHashSet() + val countSearch = + Search(baseResourceConfig.resource).apply { + applyConfiguredSortAndFilters( + resourceConfig = baseResourceConfig, + sortData = false, + filterActiveResources = filterActiveResources, + configComputedRuleValues = configComputedRuleValues, + ) + } + val totalCount = fhirEngine.count(countSearch) + val searchResults = ArrayDeque>() + var pageNumber = 0 + var count = 0 + while (count < totalCount) { + val baseResourceSearch = + createSearch( + baseResourceConfig = baseResourceConfig, + filterActiveResources = filterActiveResources, + configComputedRuleValues = configComputedRuleValues, + currentPage = pageNumber, + count = DEFAULT_BATCH_SIZE, + ) + val result = fhirEngine.batchedSearch(baseResourceSearch) + searchResults.addAll( + result.filter { searchResult -> + when (baseResourceConfig.resource) { + ResourceType.Location -> locationIds.contains(searchResult.resource.logicalId) + else -> + searchResult.resource.meta.tag.any { + it.system == + context.getString(R.string.sync_strategy_related_entity_location_system) && + locationIds.contains(it.code) + } + } + }, ) + count += DEFAULT_BATCH_SIZE + pageNumber++ if (currentPage != null && pageSize != null) { - count = pageSize - from = currentPage * pageSize + val maxPageCount = (currentPage + 1) * pageSize + if (searchResults.size >= maxPageCount) break } } - val baseFhirResources = - kotlin - .runCatching { - val searchTime = System.currentTimeMillis() - val result = fhirEngine.search(search) - Timber.w( - "It took ${(System.currentTimeMillis() - searchTime) / 1000} second(s) to search resources of type ${baseResourceConfig.resource}", - ) - result - } - .onFailure { - Timber.e( - it, - "Error retrieving resources. Empty list returned by default", + if (currentPage != null && pageSize != null) { + val fromIndex = currentPage * pageSize + val toIndex = (currentPage + 1) * pageSize + with(searchResults.subList(fromIndex, min(toIndex, searchResults.size))) { + mapResourceToRepositoryResourceData( + relatedResourcesConfig = relatedResourcesConfig, + configComputedRuleValues = configComputedRuleValues, + secondaryResourceConfigs = secondaryResourceConfigs, + filterActiveResources = filterActiveResources, + baseResourceConfig = baseResourceConfig, ) } - .getOrDefault(emptyList()) - - baseFhirResources.map { searchResult -> - val retrievedRelatedResources = - retrieveRelatedResources( - resources = listOf(searchResult.resource), - relatedResourcesConfigs = relatedResourcesConfig, - relatedResourceWrapper = RelatedResourceWrapper(), + } else { + searchResults.mapResourceToRepositoryResourceData( + relatedResourcesConfig = relatedResourcesConfig, configComputedRuleValues = configComputedRuleValues, + secondaryResourceConfigs = secondaryResourceConfigs, + filterActiveResources = filterActiveResources, + baseResourceConfig = baseResourceConfig, ) - val secondaryRepositoryResourceData = - secondaryResourceConfigs.retrieveSecondaryRepositoryResourceData(filterActiveResources) - RepositoryResourceData( - resourceRulesEngineFactId = baseResourceConfig.id ?: baseResourceConfig.resource.name, - resource = searchResult.resource, - relatedResourcesMap = retrievedRelatedResources.relatedResourceMap, - relatedResourcesCountMap = retrievedRelatedResources.relatedResourceCountMap, - secondaryRepositoryResourceData = secondaryRepositoryResourceData, + } + } else { + val baseFhirResources: List> = + kotlin + .runCatching { + val search = + createSearch( + baseResourceConfig = baseResourceConfig, + filterActiveResources = filterActiveResources, + configComputedRuleValues = configComputedRuleValues, + currentPage = currentPage, + count = pageSize, + ) + fhirEngine.batchedSearch(search) + } + .onFailure { + Timber.e( + t = it, + message = "Error retrieving resources. Empty list returned by default", + ) + } + .getOrDefault(emptyList()) + baseFhirResources.mapResourceToRepositoryResourceData( + relatedResourcesConfig = relatedResourcesConfig, + configComputedRuleValues = configComputedRuleValues, + secondaryResourceConfigs = secondaryResourceConfigs, + filterActiveResources = filterActiveResources, + baseResourceConfig = baseResourceConfig, ) } + as List + } + } + + private suspend fun List>.mapResourceToRepositoryResourceData( + relatedResourcesConfig: List, + configComputedRuleValues: Map, + secondaryResourceConfigs: List?, + filterActiveResources: List?, + baseResourceConfig: ResourceConfig, + ) = + this.pmap { searchResult -> + val retrievedRelatedResources = + retrieveRelatedResources( + resource = searchResult.resource, + relatedResourcesConfigs = relatedResourcesConfig, + configComputedRuleValues = configComputedRuleValues, + ) + val secondaryRepositoryResourceData = + secondaryResourceConfigs.retrieveSecondaryRepositoryResourceData(filterActiveResources) + RepositoryResourceData( + resourceRulesEngineFactId = baseResourceConfig.id ?: baseResourceConfig.resource.name, + resource = searchResult.resource, + relatedResourcesMap = retrievedRelatedResources.relatedResourceMap, + relatedResourcesCountMap = retrievedRelatedResources.relatedResourceCountMap, + secondaryRepositoryResourceData = secondaryRepositoryResourceData, + ) } + + protected fun createSearch( + baseResourceConfig: ResourceConfig, + filterActiveResources: List?, + configComputedRuleValues: Map, + currentPage: Int?, + count: Int?, + ): Search { + val search = + Search(type = baseResourceConfig.resource).apply { + applyConfiguredSortAndFilters( + resourceConfig = baseResourceConfig, + filterActiveResources = filterActiveResources, + sortData = true, + configComputedRuleValues = configComputedRuleValues, + ) + if (currentPage != null && count != null) { + this.count = count + from = currentPage * count + } + } + return search } protected fun List?.configRulesComputedValues(): Map { @@ -1038,10 +1197,10 @@ constructor( /** This function fetches other resources that are not linked to the base/primary resource. */ protected suspend fun List?.retrieveSecondaryRepositoryResourceData( filterActiveResources: List?, - ): LinkedList { - val secondaryRepositoryResourceDataLinkedList = LinkedList() + ): List { + val secondaryRepositoryResourceDataList = mutableListOf() this?.forEach { - secondaryRepositoryResourceDataLinkedList.addAll( + secondaryRepositoryResourceDataList.addAll( searchResourcesRecursively( fhirResourceConfig = it, filterActiveResources = filterActiveResources, @@ -1051,55 +1210,7 @@ constructor( ), ) } - return secondaryRepositoryResourceDataLinkedList - } - - suspend fun Search.applyFilterByRelatedEntityLocationMetaTag( - baseResourceType: ResourceType, - filterByRelatedEntityLocation: Boolean, - ) { - runBlocking { - if (filterByRelatedEntityLocation) { - val system = context.getString(R.string.sync_strategy_related_entity_location_system) - val display = context.getString(R.string.sync_strategy_related_entity_location_display) - val syncLocationIds = context.retrieveRelatedEntitySyncLocationIds() - // TODO Do we want to configure when to include subLocations and parent ids? - // TODO This will require a new config model for related entity location filter - val locationIds = - syncLocationIds - .map { retrieveFlattenedSubLocations(it).map { subLocation -> subLocation.logicalId } } - .flatten() - .plus(syncLocationIds) - - val filters = - if ( - baseResourceType == ResourceType.Location && locationIds.isNotEmpty() - ) { // E.g where _id=uuid1,uuid2 - locationIds.map { - val apply: TokenParamFilterCriterion.() -> Unit = { value = of(it) } - apply - } - } else { - locationIds.map { code -> // The RelatedEntityLocation is retrieved from meta tag - val apply: TokenParamFilterCriterion.() -> Unit = { - value = of(Coding(system, code, display)) - } - apply - } - } - - if (filters.isNotEmpty()) { - this@applyFilterByRelatedEntityLocationMetaTag.filter( - if (baseResourceType == ResourceType.Location) { - Location.RES_ID - } else { - TokenClientParam(TAG) - }, - *filters.toTypedArray(), - ) - } - } - } + return secondaryRepositoryResourceDataList } suspend fun retrieveUniqueIdAssignmentResource( @@ -1146,42 +1257,42 @@ constructor( return null } - suspend fun retrieveFlattenedSubLocations(locationId: String): LinkedList { - val locations = LinkedList() - val resources: LinkedList = retrieveSubLocations(locationId) - + suspend fun retrieveFlattenedSubLocations(locationId: String): ArrayDeque { + val locations = ArrayDeque() + val resources: ArrayDeque = retrieveSubLocations(locationId) while (resources.isNotEmpty()) { val currentResource = resources.removeFirst() locations.add(currentResource) retrieveSubLocations(currentResource.logicalId).forEach(resources::addLast) } + loadResource(locationId)?.let { parentLocation -> locations.addFirst(parentLocation) } return locations } - private suspend fun retrieveSubLocations(locationId: String) = - withContext(dispatcherProvider.io()) { - fhirEngine - .search( - Search(type = ResourceType.Location).apply { - filter( - Location.PARTOF, - { value = locationId.asReference(ResourceType.Location).reference }, - ) - }, - ) - .mapTo(LinkedList()) { it.resource } - } + private suspend fun retrieveSubLocations(locationId: String): ArrayDeque = + fhirEngine + .batchedSearch( + Search(type = ResourceType.Location).apply { + filter( + Location.PARTOF, + { value = locationId.asReference(ResourceType.Location).reference }, + ) + }, + ) + .mapTo(ArrayDeque()) { it.resource } /** * A wrapper data class to hold search results. All related resources are flattened into one Map * including the nested related resources as required by the Rules Engine facts. */ data class RelatedResourceWrapper( - val relatedResourceMap: MutableMap> = mutableMapOf(), - val relatedResourceCountMap: MutableMap> = mutableMapOf(), + val relatedResourceMap: MutableMap> = mutableMapOf(), + val relatedResourceCountMap: MutableMap> = + mutableMapOf(), ) companion object { + const val DEFAULT_BATCH_SIZE = 250 const val SNOMED_SYSTEM = "http://hl7.org/fhir/R4B/valueset-condition-clinical.html" const val PATIENT_CONDITION_RESOLVED_CODE = "resolved" const val PATIENT_CONDITION_RESOLVED_DISPLAY = "Resolved" diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/register/RegisterRepository.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/register/RegisterRepository.kt index d8ae465d39..35baead2f3 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/register/RegisterRepository.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/local/register/RegisterRepository.kt @@ -19,7 +19,6 @@ package org.smartregister.fhircore.engine.data.local.register import android.content.Context import ca.uhn.fhir.parser.IParser import com.google.android.fhir.FhirEngine -import com.google.android.fhir.search.Search import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import kotlinx.coroutines.withContext @@ -40,7 +39,6 @@ import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor -import timber.log.Timber class RegisterRepository @Inject @@ -93,29 +91,20 @@ constructor( fhirResourceConfig: FhirResourceConfig?, paramsMap: Map?, ): Long { - val registerConfiguration = retrieveRegisterConfiguration(registerId, paramsMap) - val fhirResource = fhirResourceConfig ?: registerConfiguration.fhirResource - val baseResourceConfig = fhirResource.baseResource - val configComputedRuleValues = registerConfiguration.configRules.configRulesComputedValues() - val filterByRelatedEntityLocation = registerConfiguration.filterDataByRelatedEntityLocation - val search = - Search(baseResourceConfig.resource).apply { - applyConfiguredSortAndFilters( - resourceConfig = baseResourceConfig, - sortData = false, - filterActiveResources = registerConfiguration.activeResourceFilters, - configComputedRuleValues = configComputedRuleValues, - ) - applyFilterByRelatedEntityLocationMetaTag( - baseResourceType = baseResourceConfig.resource, - filterByRelatedEntityLocation = filterByRelatedEntityLocation, - ) - } - return search.count( - onFailure = { - Timber.e(it, "Error counting register data for register id: ${registerConfiguration.id}") - }, - ) + return withContext(dispatcherProvider.io()) { + val registerConfiguration = retrieveRegisterConfiguration(registerId, paramsMap) + val fhirResource = fhirResourceConfig ?: registerConfiguration.fhirResource + val baseResourceConfig = fhirResource.baseResource + val configComputedRuleValues = registerConfiguration.configRules.configRulesComputedValues() + val filterByRelatedEntityLocation = registerConfiguration.filterDataByRelatedEntityLocation + val filterActiveResources = registerConfiguration.activeResourceFilters + countResources( + filterByRelatedEntityLocation = filterByRelatedEntityLocation, + baseResourceConfig = baseResourceConfig, + filterActiveResources = filterActiveResources, + configComputedRuleValues = configComputedRuleValues, + ) + } } override suspend fun loadProfileData( @@ -145,9 +134,8 @@ constructor( val retrievedRelatedResources = retrieveRelatedResources( - resources = listOf(baseResource), + resource = baseResource, relatedResourcesConfigs = resourceConfig.relatedResources, - relatedResourceWrapper = RelatedResourceWrapper(), configComputedRuleValues = configComputedRuleValues, ) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt index 0f82f47883..efa101b299 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt @@ -189,7 +189,9 @@ constructor( accountManager.peekAuthToken(account, AUTH_TOKEN_TYPE), ) Result.success(true) - } else Result.success(false) + } else { + Result.success(false) + } } catch (httpException: HttpException) { Result.failure(httpException) } catch (unknownHostException: UnknownHostException) { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt index e2c346fc93..1ce80b7bf7 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/di/NetworkModule.kt @@ -66,7 +66,9 @@ class NetworkModule { level = if (BuildConfig.DEBUG) { HttpLoggingInterceptor.Level.BODY - } else HttpLoggingInterceptor.Level.BASIC + } else { + HttpLoggingInterceptor.Level.BASIC + } redactHeader(AUTHORIZATION) redactHeader(COOKIE) }, @@ -141,7 +143,9 @@ class NetworkModule { level = if (BuildConfig.DEBUG) { HttpLoggingInterceptor.Level.BODY - } else HttpLoggingInterceptor.Level.BASIC + } else { + HttpLoggingInterceptor.Level.BASIC + } redactHeader(AUTHORIZATION) redactHeader(COOKIE) }, diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt index 2893539540..474f2c0da8 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt @@ -36,6 +36,7 @@ import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration import org.smartregister.fhircore.engine.util.DispatcherProvider +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.isValidResourceType import org.smartregister.fhircore.engine.util.extension.resourceClassType import org.smartregister.p2p.model.RecordCount @@ -108,7 +109,7 @@ constructor( count = batchSize from = offset } - fhirEngine.search(search) + fhirEngine.batchedSearch(search) } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt index 28aaf581ed..bcdb51f226 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt @@ -140,7 +140,9 @@ class HtmlPopulator( questionnaireResponseItemMap.getOrDefault(linkId, listOf()).joinToString { answer -> if (dateFormat == null) { answer.value.valueToString() - } else answer.value.valueToString(dateFormat) + } else { + answer.value.valueToString(dateFormat) + } } html.replace(i, matcher.end() + i, answer) } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ConfigRulesExecutor.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ConfigRulesExecutor.kt index 7c1821d6b4..c03ff0a495 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ConfigRulesExecutor.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ConfigRulesExecutor.kt @@ -52,7 +52,9 @@ class ConfigRulesExecutor @Inject constructor(val fhirPathDataExtractor: FhirPat if (BuildConfig.DEBUG) { val timeToFireRules = measureTimeMillis { rulesEngine.fire(rules, facts) } Timber.d("Rule executed in $timeToFireRules millisecond(s)") - } else rulesEngine.fire(rules, facts) + } else { + rulesEngine.fire(rules, facts) + } return facts.get(DATA) as Map } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ResourceDataRulesExecutor.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ResourceDataRulesExecutor.kt index fce7444a95..6124b286ad 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ResourceDataRulesExecutor.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/ResourceDataRulesExecutor.kt @@ -186,7 +186,9 @@ class ResourceDataRulesExecutor @Inject constructor(val rulesFactory: RulesFacto resources = newListRelatedResources, conditionalFhirPathExpression = listResource.conditionalFhirPathExpression, ) - } else newListRelatedResources ?: listOf() + } else { + newListRelatedResources ?: listOf() + } val sortConfig = listResource.sortConfig diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt index 9e563579fe..77a8e41935 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt @@ -140,7 +140,9 @@ constructor( if (BuildConfig.DEBUG) { val timeToFireRules = measureTimeMillis { rulesEngine.fire(rules, facts) } Timber.d("Rule executed in $timeToFireRules millisecond(s)") - } else rulesEngine.fire(rules, facts) + } else { + rulesEngine.fire(rules, facts) + } return facts.get(DATA) as Map } @@ -192,13 +194,14 @@ constructor( return if (referenceFhirPathExpression.isNullOrEmpty()) { value - } else + } else { value.filter { resource.logicalId == fhirPathDataExtractor .extractValue(it, referenceFhirPathExpression) .extractLogicalIdUuid() } + } } /** @@ -686,7 +689,9 @@ constructor( } if (createLocalChangeEntitiesAfterPurge) { defaultRepository.addOrUpdate(resource = updatedResource as Resource) - } else defaultRepository.createRemote(resource = arrayOf(updatedResource as Resource)) + } else { + defaultRepository.createRemote(resource = arrayOf(updatedResource as Resource)) + } } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt index 6fee7dfe88..55c2df7da2 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt @@ -23,7 +23,6 @@ import ca.uhn.fhir.util.TerserUtil import com.google.android.fhir.FhirEngine import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.get -import com.google.android.fhir.search.search import dagger.hilt.android.qualifiers.ApplicationContext import java.util.Date import javax.inject.Inject @@ -60,6 +59,7 @@ import org.smartregister.fhircore.engine.configuration.event.EventType import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.util.extension.addResourceParameter import org.smartregister.fhircore.engine.util.extension.asReference +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.encodeResourceToString import org.smartregister.fhircore.engine.util.extension.extractFhirpathDuration import org.smartregister.fhircore.engine.util.extension.extractFhirpathPeriod @@ -120,7 +120,7 @@ constructor( // Only one CarePlan per plan, update or init a new one if not exists val output = fhirEngine - .search { + .batchedSearch { filter( CarePlan.INSTANTIATES_CANONICAL, { value = planDefinition.referenceValue() }, @@ -396,7 +396,9 @@ constructor( end = if (durationExpression.isNotBlank() && offsetDate.hasValue()) { evaluateToDate(offsetDate, "\$this + $durationExpression")?.value - } else carePlan.period.end + } else { + carePlan.period.end + } } .also { taskPeriods.add(it) } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCompleteCarePlanWorker.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCompleteCarePlanWorker.kt index 0ff00e5ced..050173774b 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCompleteCarePlanWorker.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCompleteCarePlanWorker.kt @@ -20,7 +20,6 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import com.google.android.fhir.search.search import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.withContext @@ -33,6 +32,7 @@ import org.smartregister.fhircore.engine.configuration.app.ApplicationConfigurat import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SharedPreferencesHelper +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.extractId import org.smartregister.fhircore.engine.util.extension.lastOffset import org.smartregister.fhircore.engine.util.getLastOffset @@ -94,7 +94,7 @@ constructor( suspend fun getCarePlans(batchSize: Int, lastOffset: Int) = defaultRepository.fhirEngine - .search { + .batchedSearch { filter( CarePlan.STATUS, { value = of(CarePlan.CarePlanStatus.DRAFT.toCode()) }, diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirResourceUtil.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirResourceUtil.kt index 9fbcf49923..fafba12498 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirResourceUtil.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirResourceUtil.kt @@ -21,7 +21,6 @@ import ca.uhn.fhir.rest.param.ParamPrefixEnum import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.get import com.google.android.fhir.search.filter.TokenParamFilterCriterion -import com.google.android.fhir.search.search import dagger.hilt.android.qualifiers.ApplicationContext import java.util.Date import javax.inject.Inject @@ -39,6 +38,7 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration import org.smartregister.fhircore.engine.configuration.event.EventType import org.smartregister.fhircore.engine.data.local.DefaultRepository +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.executionStartIsBeforeOrToday import org.smartregister.fhircore.engine.util.extension.expiredConcept import org.smartregister.fhircore.engine.util.extension.extractId @@ -66,7 +66,7 @@ constructor( Timber.i("Fetch and expire overdue tasks") val tasksResult = fhirEngine - .search { + .batchedSearch { filter( Task.STATUS, { value = of(TaskStatus.REQUESTED.toCoding()) }, @@ -148,7 +148,7 @@ constructor( val tasks = defaultRepository.fhirEngine - .search { + .batchedSearch { filter( Task.STATUS, { value = of(TaskStatus.REQUESTED.toCoding()) }, @@ -235,7 +235,7 @@ constructor( suspend fun closeResourcesRelatedToCompletedServiceRequests() { Timber.i("Fetch completed service requests and close related resources") defaultRepository.fhirEngine - .search { + .batchedSearch { filter( ServiceRequest.STATUS, { value = of(ServiceRequest.ServiceRequestStatus.COMPLETED.toCode()) }, diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt index 677cf7b786..b329d1a556 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/base/AlertDialogue.kt @@ -81,7 +81,9 @@ object AlertDialogue { dialog.findViewById(R.id.pr_circular)?.apply { if (alertIntent == AlertIntent.PROGRESS) { this.show() - } else this.hide() + } else { + this.hide() + } } dialog.findViewById(R.id.tv_alert_message)?.apply { this.text = message } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/Pin.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/Pin.kt index 5487b9d92e..7b6eb5619c 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/Pin.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/Pin.kt @@ -114,7 +114,9 @@ fun PinInput( enteredPin = if (it.length < enteredPin.size) { enteredPin.safeRemoveLast() - } else enteredPin.safePlus(it.last()) + } else { + enteredPin.safePlus(it.last()) + } nextCellIndex = enteredPin.size onPinSet(enteredPin) onShowPinError(false) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/RegisterFooter.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/RegisterFooter.kt index e6dc80a54a..fdddeae1ea 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/RegisterFooter.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/RegisterFooter.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.smartregister.fhircore.engine.R -import org.smartregister.fhircore.engine.configuration.navigation.NavigationMenuConfig import org.smartregister.fhircore.engine.ui.theme.GreyTextColor import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundExcludeGenerated @@ -43,8 +42,6 @@ const val SEARCH_FOOTER_TAG = "searchFooterTag" const val SEARCH_FOOTER_PREVIOUS_BUTTON_TAG = "searchFooterPreviousButtonTag" const val SEARCH_FOOTER_NEXT_BUTTON_TAG = "searchFooterNextButtonTag" const val SEARCH_FOOTER_PAGINATION_TAG = "searchFooterPaginationTag" -const val PADDING_BOTTOM_WITH_FAB = 80 -const val PADDING_BOTTOM_WITHOUT_FAB = 32 @Composable fun RegisterFooter( @@ -54,21 +51,9 @@ fun RegisterFooter( previousButtonClickListener: () -> Unit, nextButtonClickListener: () -> Unit, modifier: Modifier = Modifier, - fabActions: List? = null, ) { if (resultCount > 0) { - Row( - modifier = - modifier - .fillMaxWidth() - .testTag(SEARCH_FOOTER_TAG) - .padding( - bottom = - if (!fabActions.isNullOrEmpty() && fabActions.first().visible) { - PADDING_BOTTOM_WITH_FAB.dp - } else PADDING_BOTTOM_WITHOUT_FAB.dp, - ), - ) { + Row(modifier = modifier.fillMaxWidth().testTag(SEARCH_FOOTER_TAG)) { Box( modifier = modifier.weight(1f).padding(4.dp).wrapContentWidth(Alignment.Start), ) { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/multiselect/MultiSelectView.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/multiselect/MultiSelectView.kt index 76688be132..59bb1305d8 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/multiselect/MultiSelectView.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/multiselect/MultiSelectView.kt @@ -39,7 +39,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.unit.dp -import java.util.LinkedList import org.smartregister.fhircore.engine.domain.model.SyncLocationState @Composable @@ -88,7 +87,9 @@ fun MultiSelectCheckbox( imageVector = if (collapsedState.value) { Icons.Default.ArrowDropDown - } else Icons.AutoMirrored.Filled.ArrowRight, + } else { + Icons.AutoMirrored.Filled.ArrowRight + }, contentDescription = null, tint = Color.Gray, modifier = Modifier.clickable { collapsedState.value = !collapsedState.value }, @@ -134,17 +135,17 @@ fun MultiSelectCheckbox( } // Select all the nested checkboxes - val linkedList = LinkedList(currentTreeNode.children) + val treeNodeArrayDeque = ArrayDeque(currentTreeNode.children) - while (linkedList.isNotEmpty()) { - val currentNode = linkedList.removeFirst() + while (treeNodeArrayDeque.isNotEmpty()) { + val currentNode = treeNodeArrayDeque.removeFirst() syncLocationStateMap[currentNode.id] = SyncLocationState( currentNode.id, currentNode.parent?.id, ToggleableState(checked.value), ) - currentNode.children.forEach { linkedList.add(it) } + currentNode.children.forEach { treeNodeArrayDeque.addLast(it) } } }, modifier = Modifier.padding(0.dp), diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt index 211c1a746c..7d873a8d40 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/AndroidExtensions.kt @@ -207,13 +207,6 @@ inline fun Bundle.parcelable(key: String): T? = else -> @Suppress("DEPRECATION") getParcelable(key) as? T } -@ExcludeFromJacocoGeneratedReport -inline fun Bundle.parcelableArrayList(key: String): ArrayList? = - when { - SDK_INT >= 33 -> getParcelableArrayList(key, T::class.java) - else -> @Suppress("DEPRECATION") getParcelableArrayList(key) - } - @ExcludeFromJacocoGeneratedReport inline fun Intent.serializable(key: String): T? = when { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/BitmapExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/BitmapExtension.kt index af488ee20a..579f49c901 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/BitmapExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/BitmapExtension.kt @@ -27,6 +27,6 @@ fun Bitmap.encodeToByteArray(): ByteArray { } } -fun ByteArray.decodeToBitmap(offset: Int = 0): Bitmap { - return BitmapFactory.decodeByteArray(this, offset, this.size) +fun ByteArray.decodeToBitmap(offset: Int = 0): Bitmap? { + return kotlin.runCatching { BitmapFactory.decodeByteArray(this, offset, this.size) }.getOrNull() } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtension.kt index 3688624ac4..ff4b014e10 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtension.kt @@ -18,9 +18,10 @@ package org.smartregister.fhircore.engine.util.extension import ca.uhn.fhir.util.UrlUtil import com.google.android.fhir.FhirEngine +import com.google.android.fhir.SearchResult import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.get -import com.google.android.fhir.search.search +import com.google.android.fhir.search.Search import com.google.android.fhir.workflow.FhirOperator import org.hl7.fhir.r4.model.Composition import org.hl7.fhir.r4.model.IdType @@ -40,7 +41,7 @@ suspend inline fun FhirEngine.loadResource(resourceId: St } suspend fun FhirEngine.searchCompositionByIdentifier(identifier: String): Composition? = - this.search { + this.batchedSearch { filter(Composition.IDENTIFIER, { value = of(Identifier().apply { value = identifier }) }) } .map { it.resource } @@ -50,7 +51,9 @@ suspend fun FhirEngine.loadLibraryAtPath(fhirOperator: FhirOperator, path: Strin // resource path could be Library/123 OR something like http://fhir.labs.common/Library/123 val library = runCatching { get(IdType(path).idPart) }.getOrNull() - ?: search { filter(Library.URL, { value = path }) }.map { it.resource }.firstOrNull() + ?: batchedSearch { filter(Library.URL, { value = path }) } + .map { it.resource } + .firstOrNull() } suspend fun FhirEngine.loadLibraryAtPath( @@ -72,7 +75,7 @@ suspend fun FhirEngine.loadCqlLibraryBundle(fhirOperator: FhirOperator, measureP // resource path could be Measure/123 OR something like http://fhir.labs.common/Measure/123 val measure: Measure? = if (UrlUtil.isValid(measurePath)) { - search { filter(Measure.URL, { value = measurePath }) } + batchedSearch { filter(Measure.URL, { value = measurePath }) } .map { it.resource } .firstOrNull() } else { @@ -93,3 +96,29 @@ suspend fun FhirEngine.countUnSyncedResources() = .groupingBy { it.resourceType.spaceByUppercase() } .eachCount() .map { it.key to it.value } + +suspend fun FhirEngine.batchedSearch(search: Search) = + if (search.count != null) { + this.search(search) + } else { + val result = mutableListOf>() + var offset = search.from ?: 0 + val pageCount = 100 + do { + search.from = offset + search.count = pageCount + val searchResults = this.search(search) + result += searchResults + offset += searchResults.size + } while (searchResults.size == pageCount) + + result + } + +suspend inline fun FhirEngine.batchedSearch( + init: Search.() -> Unit, +): List> { + val search = Search(type = R::class.java.newInstance().resourceType) + search.init() + return this.batchedSearch(search) +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/MeasureExtensions.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/MeasureExtensions.kt index dd733712c4..32662559aa 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/MeasureExtensions.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/MeasureExtensions.kt @@ -92,7 +92,7 @@ fun MeasureReport.StratifierGroupComponent.findPercentage( ): String { return if (denominator == 0) { "0" - } else + } else { findPopulation(MeasurePopulationType.NUMERATOR) ?.count ?.toBigDecimal() @@ -103,6 +103,7 @@ fun MeasureReport.StratifierGroupComponent.findPercentage( reportConfiguration?.roundingStrategy?.value ?: DEFAULT_ROUNDING_STRATEGY.value, ) .toString() + } } val MeasureReport.StratifierGroupComponent.displayText @@ -165,5 +166,5 @@ suspend inline fun FhirEngine.retrievePreviouslyGeneratedMeasureReports( search.filter(MeasureReport.MEASURE, { value = measureUrl }) subjects.forEach { search.filter(MeasureReport.SUBJECT, { value = it }) } - return this.search(search).map { it.resource } + return this.batchedSearch(search).map { it.resource } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/QuestionnaireExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/QuestionnaireExtension.kt index d036dfdde6..cbb925e223 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/QuestionnaireExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/QuestionnaireExtension.kt @@ -183,7 +183,9 @@ fun List.prePopulateInitialValues( (it.value is Coding) && if (actionParam.value.contains(",")) { actionParam.value.split(",").contains((it.value as Coding).code) - } else actionParam.value == (it.value as Coding).code + } else { + actionParam.value == (it.value as Coding).code + } } .forEach { it.initialSelected = true } } else { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ReferenceExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ReferenceExtension.kt index d833d7b9b0..c31a255128 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ReferenceExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ReferenceExtension.kt @@ -25,10 +25,11 @@ fun Reference.extractId(): String = fun Reference.extractType(): ResourceType? = if (this.reference.isNullOrEmpty()) { null - } else + } else { this.reference.substringBefore("/" + this.extractId()).substringAfterLast("/").let { ResourceType.fromCode(it) } + } fun String.asReference(resourceType: ResourceType): Reference { val resourceId = this diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt index 9c08e1e1a4..a5642cad2b 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt @@ -23,11 +23,9 @@ import ca.uhn.fhir.rest.gclient.ReferenceClientParam import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.get -import com.google.android.fhir.search.search import java.time.Duration import java.time.temporal.ChronoUnit import java.util.Date -import java.util.LinkedList import java.util.Locale import java.util.UUID import kotlin.math.abs @@ -64,7 +62,6 @@ import org.hl7.fhir.r4.model.StructureMap import org.hl7.fhir.r4.model.Task import org.hl7.fhir.r4.model.Timing import org.hl7.fhir.r4.model.Type -import org.hl7.fhir.r4.model.codesystems.AdministrativeGender import org.joda.time.Instant import org.json.JSONException import org.json.JSONObject @@ -438,7 +435,7 @@ fun ImplementationGuide.retrieveImplementationGuideDefinitionResources(): */ fun Composition.retrieveCompositionSections(): List { val sections = mutableListOf() - val sectionsQueue = LinkedList() + val sectionsQueue = ArrayDeque() this.section.forEach { if (!it.section.isNullOrEmpty()) { it.section.forEach { sectionComponent -> sectionsQueue.addLast(sectionComponent) } @@ -486,7 +483,7 @@ suspend fun Task.updateDependentTaskDueDate( return apply { val dependentTasks = defaultRepository.fhirEngine - .search { + .batchedSearch { filter( referenceParameter = ReferenceClientParam(PARTOF), { value = id }, diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt index d9b0ceba8e..4bef2826de 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/DefaultRepositoryTest.kt @@ -1604,15 +1604,15 @@ class DefaultRepositoryTest : RobolectricTest() { val location1SubLocations = defaultRepository.retrieveFlattenedSubLocations(location1.logicalId) - Assert.assertEquals(4, location1SubLocations.size) - Assert.assertEquals(location2.logicalId, location1SubLocations[0].logicalId) - Assert.assertEquals(location3.logicalId, location1SubLocations[1].logicalId) - Assert.assertEquals(location4.logicalId, location1SubLocations[2].logicalId) - Assert.assertEquals(location5.logicalId, location1SubLocations[3].logicalId) + Assert.assertEquals(5, location1SubLocations.size) + Assert.assertEquals(location2.logicalId, location1SubLocations[1].logicalId) + Assert.assertEquals(location3.logicalId, location1SubLocations[2].logicalId) + Assert.assertEquals(location4.logicalId, location1SubLocations[3].logicalId) + Assert.assertEquals(location5.logicalId, location1SubLocations[4].logicalId) val location4SubLocations = defaultRepository.retrieveFlattenedSubLocations(location4.logicalId) - Assert.assertEquals(1, location4SubLocations.size) - Assert.assertEquals(location5.logicalId, location4SubLocations.first().logicalId) + Assert.assertEquals(2, location4SubLocations.size) + Assert.assertEquals(location5.logicalId, location4SubLocations.last().logicalId) } } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt index 589a5494c6..d276880a9a 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt @@ -28,7 +28,6 @@ import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.get import com.google.android.fhir.knowledge.KnowledgeManager import com.google.android.fhir.search.Search -import com.google.android.fhir.search.search import com.google.android.fhir.workflow.FhirOperator import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -117,6 +116,7 @@ import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.extension.REFERENCE import org.smartregister.fhircore.engine.util.extension.SDF_YYYY_MM_DD import org.smartregister.fhircore.engine.util.extension.asReference +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.decodeResourceFromString import org.smartregister.fhircore.engine.util.extension.encodeResourceToString import org.smartregister.fhircore.engine.util.extension.extractId @@ -1784,7 +1784,7 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { } val bundle = Bundle().apply { addEntry().resource = patient } coEvery { - fhirEngine.search { + fhirEngine.batchedSearch { filter( CarePlan.INSTANTIATES_CANONICAL, { value = "${PlanDefinition().fhirType()}/plandef-1" }, @@ -1836,7 +1836,7 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { } val bundle = Bundle().apply { addEntry().resource = patient } coEvery { - fhirEngine.search { + fhirEngine.batchedSearch { filter( CarePlan.INSTANTIATES_CANONICAL, { value = "${PlanDefinition().fhirType()}/plandef-1" }, @@ -2088,7 +2088,7 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { input = listOf(Task.ParameterComponent(CodeableConcept(), StringType("9"))) } coEvery { - fhirEngine.search { + fhirEngine.batchedSearch { filter( referenceParameter = ReferenceClientParam("part-of"), { value = opv1.id.extractLogicalIdUuid() }, @@ -2104,7 +2104,7 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { ), ) coEvery { - fhirEngine.search { + fhirEngine.batchedSearch { filter( referenceParameter = ReferenceClientParam("part-of"), { value = immunizationResource.id.extractLogicalIdUuid() }, @@ -2112,7 +2112,7 @@ class FhirCarePlanGeneratorTest : RobolectricTest() { } } returns listOf(SearchResult(resource = immunizationResource, null, null)) coEvery { - fhirEngine.search { + fhirEngine.batchedSearch { filter( referenceParameter = ReferenceClientParam("part-of"), { value = encounter.id.extractLogicalIdUuid() }, diff --git a/android/geowidget/src/main/AndroidManifest.xml b/android/geowidget/src/main/AndroidManifest.xml index 738a9194e0..e721f61bbe 100644 --- a/android/geowidget/src/main/AndroidManifest.xml +++ b/android/geowidget/src/main/AndroidManifest.xml @@ -1,8 +1,10 @@ - + diff --git a/android/geowidget/src/main/assets/conversion_config.json b/android/geowidget/src/main/assets/conversion_config.json deleted file mode 100644 index 8b16ae0668..0000000000 --- a/android/geowidget/src/main/assets/conversion_config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Group": { - "name": "Group.name", - "family-id": "Group.id" - }, - "Location": { - } -} \ No newline at end of file diff --git a/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetFragment.kt b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetFragment.kt index f8b3300221..f8b49da345 100644 --- a/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetFragment.kt +++ b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetFragment.kt @@ -26,8 +26,8 @@ import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import androidx.fragment.app.activityViewModels -import com.mapbox.geojson.Feature +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider import com.mapbox.geojson.FeatureCollection import com.mapbox.geojson.MultiPoint import com.mapbox.geojson.Point @@ -41,7 +41,6 @@ import com.mapbox.mapboxsdk.style.layers.PropertyFactory import com.mapbox.mapboxsdk.style.layers.SymbolLayer import com.mapbox.mapboxsdk.style.sources.GeoJsonSource import com.mapbox.turf.TurfMeasurement -import dagger.hilt.android.AndroidEntryPoint import io.ona.kujaku.callbacks.AddPointCallback import io.ona.kujaku.plugin.switcher.BaseLayerSwitcherPlugin import io.ona.kujaku.plugin.switcher.layer.StreetsBaseLayer @@ -62,10 +61,8 @@ import org.smartregister.fhircore.geowidget.model.TYPE import org.smartregister.fhircore.geowidget.util.ResourceUtils import timber.log.Timber -@AndroidEntryPoint class GeoWidgetFragment : Fragment() { - private val geoWidgetViewModel by activityViewModels() internal var onAddLocationCallback: (GeoJsonFeature) -> Unit = {} internal var onCancelAddingLocationCallback: () -> Unit = {} internal var onClickLocationCallback: (GeoJsonFeature, FragmentManager) -> Unit = @@ -76,99 +73,92 @@ class GeoWidgetFragment : Fragment() { internal var showCurrentLocationButton: Boolean = true internal var showPlaneSwitcherButton: Boolean = true internal var showAddLocationButton: Boolean = true - private lateinit var mapView: KujakuMapView - private lateinit var featureCollection: FeatureCollection - private var geoJsonSource: GeoJsonSource? = null + private var mapView: KujakuMapView? = null + private lateinit var geoWidgetViewModel: GeoWidgetViewModel override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { - Mapbox.getInstance(requireContext(), BuildConfig.MAPBOX_SDK_TOKEN) - val view = setupViews() - mapView.onCreate(savedInstanceState) - return view + setUpMapView(savedInstanceState) + return LinearLayout(requireContext()).apply { + orientation = LinearLayout.VERTICAL + addView(mapView) + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + geoWidgetViewModel = ViewModelProvider(this)[GeoWidgetViewModel::class.java] geoWidgetViewModel.features.observe(viewLifecycleOwner) { result -> - zoomToLocationsOnMap(result.map { it.toFeature() }) + if (result.isNotEmpty()) { + geoWidgetViewModel.updateMapFeatures(result) + zoomMapWithFeatures() + } } } override fun onStart() { super.onStart() - mapView.onStart() + mapView?.onStart() } override fun onResume() { super.onResume() - mapView.onResume() + mapView?.onResume() } override fun onPause() { super.onPause() - mapView.onPause() + mapView?.onPause() } override fun onStop() { super.onStop() - mapView.onStop() + mapView?.onStop() } override fun onDestroy() { super.onDestroy() - mapView.onDestroy() + geoWidgetViewModel.clearMapFeatures() + mapView?.onDestroy() } override fun onLowMemory() { super.onLowMemory() - mapView.onLowMemory() + mapView?.onLowMemory() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - mapView.onSaveInstanceState(outState) - } - - private fun setupViews(): LinearLayout { - mapView = setUpMapView() - featureCollection = - FeatureCollection.fromFeatures( - geoWidgetViewModel.features.value?.map { it.toFeature() } ?: listOf(), - ) - return LinearLayout(requireContext()).apply { - orientation = LinearLayout.VERTICAL - addView(mapView) - } + mapView?.onSaveInstanceState(outState) } - private fun setUpMapView(): KujakuMapView { - return try { - KujakuMapView(requireActivity()).apply { - id = R.id.kujaku_widget - val builder = Style.Builder().fromUri(context.getString(R.string.style_map_fhir_core)) - getMapAsync { mapboxMap -> - mapboxMap.setStyle(builder) { style -> - geoJsonSource = style.getSourceAs(context.getString(R.string.data_set_quest)) - if (geoJsonSource != null) { - geoJsonSource!!.setGeoJson(featureCollection) + private fun setUpMapView(savedInstanceState: Bundle?) { + geoWidgetViewModel = viewModels().value + Mapbox.getInstance(requireContext(), BuildConfig.MAPBOX_SDK_TOKEN) + mapView = + try { + KujakuMapView(requireActivity()).apply { + id = R.id.kujaku_widget + val builder = Style.Builder().fromUri(context.getString(R.string.style_map_fhir_core)) + getMapAsync { mapboxMap -> + mapboxMap.setStyle(builder) { style -> + addIconsLayer(style) + addMapStyle(style) } - addIconsLayer(style) - addMapStyle(style) } - } - if (showAddLocationButton) { - setOnAddLocationListener(this) + if (showAddLocationButton) { + setOnAddLocationListener(this) + } + setOnClickLocationListener(this) } - setOnClickLocationListener(this) + } catch (mapboxConfigurationException: MapboxConfigurationException) { + Timber.e(mapboxConfigurationException) + null } - } catch (e: MapboxConfigurationException) { - Timber.e(e) - mapView - } + mapView?.onCreate(savedInstanceState) } private fun addIconsLayer(mMapboxMapStyle: Style) { @@ -270,7 +260,11 @@ class GeoWidgetFragment : Fragment() { mapLayers.forEach { when (it.layer) { MapLayer.STREET -> addBaseLayer(MapBoxSatelliteLayer(), it.active) - MapLayer.SATELLITE -> addBaseLayer(StreetsBaseLayer(requireContext()), it.active) + MapLayer.SATELLITE -> + addBaseLayer( + StreetsBaseLayer(requireContext()), + it.active, + ) MapLayer.STREET_SATELLITE -> addBaseLayer(StreetSatelliteLayer(requireContext()), it.active) } @@ -317,25 +311,29 @@ class GeoWidgetFragment : Fragment() { ) } - private fun zoomToLocationsOnMap(features: List) { - if (features.isEmpty()) return - featureCollection = FeatureCollection.fromFeatures(features) - - val locationPoints = - featureCollection - .features() - ?.asSequence() - ?.filter { it.geometry() is Point } - ?.map { it.geometry() as Point } - ?.toMutableList() ?: emptyList() - - val bbox = TurfMeasurement.bbox(MultiPoint.fromLngLats(locationPoints)) - val paddedBbox = CoordinateUtils.getPaddedBbox(bbox, 1000.0) - val bounds = LatLngBounds.from(paddedBbox[3], paddedBbox[2], paddedBbox[1], paddedBbox[0]) - val finalCameraPosition = CameraUpdateFactory.newLatLngBounds(bounds, 50) - - geoJsonSource?.setGeoJson(featureCollection) - mapView.getMapAsync { mapboxMap -> mapboxMap.easeCamera(finalCameraPosition) } + private fun zoomMapWithFeatures() { + mapView?.getMapAsync { mapboxMap -> + val featureCollection = + FeatureCollection.fromFeatures(geoWidgetViewModel.mapFeatures.toList()) + val locationPoints = + featureCollection + .features() + ?.asSequence() + ?.filter { it.geometry() is Point } + ?.map { it.geometry() as Point } + ?.toMutableList() ?: emptyList() + + val bbox = TurfMeasurement.bbox(MultiPoint.fromLngLats(locationPoints)) + val paddedBbox = CoordinateUtils.getPaddedBbox(bbox, 1000.0) + val bounds = LatLngBounds.from(paddedBbox[3], paddedBbox[2], paddedBbox[1], paddedBbox[0]) + val finalCameraPosition = CameraUpdateFactory.newLatLngBounds(bounds, 50) + + with(mapboxMap) { + (style?.getSourceAs(requireContext().getString(R.string.data_set_quest)) as GeoJsonSource?) + ?.apply { setGeoJson(featureCollection) } + easeCamera(finalCameraPosition) + } + } } class Builder { @@ -391,7 +389,17 @@ class GeoWidgetFragment : Fragment() { } } + fun submitFeatures(geoJsonFeatures: List) { + if (this::geoWidgetViewModel.isInitialized) { + geoWidgetViewModel.submitFeatures(geoJsonFeatures) + } + } + + fun clearMapFeatures() = geoWidgetViewModel.clearMapFeatures() + companion object { + const val MAP_FEATURES_LIMIT = 1000 + fun builder() = Builder() } } diff --git a/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModel.kt b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModel.kt index 8862acba5d..ac36ca170c 100644 --- a/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModel.kt +++ b/android/geowidget/src/main/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModel.kt @@ -16,20 +16,32 @@ package org.smartregister.fhircore.geowidget.screens +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import java.util.LinkedList -import javax.inject.Inject -import org.smartregister.fhircore.engine.util.DispatcherProvider +import com.mapbox.geojson.Feature import org.smartregister.fhircore.geowidget.model.GeoJsonFeature import org.smartregister.fhircore.geowidget.model.ServicePointType +import org.smartregister.fhircore.geowidget.screens.GeoWidgetFragment.Companion.MAP_FEATURES_LIMIT -@HiltViewModel -class GeoWidgetViewModel @Inject constructor(val dispatcherProvider: DispatcherProvider) : - ViewModel() { +class GeoWidgetViewModel : ViewModel() { - val features = MutableLiveData>(LinkedList()) + val mapFeatures = ArrayDeque() + private val _features = MutableLiveData>(mutableListOf()) + val features: LiveData> + get() = _features + + fun submitFeatures(geoJsonFeatures: List) { + _features.postValue(geoJsonFeatures) + } + + fun updateMapFeatures(geoJsonFeatures: List) { + if (mapFeatures.size <= MAP_FEATURES_LIMIT) { + mapFeatures.addAll(geoJsonFeatures.map { it.toFeature() }) + } + } + + fun clearMapFeatures() = mapFeatures.clear() fun getServicePointKeyToType(): Map { val map: MutableMap = HashMap() @@ -65,4 +77,9 @@ class GeoWidgetViewModel @Inject constructor(val dispatcherProvider: DispatcherP map[ServicePointType.LYCÉE.name.lowercase()] = ServicePointType.LYCÉE return map } + + override fun onCleared() { + super.onCleared() + clearMapFeatures() + } } diff --git a/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/rule/CoroutineTestRule.kt b/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/rule/CoroutineTestRule.kt index 2240510d7a..9c550cf424 100644 --- a/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/rule/CoroutineTestRule.kt +++ b/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/rule/CoroutineTestRule.kt @@ -14,42 +14,24 @@ * limitations under the License. */ -package org.smartregister.fhircore.geowidget.rule - import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain -import org.junit.rules.TestRule +import org.junit.rules.TestWatcher import org.junit.runner.Description -import org.junit.runners.model.Statement -import org.smartregister.fhircore.engine.util.DispatcherProvider - -@ExperimentalCoroutinesApi -class CoroutineTestRule(val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : - TestRule, TestCoroutineScope by TestCoroutineScope(testDispatcher) { - val testDispatcherProvider = - object : DispatcherProvider { - override fun default() = testDispatcher +@OptIn(ExperimentalCoroutinesApi::class) val testDispatcher = UnconfinedTestDispatcher() - override fun io() = testDispatcher - - override fun main() = testDispatcher +@ExperimentalCoroutinesApi +class CoroutineTestRule : TestWatcher() { - override fun unconfined() = testDispatcher - } + override fun starting(description: Description) { + Dispatchers.setMain(testDispatcher) + } - override fun apply(base: Statement?, description: Description?) = - object : Statement() { - @Throws(Throwable::class) - override fun evaluate() { - Dispatchers.setMain(testDispatcher) - base?.evaluate() - Dispatchers.resetMain() - testDispatcher.cleanupTestCoroutines() - } - } + override fun finished(description: Description) { + Dispatchers.resetMain() + } } diff --git a/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt b/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt index b84dcd5928..9713580412 100644 --- a/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt +++ b/android/geowidget/src/test/java/org/smartregister/fhircore/geowidget/screens/GeoWidgetViewModelTest.kt @@ -16,6 +16,7 @@ package org.smartregister.fhircore.geowidget.screens +import CoroutineTestRule import android.os.Build import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider @@ -48,7 +49,6 @@ import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor import org.smartregister.fhircore.geowidget.model.GeoJsonFeature import org.smartregister.fhircore.geowidget.model.Geometry import org.smartregister.fhircore.geowidget.model.ServicePointType -import org.smartregister.fhircore.geowidget.rule.CoroutineTestRule @RunWith(RobolectricTestRunner::class) @Config(sdk = [Build.VERSION_CODES.O_MR1], application = HiltTestApplication::class) @@ -63,6 +63,10 @@ class GeoWidgetViewModelTest { @Inject lateinit var configService: ConfigService + @Inject lateinit var fhirPathDataExtractor: FhirPathDataExtractor + + @Inject lateinit var parser: IParser + private lateinit var configurationRegistry: ConfigurationRegistry private lateinit var sharedPreferencesHelper: SharedPreferencesHelper @@ -75,9 +79,6 @@ class GeoWidgetViewModelTest { private val configRulesExecutor: ConfigRulesExecutor = mockk() - @Inject lateinit var fhirPathDataExtractor: FhirPathDataExtractor - - @Inject lateinit var parser: IParser private lateinit var viewModel: GeoWidgetViewModel @Mock private lateinit var dispatcherProvider: DispatcherProvider @@ -93,7 +94,7 @@ class GeoWidgetViewModelTest { spyk( DefaultRepository( fhirEngine = fhirEngine, - dispatcherProvider = coroutinesTestRule.testDispatcherProvider, + dispatcherProvider = dispatcherProvider, sharedPreferencesHelper = sharedPreferencesHelper, configurationRegistry = configurationRegistry, configService = configService, @@ -103,7 +104,7 @@ class GeoWidgetViewModelTest { context = ApplicationProvider.getApplicationContext(), ), ) - geoWidgetViewModel = spyk(GeoWidgetViewModel(coroutinesTestRule.testDispatcherProvider)) + geoWidgetViewModel = spyk(GeoWidgetViewModel(dispatcherProvider)) coEvery { defaultRepository.create(any()) } returns emptyList() } @@ -145,7 +146,7 @@ class GeoWidgetViewModelTest { serverVersion = serverVersion, ), ) - geoWidgetViewModel.features.value = geoJsonFeatures + geoWidgetViewModel.submitFeatures(geoJsonFeatures) Assert.assertEquals(geoWidgetViewModel.features.value!!.size, geoJsonFeatures.size) } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 4713965fe4..12e8cf0182 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -8,7 +8,6 @@ androidx-test = "1.6.1" appcompat = "1.7.0" benchmark-junit = "1.2.4" cardview = "1.0.0" -commonsJexl3 = "3.2.1" compose-material-icons = "1.6.8" compressor = "3.0.1" constraintlayout = "2.1.4" @@ -22,7 +21,7 @@ dagger-hilt = "2.51" datastore = "1.1.1" desugar-jdk-libs = "2.0.4" dokkaBase = "1.8.20" -easy-rules-jexl = "4.1.1-SNAPSHOT" +easyRulesCore = "4.1.1-SNAPSHOT" espresso-core = "3.6.1" fhir-common-utils = "1.0.0-SNAPSHOT" fhir-sdk-contrib-barcode = "0.1.0-beta3-preview7-SNAPSHOT" @@ -48,14 +47,14 @@ junit-jupiter = "5.10.3" junit-ktx = "1.2.1" kotlin = "1.9.22" kotlin-serialization = "1.8.10" -kotlinx-coroutines = "1.8.1" +kotlinx-coroutines = "1.9.0" kotlinx-serialization-json = "1.6.0" kt3k-coveralls-ver="2.12.0" ktlint = "0.50.0" -kujaku-library = "0.10.5-SNAPSHOT" +kujaku-library = "0.10.6-ALPHA-SNAPSHOT" leakcanary-android = "2.10" lifecycle= "2.8.3" -mapbox-sdk-turf = "4.8.0" +mapbox-sdk-turf = "7.2.0" material = "1.12.0" mlkit-barcode-scanning = "17.3.0" mockk = "1.13.8" @@ -99,7 +98,6 @@ activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } benchmark-junit = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "benchmark-junit" } cardview = { group = "androidx.cardview", name = "cardview", version.ref = "cardview" } -commons-jexl3 = { module = "org.apache.commons:commons-jexl3", version.ref = "commonsJexl3" } compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "compose-material-icons" } compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "compose-material-icons" } compressor = { group = "id.zelory", name = "compressor", version.ref = "compressor" } @@ -121,7 +119,7 @@ data-capture = { group = "org.smartregister", name = "data-capture", version.ref datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" } datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore"} dokka-base = { module = "org.jetbrains.dokka:dokka-base", version.ref = "dokkaBase" } -easy-rules-jexl = { group = "org.smartregister", name = "easy-rules-jexl", version.ref = "easy-rules-jexl" } +easy-rules-jexl = { group = "org.smartregister", name = "easy-rules-jexl", version.ref = "easyRulesCore" } espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } fhir-common-utils = { group = "org.smartregister", name = "fhir-common-utils", version.ref = "fhir-common-utils" } fhir-engine = { group = "org.smartregister", name = "engine", version.ref = "fhir-sdk-engine" } @@ -175,7 +173,6 @@ orchestrator = { group = "androidx.test", name = "orchestrator", version.ref = " p2p-lib = { group = "org.smartregister", name = "p2p-lib", version.ref = "p2p-lib" } paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "androidx-paging" } paging-runtime-ktx = { group = "androidx.paging", name = "paging-runtime-ktx", version.ref = "androidx-paging" } -play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" } play-services-tasks = { group = "com.google.android.gms", name = "play-services-tasks", version.ref = "playServicesTasks" } preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference-ktx" } prettytime = { group = "org.ocpsoft.prettytime", name = "prettytime", version.ref = "prettytime" } diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/main/components/TopScreenSectionTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/main/components/TopScreenSectionTest.kt index 24bb96b8cd..0210d52b4e 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/main/components/TopScreenSectionTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/main/components/TopScreenSectionTest.kt @@ -38,7 +38,7 @@ import org.smartregister.fhircore.quest.ui.main.components.TopScreenSection import org.smartregister.fhircore.quest.ui.shared.models.SearchQuery class TopScreenSectionTest { - private val listener: (SearchQuery) -> Unit = {} + private val listener: (SearchQuery, Boolean) -> Unit = { _, _ -> } @get:Rule val composeTestRule = createComposeRule() @@ -112,7 +112,7 @@ class TopScreenSectionTest { TopScreenSection( title = "All Clients", searchQuery = SearchQuery("search text"), - onSearchTextChanged = { clicked = true }, + onSearchTextChanged = { _, _ -> clicked = true }, navController = TestNavHostController(LocalContext.current), isSearchBarVisible = true, onClick = {}, diff --git a/android/quest/src/main/AndroidManifest.xml b/android/quest/src/main/AndroidManifest.xml index 2c2d7676f4..ea0e09ac7f 100644 --- a/android/quest/src/main/AndroidManifest.xml +++ b/android/quest/src/main/AndroidManifest.xml @@ -97,5 +97,13 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> + + + diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/data/DataMigration.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/DataMigration.kt index 8807b5d3e6..a565ea5d29 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/data/DataMigration.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/DataMigration.kt @@ -211,7 +211,9 @@ constructor( } if (migrationConfig.createLocalChangeEntitiesAfterPurge) { defaultRepository.addOrUpdate(resource = updatedResource as Resource) - } else defaultRepository.createRemote(resource = *arrayOf(updatedResource as Resource)) + } else { + defaultRepository.createRemote(resource = *arrayOf(updatedResource as Resource)) + } } } Timber.i("Data migration completed successfully for version: ${migrationConfig.version}") diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/data/geowidget/GeoWidgetPagingSource.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/geowidget/GeoWidgetPagingSource.kt new file mode 100644 index 0000000000..8e0217064b --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/geowidget/GeoWidgetPagingSource.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.quest.data.geowidget + +import android.database.SQLException +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.google.android.fhir.datacapture.extensions.logicalId +import kotlinx.serialization.json.JsonPrimitive +import org.hl7.fhir.r4.model.Location +import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration +import org.smartregister.fhircore.engine.data.local.DefaultRepository +import org.smartregister.fhircore.engine.data.local.register.RegisterRepository +import org.smartregister.fhircore.engine.rulesengine.ResourceDataRulesExecutor +import org.smartregister.fhircore.engine.util.extension.interpolate +import org.smartregister.fhircore.geowidget.model.GeoJsonFeature +import org.smartregister.fhircore.geowidget.model.Geometry +import timber.log.Timber + +/** [RegisterRepository] function for loading data to the paging source. */ +class GeoWidgetPagingSource( + private val defaultRepository: DefaultRepository, + private val resourceDataRulesExecutor: ResourceDataRulesExecutor, + private val geoWidgetConfig: GeoWidgetConfiguration, +) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val currentPage = params.key ?: 0 + val prevKey = if (currentPage > 0) currentPage - 1 else null + + val registerData = + defaultRepository.searchResourcesRecursively( + filterActiveResources = null, + fhirResourceConfig = geoWidgetConfig.resourceConfig, + configRules = null, + secondaryResourceConfigs = null, + filterByRelatedEntityLocationMetaTag = + geoWidgetConfig.filterDataByRelatedEntityLocation == true, + currentPage = currentPage, + pageSize = DEFAULT_PAGE_SIZE, + ) + + val nextKey = if (registerData.isNotEmpty()) currentPage + 1 else null + + val data = + registerData + .asSequence() + .filter { it.resource is Location } + .filter { (it.resource as Location).hasPosition() } + .filter { with((it.resource as Location).position) { hasLongitude() && hasLatitude() } } + .map { + Pair( + it.resource as Location, + resourceDataRulesExecutor.processResourceData( + repositoryResourceData = it, + ruleConfigs = geoWidgetConfig.servicePointConfig?.rules ?: emptyList(), + params = emptyMap(), + ), + ) + } + .map { (location, resourceData) -> + GeoJsonFeature( + id = location.logicalId, + geometry = + Geometry( + coordinates = // MapBox coordinates are represented as Long,Lat (NOT Lat,Long) + listOf( + location.position.longitude.toDouble(), + location.position.latitude.toDouble(), + ), + ), + properties = + geoWidgetConfig.servicePointConfig?.servicePointProperties?.mapValues { + JsonPrimitive(it.value.interpolate(resourceData.computedValuesMap)) + } ?: emptyMap(), + ) + } + .toList() + LoadResult.Page(data = data, prevKey = prevKey, nextKey = nextKey) + } catch (exception: SQLException) { + Timber.e(exception) + LoadResult.Error(exception) + } catch (exception: Exception) { + Timber.e(exception) + LoadResult.Error(exception) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + } + + companion object { + const val DEFAULT_PAGE_SIZE = 20 + } +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/data/register/RegisterPagingSource.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/register/RegisterPagingSource.kt index ade0d08f77..f489eba575 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/data/register/RegisterPagingSource.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/data/register/RegisterPagingSource.kt @@ -64,15 +64,12 @@ class RegisterPagingSource( ) val prevKey = - when { - _registerPagingSourceState.loadAll -> if (currentPage == 0) null else currentPage - 1 - else -> null - } + if (_registerPagingSourceState.loadAll && currentPage > 0) currentPage - 1 else null val nextKey = - when { - _registerPagingSourceState.loadAll -> - if (registerData.isNotEmpty()) currentPage + 1 else null - else -> null + if (_registerPagingSourceState.loadAll && registerData.isNotEmpty()) { + currentPage + 1 + } else { + null } val data = @@ -87,9 +84,13 @@ class RegisterPagingSource( } catch (exception: SQLException) { Timber.e(exception) LoadResult.Error(exception) + } catch (exception: Exception) { + Timber.e(exception) + LoadResult.Error(exception) } } + @Synchronized fun setPatientPagingSourceState(registerPagingSourceState: RegisterPagingSourceState) { this._registerPagingSourceState = registerPagingSourceState } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt index 03e1cd12fb..8ac0f9d169 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt @@ -172,13 +172,14 @@ constructor( entry.key, parentIt.map { it.focus.extractId() }, ) - } else + } else { fhirResourceDataSource.post( requestBody = generateRequestBundle(entry.key, parentIt.map { it.focus.extractId() }) .encodeResourceToString() .toRequestBody(NetworkModule.JSON_MEDIA_TYPE), ) + } resultBundle.entry.forEach { bundleEntryComponent -> if (bundleEntryComponent.resource != null) { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetEvent.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetEvent.kt new file mode 100644 index 0000000000..0038324b78 --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetEvent.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.quest.ui.geowidget + +import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration +import org.smartregister.fhircore.quest.ui.shared.models.SearchQuery + +sealed class GeoWidgetEvent { + data class SearchFeatures( + val searchQuery: SearchQuery = SearchQuery.emptyText, + val geoWidgetConfig: GeoWidgetConfiguration, + ) : GeoWidgetEvent() +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt index 948f7cafa1..18c1046818 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherFragment.kt @@ -54,16 +54,15 @@ import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration import org.smartregister.fhircore.engine.domain.model.ResourceData -import org.smartregister.fhircore.engine.domain.model.SnackBarMessageConfig import org.smartregister.fhircore.engine.sync.OnSyncListener import org.smartregister.fhircore.engine.sync.SyncListenerManager import org.smartregister.fhircore.engine.ui.base.AlertDialogue import org.smartregister.fhircore.engine.ui.base.AlertIntent import org.smartregister.fhircore.engine.ui.theme.AppTheme +import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.geowidget.model.GeoJsonFeature import org.smartregister.fhircore.geowidget.screens.GeoWidgetFragment -import org.smartregister.fhircore.geowidget.screens.GeoWidgetViewModel import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.event.AppEvent import org.smartregister.fhircore.quest.event.EventBus @@ -74,6 +73,7 @@ import org.smartregister.fhircore.quest.ui.main.AppMainUiState import org.smartregister.fhircore.quest.ui.main.AppMainViewModel import org.smartregister.fhircore.quest.ui.main.components.AppDrawer import org.smartregister.fhircore.quest.ui.shared.components.SnackBarMessage +import org.smartregister.fhircore.quest.ui.shared.models.SearchMode import org.smartregister.fhircore.quest.ui.shared.models.SearchQuery import org.smartregister.fhircore.quest.ui.shared.viewmodels.SearchViewModel import org.smartregister.fhircore.quest.util.extensions.handleClickEvent @@ -90,13 +90,14 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { @Inject lateinit var configurationRegistry: ConfigurationRegistry + @Inject lateinit var dispatcherProvider: DispatcherProvider + private lateinit var geoWidgetFragment: GeoWidgetFragment private lateinit var geoWidgetConfiguration: GeoWidgetConfiguration private val navArgs by navArgs() private val geoWidgetLauncherViewModel by viewModels() private val appMainViewModel by activityViewModels() private val searchViewModel by activityViewModels() - private val geoWidgetViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, @@ -104,7 +105,10 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { savedInstanceState: Bundle?, ): View { buildGeoWidgetFragment() - + geoWidgetLauncherViewModel.retrieveLocations( + geoWidgetConfig = geoWidgetConfiguration, + searchText = searchViewModel.searchQuery.value.query, + ) return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { @@ -163,34 +167,24 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { }, ) { innerPadding -> Box(modifier = Modifier.padding(innerPadding)) { - val fragment = remember { geoWidgetFragment } + val geoWidgetFragment = remember { geoWidgetFragment } GeoWidgetLauncherScreen( modifier = Modifier.fillMaxSize(), openDrawer = openDrawer, navController = findNavController(), toolBarHomeNavigation = navArgs.toolBarHomeNavigation, fragmentManager = childFragmentManager, - geoWidgetFragment = fragment, + geoWidgetFragment = geoWidgetFragment, geoWidgetConfiguration = geoWidgetConfiguration, searchQuery = searchViewModel.searchQuery, search = { searchText -> - coroutineScope.launch { - val geoJsonFeatures = - geoWidgetLauncherViewModel.retrieveLocations( - geoWidgetConfig = geoWidgetConfiguration, - searchText = searchText, - ) - if (geoJsonFeatures.isNotEmpty()) { - geoWidgetViewModel.features.postValue(geoJsonFeatures) - } else { - geoWidgetLauncherViewModel.emitSnackBarState( - SnackBarMessageConfig( - message = - getString(R.string.no_found_locations_matching_text, searchText), - ), - ) - } - } + geoWidgetFragment.clearMapFeatures() + geoWidgetLauncherViewModel.onEvent( + GeoWidgetEvent.SearchFeatures( + searchQuery = SearchQuery(searchText, SearchMode.KeyboardInput), + geoWidgetConfig = geoWidgetConfiguration, + ), + ) }, isFirstTimeSync = geoWidgetLauncherViewModel.isFirstTime(), appDrawerUIState = appDrawerUIState, @@ -224,14 +218,13 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { } is CurrentSyncJobStatus.Succeeded, is CurrentSyncJobStatus.Failed, -> { - lifecycleScope.launch { - val geoJsonFeatures = - geoWidgetLauncherViewModel.retrieveLocations( - geoWidgetConfig = geoWidgetConfiguration, - searchText = searchViewModel.searchQuery.value.query, - ) - geoWidgetViewModel.features.postValue(geoJsonFeatures) + if (syncJobStatus is CurrentSyncJobStatus.Succeeded) { + geoWidgetFragment.clearMapFeatures() } + geoWidgetLauncherViewModel.retrieveLocations( + geoWidgetConfig = geoWidgetConfiguration, + searchText = searchViewModel.searchQuery.value.query, + ) appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) } else -> appMainViewModel.updateAppDrawerUIState(currentSyncJobStatus = syncJobStatus) @@ -240,28 +233,27 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - showSetLocationDialog() - lifecycleScope.launch { - // Retrieve if searchText is null; filter will be triggered automatically if text is not empty - if (searchViewModel.searchQuery.value.isBlank()) { - val geoJsonFeatures = - geoWidgetLauncherViewModel.retrieveLocations( - geoWidgetConfig = geoWidgetConfiguration, - searchText = searchViewModel.searchQuery.value.query, - ) - if (geoJsonFeatures.isNotEmpty()) { - geoWidgetViewModel.features.postValue(geoJsonFeatures) - } else { - geoWidgetLauncherViewModel.showNoLocationDialog(geoWidgetConfiguration) - } + geoWidgetLauncherViewModel.noLocationFoundDialog.observe(viewLifecycleOwner) { show -> + if (show) { + AlertDialogue.showAlert( + context = requireContext(), + alertIntent = AlertIntent.INFO, + message = geoWidgetConfiguration.noResults?.message!!, + title = geoWidgetConfiguration.noResults?.title!!, + confirmButtonListener = { + geoWidgetConfiguration.noResults + ?.actionButton + ?.actions + ?.handleClickEvent(findNavController()) + }, + confirmButtonText = R.string.positive_button_location_set, + cancellable = true, + neutralButtonListener = {}, + ) } } - setOnQuestionnaireSubmissionListener { - lifecycleScope.launch { - geoWidgetViewModel.features.postValue(geoWidgetViewModel.features.value?.plus(it)) - } - } + setOnQuestionnaireSubmissionListener { geoWidgetFragment.submitFeatures(listOf(it)) } } override fun onPause() { @@ -320,6 +312,10 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { .showCurrentLocationButtonVisibility(geoWidgetConfiguration.showLocation) .setPlaneSwitcherButtonVisibility(geoWidgetConfiguration.showPlaneSwitcher) .build() + + lifecycleScope.launch { + geoWidgetLauncherViewModel.geoJsonFeatures.collect { geoWidgetFragment.submitFeatures(it) } + } } private fun setOnQuestionnaireSubmissionListener(emitFeature: (GeoJsonFeature) -> Unit) { @@ -340,28 +336,4 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { } } } - - private fun showSetLocationDialog() { - viewLifecycleOwner.lifecycleScope.launch { - geoWidgetLauncherViewModel.noLocationFoundDialog.observe(requireActivity()) { show -> - if (show) { - AlertDialogue.showAlert( - context = requireContext(), - alertIntent = AlertIntent.INFO, - message = geoWidgetConfiguration.noResults?.message!!, - title = geoWidgetConfiguration.noResults?.title!!, - confirmButtonListener = { - geoWidgetConfiguration.noResults - ?.actionButton - ?.actions - ?.handleClickEvent(findNavController()) - }, - confirmButtonText = R.string.positive_button_location_set, - cancellable = true, - neutralButtonListener = {}, - ) - } - } - } - } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt index 59189235a8..e2d016c7b9 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherScreen.kt @@ -35,6 +35,7 @@ import androidx.navigation.NavController import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration import org.smartregister.fhircore.engine.domain.model.ToolBarHomeNavigation import org.smartregister.fhircore.engine.util.extension.showToast +import org.smartregister.fhircore.geowidget.screens.GeoWidgetFragment import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.event.ToolbarClickEvent import org.smartregister.fhircore.quest.ui.main.AppMainEvent @@ -51,7 +52,7 @@ fun GeoWidgetLauncherScreen( navController: NavController, toolBarHomeNavigation: ToolBarHomeNavigation = ToolBarHomeNavigation.OPEN_DRAWER, fragmentManager: FragmentManager, - geoWidgetFragment: Fragment, + geoWidgetFragment: GeoWidgetFragment, geoWidgetConfiguration: GeoWidgetConfiguration, searchQuery: MutableState, search: (String) -> Unit, @@ -71,13 +72,16 @@ fun GeoWidgetLauncherScreen( showSearchByQrCode = geoWidgetConfiguration.topScreenSection?.searchBar?.searchByQrCode ?: false, toolBarHomeNavigation = toolBarHomeNavigation, - onSearchTextChanged = { searchedQuery: SearchQuery -> + performSearchOnValueChanged = false, + onSearchTextChanged = { searchedQuery: SearchQuery, performSearchOnValueChanged -> searchQuery.value = searchedQuery - val computedRules = geoWidgetConfiguration.topScreenSection?.searchBar?.computedRules - if (!computedRules.isNullOrEmpty()) { - search(searchQuery.value.query) - } else { - context.showToast(context.getString(R.string.no_search_coonfigs_provided)) + if (performSearchOnValueChanged) { + val computedRules = geoWidgetConfiguration.topScreenSection?.searchBar?.computedRules + if (!computedRules.isNullOrEmpty()) { + search(searchQuery.value.query) + } else { + context.showToast(context.getString(R.string.no_search_coonfigs_provided)) + } } }, isFilterIconEnabled = false, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt index 9c4d8d056f..5cbeeae8e3 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModel.kt @@ -20,10 +20,15 @@ import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.android.fhir.datacapture.extensions.logicalId import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonPrimitive import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.IdType @@ -34,10 +39,10 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration import org.smartregister.fhircore.engine.configuration.geowidget.GeoWidgetConfiguration +import org.smartregister.fhircore.engine.configuration.register.ActiveResourceFilterConfig import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.domain.model.ActionParameter import org.smartregister.fhircore.engine.domain.model.ActionParameterType -import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData import org.smartregister.fhircore.engine.domain.model.SnackBarMessageConfig import org.smartregister.fhircore.engine.rulesengine.ResourceDataRulesExecutor import org.smartregister.fhircore.engine.util.DispatcherProvider @@ -59,7 +64,6 @@ constructor( val resourceDataRulesExecutor: ResourceDataRulesExecutor, val configurationRegistry: ConfigurationRegistry, ) : ViewModel() { - private val _snackBarStateFlow = MutableSharedFlow() val snackBarStateFlow = _snackBarStateFlow.asSharedFlow() @@ -70,87 +74,109 @@ constructor( private val applicationConfiguration by lazy { configurationRegistry.retrieveConfiguration(ConfigType.Application) } - private lateinit var repositoryResourceDataList: List - - suspend fun retrieveLocations( - geoWidgetConfig: GeoWidgetConfiguration, - searchText: String? = null, - ): List { - val geoJsonFeatures = mutableListOf() - val repositoryResourceDataList = retrieveResources(geoWidgetConfig) - - repositoryResourceDataList.forEach { repositoryResourceData -> - val location = repositoryResourceData.resource as Location - val resourceData = - resourceDataRulesExecutor.processResourceData( - repositoryResourceData = repositoryResourceData, - ruleConfigs = geoWidgetConfig.servicePointConfig?.rules ?: emptyList(), - params = emptyMap(), - ) - val servicePointProperties = mutableMapOf() - geoWidgetConfig.servicePointConfig?.servicePointProperties?.forEach { (key, value) -> - servicePointProperties[key] = - JsonPrimitive(value.interpolate(resourceData.computedValuesMap)) - } - if ( - location.hasPosition() && - location.position.hasLatitude() && - location.position.hasLongitude() - ) { - val feature = - GeoJsonFeature( - id = location.idElement.idPart, - geometry = - Geometry( - coordinates = // MapBox coordinates are represented as Long,Lat (NOT Lat,Long) - listOf( - location.position.longitude.toDouble(), - location.position.latitude.toDouble(), - ), + val geoJsonFeatures: MutableStateFlow> = MutableStateFlow(emptyList()) + + fun retrieveLocations(geoWidgetConfig: GeoWidgetConfiguration, searchText: String?) { + viewModelScope.launch { + val totalCount = + withContext(dispatcherProvider.io()) { + defaultRepository.countResources( + filterByRelatedEntityLocation = + geoWidgetConfig.filterDataByRelatedEntityLocation == true, + baseResourceConfig = geoWidgetConfig.resourceConfig.baseResource, + filterActiveResources = + listOf( + ActiveResourceFilterConfig( + resourceType = ResourceType.Patient, + active = true, + ), + ActiveResourceFilterConfig( + resourceType = ResourceType.Group, + active = true, + ), ), - properties = servicePointProperties, + configComputedRuleValues = emptyMap(), ) - - val keys = geoWidgetConfig.topScreenSection?.searchBar?.computedRules - - if (!keys.isNullOrEmpty() && !searchText.isNullOrEmpty()) { - val addFeature = - keys.any { key -> - servicePointProperties[key].toString().contains(other = searchText, ignoreCase = true) + } + if (totalCount == 0L) { + showNoLocationDialog(geoWidgetConfig) + return@launch + } + var count = 0 + var pageNumber = 0 + while (count < totalCount) { + val registerData = + defaultRepository + .searchResourcesRecursively( + filterActiveResources = null, + fhirResourceConfig = geoWidgetConfig.resourceConfig, + configRules = null, + secondaryResourceConfigs = null, + filterByRelatedEntityLocationMetaTag = + geoWidgetConfig.filterDataByRelatedEntityLocation == true, + currentPage = pageNumber, + pageSize = DefaultRepository.DEFAULT_BATCH_SIZE, + ) + .asSequence() + .filter { it.resource is Location } + .filter { (it.resource as Location).hasPosition() } + .filter { with((it.resource as Location).position) { hasLongitude() && hasLatitude() } } + .map { + Pair( + it.resource as Location, + resourceDataRulesExecutor.processResourceData( + repositoryResourceData = it, + ruleConfigs = geoWidgetConfig.servicePointConfig?.rules ?: emptyList(), + params = emptyMap(), + ), + ) + } + .map { (location, resourceData) -> + GeoJsonFeature( + id = location.logicalId, + geometry = + Geometry( + coordinates = // MapBox coordinates are represented as Long,Lat (NOT Lat,Long) + listOf( + location.position.longitude.toDouble(), + location.position.latitude.toDouble(), + ), + ), + properties = + geoWidgetConfig.servicePointConfig?.servicePointProperties?.mapValues { + JsonPrimitive(it.value.interpolate(resourceData.computedValuesMap)) + } ?: emptyMap(), + ) + } + .toList() + geoJsonFeatures.value = + if (searchText.isNullOrBlank()) { + registerData + } else { + registerData.filter { geoJsonFeature: GeoJsonFeature -> + geoWidgetConfig.topScreenSection?.searchBar?.computedRules?.any { ruleName -> + // if ruleName not found in map return {-1}; check always return false hence no data + val value = geoJsonFeature.properties[ruleName]?.toString() ?: "{-1}" + value.contains(other = searchText, ignoreCase = true) + } == true } - if (addFeature) { - geoJsonFeatures.add(feature) } - } - - if (searchText.isNullOrEmpty()) { - geoJsonFeatures.add(feature) - } + pageNumber++ + count += DefaultRepository.DEFAULT_BATCH_SIZE } } - return geoJsonFeatures } - fun showNoLocationDialog(geoWidgetConfiguration: GeoWidgetConfiguration) { - geoWidgetConfiguration.noResults?.let { _noLocationFoundDialog.postValue(true) } + fun onEvent(geoWidgetEvent: GeoWidgetEvent) { + when (geoWidgetEvent) { + is GeoWidgetEvent.SearchFeatures -> + retrieveLocations(geoWidgetEvent.geoWidgetConfig, geoWidgetEvent.searchQuery.query) + } } - suspend fun retrieveResources( - geoWidgetConfig: GeoWidgetConfiguration, - ): List { - if (!this::repositoryResourceDataList.isInitialized) { - repositoryResourceDataList = - defaultRepository.searchResourcesRecursively( - filterActiveResources = null, - fhirResourceConfig = geoWidgetConfig.resourceConfig, - configRules = null, - secondaryResourceConfigs = null, - filterByRelatedEntityLocationMetaTag = - geoWidgetConfig.filterDataByRelatedEntityLocation == true, - ) - } - return repositoryResourceDataList + fun showNoLocationDialog(geoWidgetConfiguration: GeoWidgetConfiguration) { + geoWidgetConfiguration.noResults?.let { _noLocationFoundDialog.postValue(true) } } suspend fun onQuestionnaireSubmission( diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginActivity.kt index 966faf9bca..f873e96df6 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginActivity.kt @@ -90,7 +90,9 @@ open class LoginActivity : BaseMultiLanguageActivity() { downloadNowWorkflowConfigs() if (isPinEnabled && !hasActivePin) { navigateToPinLogin(launchSetup = true) - } else loginActivity.navigateToHome() + } else { + loginActivity.navigateToHome() + } } } launchDialPad.observe(loginActivity) { if (!it.isNullOrBlank()) launchDialPad(it) } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt index f8e9363b3b..81c1f8371b 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainActivity.kt @@ -199,7 +199,9 @@ open class AppMainActivity : BaseMultiLanguageActivity(), QuestionnaireHandler, ), ), ) - } else Timber.e("QuestionnaireConfig & QuestionnaireResponse are both null") + } else { + Timber.e("QuestionnaireConfig & QuestionnaireResponse are both null") + } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt index b170a5dde4..0a7fce88db 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/AppMainViewModel.kt @@ -85,8 +85,8 @@ import org.smartregister.fhircore.quest.navigation.NavigationArg import org.smartregister.fhircore.quest.ui.report.measure.worker.MeasureReportMonthPeriodWorker import org.smartregister.fhircore.quest.ui.shared.models.AppDrawerUIState import org.smartregister.fhircore.quest.ui.shared.models.QuestionnaireSubmission -import org.smartregister.fhircore.quest.util.extensions.decodeBinaryResourcesToBitmap import org.smartregister.fhircore.quest.util.extensions.handleClickEvent +import org.smartregister.fhircore.quest.util.extensions.resourceReferenceToBitMap import org.smartregister.fhircore.quest.util.extensions.schedulePeriodically @HiltViewModel @@ -134,18 +134,20 @@ constructor( } fun retrieveIconsAsBitmap() { - navigationConfiguration.clientRegisters - .asSequence() - .filter { - it.menuIconConfig != null && - it.menuIconConfig?.type == ICON_TYPE_REMOTE && - !it.menuIconConfig!!.reference.isNullOrEmpty() - } - .decodeBinaryResourcesToBitmap( - viewModelScope, - registerRepository, - configurationRegistry.decodedImageMap, - ) + viewModelScope.launch(dispatcherProvider.io()) { + navigationConfiguration.clientRegisters + .asSequence() + .filter { + it.menuIconConfig != null && + it.menuIconConfig?.type == ICON_TYPE_REMOTE && + !it.menuIconConfig?.reference.isNullOrBlank() + } + .mapNotNull { it.menuIconConfig!!.reference } + .resourceReferenceToBitMap( + fhirEngine = fhirEngine, + decodedImageMap = configurationRegistry.decodedImageMap, + ) + } } fun retrieveAppMainUiState(refreshAll: Boolean = true) { @@ -290,11 +292,15 @@ constructor( NavigationArg.SCREEN_TITLE to if (startDestinationConfig.screenTitle.isNullOrEmpty()) { topMenuConfig.display - } else startDestinationConfig.screenTitle, + } else { + startDestinationConfig.screenTitle + }, NavigationArg.REGISTER_ID to if (startDestinationConfig.id.isNullOrEmpty()) { clickAction?.id ?: topMenuConfig.id - } else startDestinationConfig.id, + } else { + startDestinationConfig.id + }, ) } LauncherType.MAP -> bundleOf(NavigationArg.GEO_WIDGET_ID to startDestinationConfig.id) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/AppDrawer.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/AppDrawer.kt index 0ae4299164..2e2d6b3362 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/AppDrawer.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/AppDrawer.kt @@ -321,7 +321,9 @@ private fun DefaultSyncStatus( Modifier.background( if (allDataSynced) { SideMenuBottomItemDarkColor - } else WarningColor.copy(alpha = TRANSPARENCY), + } else { + WarningColor.copy(alpha = TRANSPARENCY) + }, ) .padding(vertical = 16.dp), ) { @@ -332,7 +334,9 @@ private fun DefaultSyncStatus( stringResource( if (allDataSynced) { org.smartregister.fhircore.engine.R.string.manual_sync - } else org.smartregister.fhircore.engine.R.string.sync, + } else { + org.smartregister.fhircore.engine.R.string.sync + }, ), subTitle = if (allDataSynced) { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/TopScreenSection.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/TopScreenSection.kt index 84634dc261..acb6571cc7 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/TopScreenSection.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/main/components/TopScreenSection.kt @@ -29,6 +29,8 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Badge import androidx.compose.material.BadgedBox import androidx.compose.material.Icon @@ -57,6 +59,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -105,7 +109,8 @@ fun TopScreenSection( filteredRecordsCount: Long? = null, searchPlaceholder: String? = null, toolBarHomeNavigation: ToolBarHomeNavigation = ToolBarHomeNavigation.OPEN_DRAWER, - onSearchTextChanged: (SearchQuery) -> Unit = {}, + onSearchTextChanged: (SearchQuery, Boolean) -> Unit = { _, _ -> }, + performSearchOnValueChanged: Boolean = true, isFilterIconEnabled: Boolean = false, topScreenSection: TopScreenSectionConfig? = null, onClick: (ToolbarClickEvent) -> Unit = {}, @@ -115,7 +120,7 @@ fun TopScreenSection( // Trigger search automatically on launch if text is not empty LaunchedEffect(Unit) { if (!searchQuery.isBlank()) { - onSearchTextChanged(searchQuery) + onSearchTextChanged(searchQuery, true) } } @@ -187,7 +192,23 @@ fun TopScreenSection( OutlinedTextField( colors = TextFieldDefaults.outlinedTextFieldColors(textColor = Color.DarkGray), value = searchQuery.query, - onValueChange = { onSearchTextChanged(SearchQuery(it, mode = SearchMode.KeyboardInput)) }, + onValueChange = { + onSearchTextChanged( + SearchQuery(it, mode = SearchMode.KeyboardInput), + performSearchOnValueChanged, + ) + }, + keyboardOptions = + KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Search), + keyboardActions = + KeyboardActions( + onSearch = { + onSearchTextChanged( + SearchQuery(searchQuery.query, mode = SearchMode.KeyboardInput), + true, + ) + }, + ), maxLines = 1, singleLine = true, placeholder = { @@ -216,12 +237,14 @@ fun TopScreenSection( when { !searchQuery.isBlank() -> { IconButton( - onClick = { onSearchTextChanged(SearchQuery.emptyText) }, + onClick = { + onSearchTextChanged(SearchQuery.emptyText, performSearchOnValueChanged) + }, modifier = modifier.testTag(TRAILING_ICON_BUTTON_TEST_TAG), ) { Icon( imageVector = Icons.Filled.Clear, - CLEAR, + contentDescription = CLEAR, tint = Color.Gray, modifier = modifier.testTag(TRAILING_ICON_TEST_TAG), ) @@ -234,6 +257,7 @@ fun TopScreenSection( QrCodeScanUtils.scanQrCode(it) { code -> onSearchTextChanged( SearchQuery(code ?: "", mode = SearchMode.QrCodeScan), + performSearchOnValueChanged, ) } } @@ -319,7 +343,7 @@ fun TopScreenSectionWithFilterItemOverNinetyNinePreview() { title = "All Clients", searchQuery = SearchQuery("Eddy"), filteredRecordsCount = 120, - onSearchTextChanged = {}, + onSearchTextChanged = { _, _ -> }, toolBarHomeNavigation = ToolBarHomeNavigation.NAVIGATE_BACK, isFilterIconEnabled = true, onClick = {}, @@ -347,7 +371,7 @@ fun TopScreenSectionWithFilterCountNinetyNinePreview() { title = "All Clients", searchQuery = SearchQuery("Eddy"), filteredRecordsCount = 99, - onSearchTextChanged = {}, + onSearchTextChanged = { _, _ -> }, toolBarHomeNavigation = ToolBarHomeNavigation.NAVIGATE_BACK, isFilterIconEnabled = true, onClick = {}, @@ -362,7 +386,7 @@ fun TopScreenSectionNoFilterIconPreview() { TopScreenSection( title = "All Clients", searchQuery = SearchQuery("Eddy"), - onSearchTextChanged = {}, + onSearchTextChanged = { _, _ -> }, toolBarHomeNavigation = ToolBarHomeNavigation.NAVIGATE_BACK, isFilterIconEnabled = false, onClick = {}, @@ -387,7 +411,7 @@ fun TopScreenSectionWithFilterIconAndToggleIconPreview() { title = "All Clients", searchQuery = SearchQuery("Eddy"), filteredRecordsCount = 120, - onSearchTextChanged = {}, + onSearchTextChanged = { _, _ -> }, toolBarHomeNavigation = ToolBarHomeNavigation.NAVIGATE_BACK, isFilterIconEnabled = true, onClick = {}, @@ -412,7 +436,7 @@ fun TopScreenSectionWithToggleIconPreview() { title = "All Clients", searchQuery = SearchQuery("Eddy"), filteredRecordsCount = 120, - onSearchTextChanged = {}, + onSearchTextChanged = { _, _ -> }, toolBarHomeNavigation = ToolBarHomeNavigation.NAVIGATE_BACK, isFilterIconEnabled = false, onClick = {}, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectViewModel.kt index 1249f7d950..885990d23f 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/multiselect/MultiSelectViewModel.kt @@ -26,7 +26,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.android.fhir.datacapture.extensions.logicalId import dagger.hilt.android.lifecycle.HiltViewModel -import java.util.LinkedList import javax.inject.Inject import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @@ -38,7 +37,6 @@ import org.smartregister.fhircore.engine.ui.multiselect.TreeBuilder import org.smartregister.fhircore.engine.ui.multiselect.TreeNode import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor -import timber.log.Timber @HiltViewModel class MultiSelectViewModel @@ -63,7 +61,6 @@ constructor( previouslySelectedNodes.values.forEach { selectedNodes[it.locationId] = it } } - val currentTime = System.currentTimeMillis() val repositoryResourceData = defaultRepository.searchResourcesRecursively( filterByRelatedEntityLocationMetaTag = false, @@ -133,9 +130,6 @@ constructor( isLoading.postValue(false) _rootTreeNodes = TreeBuilder.buildTrees(lookupItems, rootNodeIds) rootTreeNodes.addAll(_rootTreeNodes) - Timber.w( - "Building tree of resource type ${multiSelectViewConfig.resourceConfig.baseResource.resource} took ${(System.currentTimeMillis() - currentTime) / 1000} second(s)", - ) } } @@ -166,9 +160,9 @@ constructor( rootTreeNodeMap[rootTreeNode.id] = rootTreeNode return@forEach } - val childrenList = LinkedList(rootTreeNode.children) - while (childrenList.isNotEmpty()) { - val currentNode = childrenList.removeFirst() + val treeNodeArrayDeque = ArrayDeque(rootTreeNode.children) + while (treeNodeArrayDeque.isNotEmpty()) { + val currentNode = treeNodeArrayDeque.removeFirst() if (currentNode.data.contains(other = searchTerm, ignoreCase = true)) { when { rootTreeNodeMap.containsKey(rootTreeNode.id) -> return@forEach @@ -178,7 +172,7 @@ constructor( } } } - currentNode.children.forEach { childrenList.add(it) } + currentNode.children.forEach { treeNodeArrayDeque.addLast(it) } } } rootTreeNodes.addAll(rootTreeNodeMap.values) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileScreen.kt index 1037a76430..04a946015a 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileScreen.kt @@ -163,7 +163,9 @@ fun ProfileScreen( bottom = if (!fabActions.isNullOrEmpty() && fabActions.first().visible) { PADDING_BOTTOM_WITH_FAB.dp - } else PADDING_BOTTOM_WITHOUT_FAB.dp, + } else { + PADDING_BOTTOM_WITHOUT_FAB.dp + }, ), ) { item(key = profileUiState.resourceData?.baseResourceId) { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileViewModel.kt index be2b697647..1673f814d2 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileViewModel.kt @@ -55,9 +55,9 @@ import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.ui.profile.bottomSheet.ProfileBottomSheetFragment import org.smartregister.fhircore.quest.ui.profile.model.EligibleManagingEntity -import org.smartregister.fhircore.quest.util.extensions.decodeBinaryResourcesToBitmap +import org.smartregister.fhircore.quest.util.extensions.decodeImageResourcesToBitmap import org.smartregister.fhircore.quest.util.extensions.handleClickEvent -import org.smartregister.fhircore.quest.util.extensions.loadRemoteImagesBitmaps +import org.smartregister.fhircore.quest.util.extensions.resourceReferenceToBitMap import org.smartregister.fhircore.quest.util.extensions.toParamDataMap import timber.log.Timber @@ -90,19 +90,22 @@ constructor( * then transformed into bitmap for use in an Image Composable (returns null if the referenced * resource doesn't exist) */ - fun decodeBinaryResourceIconsToBitmap(profileId: String) { + suspend fun decodeBinaryResourceIconsToBitmap(profileId: String) { val profileConfig = configurationRegistry.retrieveConfiguration( configId = profileId, configType = ConfigType.Profile, ) - profileConfig.overFlowMenuItems - .filter { it.icon != null && !it.icon!!.reference.isNullOrEmpty() } - .decodeBinaryResourcesToBitmap( - viewModelScope, - registerRepository, - configurationRegistry.decodedImageMap, - ) + withContext(dispatcherProvider.io()) { + profileConfig.overFlowMenuItems + .asSequence() + .filter { it.icon != null && !it.icon?.reference.isNullOrBlank() } + .mapNotNull { it.icon!!.reference } + .resourceReferenceToBitMap( + fhirEngine = registerRepository.fhirEngine, + decodedImageMap = configurationRegistry.decodedImageMap, + ) + } } suspend fun retrieveProfileUiState( @@ -141,19 +144,13 @@ constructor( computedValuesMap = resourceData.computedValuesMap.plus(paramsMap), listResourceDataStateMap = listResourceDataStateMap, ) - if ( - listResourceDataStateMap[listProperties.id] != null && - listResourceDataStateMap[listProperties.id]?.size!! > 0 - ) { - listResourceDataStateMap[listProperties.id]?.forEach { resourceData -> - loadRemoteImagesBitmaps( - profileConfiguration.views, - registerRepository, - resourceData.computedValuesMap, - configurationRegistry.decodedImageMap, - ) - } - } + } + + withContext(dispatcherProvider.io()) { + profileConfigs.views.decodeImageResourcesToBitmap( + fhirEngine = registerRepository.fhirEngine, + decodedImageMap = configurationRegistry.decodedImageMap, + ) } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/components/ChangeManagingEntityView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/components/ChangeManagingEntityView.kt index d7b6c97d24..229dc75593 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/components/ChangeManagingEntityView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/components/ChangeManagingEntityView.kt @@ -154,7 +154,9 @@ private fun ChangeManagingEntityBottomBar( id = if (isEnabled) { org.smartregister.fhircore.engine.R.color.colorPrimary - } else org.smartregister.fhircore.engine.R.color.white, + } else { + org.smartregister.fhircore.engine.R.color.white + }, ), ), ) { @@ -165,7 +167,9 @@ private fun ChangeManagingEntityBottomBar( id = if (isEnabled) { org.smartregister.fhircore.engine.R.color.white - } else org.smartregister.fhircore.engine.R.color.colorPrimary, + } else { + org.smartregister.fhircore.engine.R.color.colorPrimary + }, ), text = stringResource(id = org.smartregister.fhircore.engine.R.string.str_save).uppercase(), ) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt index 90c3747bf8..f01d407979 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt @@ -33,7 +33,6 @@ import com.google.android.fhir.datacapture.validation.Valid import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.search.Search import com.google.android.fhir.search.filter.TokenParamFilterCriterion -import com.google.android.fhir.search.search import com.google.android.fhir.workflow.FhirOperator import dagger.hilt.android.lifecycle.HiltViewModel import java.util.Date @@ -83,6 +82,7 @@ import org.smartregister.fhircore.engine.util.extension.appendOrganizationInfo import org.smartregister.fhircore.engine.util.extension.appendPractitionerInfo import org.smartregister.fhircore.engine.util.extension.appendRelatedEntityLocation import org.smartregister.fhircore.engine.util.extension.asReference +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.checkResourceValid import org.smartregister.fhircore.engine.util.extension.clearText import org.smartregister.fhircore.engine.util.extension.cqfLibraryUrls @@ -788,7 +788,7 @@ constructor( if (libraryFilters.isNotEmpty()) { defaultRepository.fhirEngine - .search { + .batchedSearch { filter( Resource.RES_ID, *libraryFilters.toTypedArray(), @@ -1043,7 +1043,7 @@ constructor( ): List { return when { subjectResourceType != null && subjectResourceIdentifier != null -> - LinkedList().apply { + mutableListOf().apply { loadResource(subjectResourceType, subjectResourceIdentifier)?.let { add(it) } val actionParametersExcludingSubject = actionParameters.filterNot { @@ -1053,7 +1053,7 @@ constructor( } addAll(retrievePopulationResources(actionParametersExcludingSubject)) } - else -> LinkedList(retrievePopulationResources(actionParameters)) + else -> retrievePopulationResources(actionParameters) } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt index 3d69aae807..03d1bb1acf 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterFragment.kt @@ -128,11 +128,11 @@ class RegisterFragment : Fragment(), OnSyncListener { AppTheme { val pagingItems = - registerViewModel.paginatedRegisterData + registerViewModel.registerData .collectAsState(emptyFlow()) .value .collectAsLazyPagingItems() - // Register screen provides access to the side navigation + Scaffold( drawerGesturesEnabled = scaffoldState.drawerState.isOpen, scaffoldState = scaffoldState, @@ -228,13 +228,11 @@ class RegisterFragment : Fragment(), OnSyncListener { updateRegisterFilterState(registerId, questionnaireResponse) } - pagesDataCache.clear() - retrieveRegisterUiState( registerId = registerId, screenTitle = screenTitle, params = params, - clearCache = false, + clearCache = true, ) } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt index 504066fba7..78ecbd3b93 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt @@ -117,9 +117,11 @@ fun RegisterScreen( searchPlaceholder = registerUiState.registerConfiguration?.searchBar?.display, showSearchByQrCode = registerUiState.registerConfiguration?.showSearchByQrCode ?: false, toolBarHomeNavigation = toolBarHomeNavigation, - onSearchTextChanged = { uiSearchQuery -> + onSearchTextChanged = { uiSearchQuery, performSearchOnValueChanged -> searchQuery.value = uiSearchQuery - onEvent(RegisterEvent.SearchRegister(searchQuery = uiSearchQuery)) + if (performSearchOnValueChanged) { + onEvent(RegisterEvent.SearchRegister(searchQuery = uiSearchQuery)) + } }, isFilterIconEnabled = filterActions?.isNotEmpty() ?: false, topScreenSection = registerUiState.registerConfiguration?.topScreenSection, @@ -171,7 +173,9 @@ fun RegisterScreen( id = if (appDrawerUIState.isSyncUpload == true) { R.string.syncing_up - } else R.string.syncing_down, + } else { + R.string.syncing_down + }, ), showPercentageProgress = true, ) @@ -181,10 +185,7 @@ fun RegisterScreen( Box( modifier = Modifier.fillMaxWidth().background(Color.White).weight(1f), ) { - if ( - registerUiState.totalRecordsCount > 0 && - registerUiState.registerConfiguration?.registerCard != null - ) { + if (registerUiState.registerConfiguration?.registerCard != null) { RegisterCardList( modifier = modifier.fillMaxSize().testTag(REGISTER_CARD_TEST_TAG), registerCardConfig = registerUiState.registerConfiguration.registerCard, @@ -194,7 +195,9 @@ fun RegisterScreen( onEvent = onEvent, registerUiState = registerUiState, currentPage = currentPage, - showPagination = searchQuery.value.isBlank(), + showPagination = + !registerUiState.registerConfiguration.infiniteScroll && + searchQuery.value.isBlank(), onSearchByQrSingleResultAction = { resourceData -> if ( !searchQuery.value.isBlank() && searchQuery.value.mode == SearchMode.QrCodeScan diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt index d07d28d85f..1aa74b6290 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt @@ -79,14 +79,13 @@ constructor( val snackBarStateFlow = _snackBarStateFlow.asSharedFlow() val registerUiState = mutableStateOf(RegisterUiState()) val currentPage: MutableState = mutableIntStateOf(0) - val paginatedRegisterData: MutableStateFlow>> = - MutableStateFlow(emptyFlow()) + val registerData: MutableStateFlow>> = MutableStateFlow(emptyFlow()) val pagesDataCache = mutableMapOf>>() val registerFilterState = mutableStateOf(RegisterFilterState()) private val _totalRecordsCount = mutableLongStateOf(0L) private val _filteredRecordsCount = mutableLongStateOf(-1L) private lateinit var registerConfiguration: RegisterConfiguration - private var allPatientRegisterData: Flow>? = null + private var completeRegisterData: Flow>? = null private val _percentageProgress: MutableSharedFlow = MutableSharedFlow(0) private val _isUploadSync: MutableSharedFlow = MutableSharedFlow(0) private val _currentSyncJobStatusFlow: MutableSharedFlow = @@ -107,9 +106,9 @@ constructor( ) { if (clearCache) { pagesDataCache.clear() - allPatientRegisterData = null + completeRegisterData = null } - paginatedRegisterData.value = + registerData.value = pagesDataCache.getOrPut(currentPage.value) { getPager(registerId, loadAll).flow.cachedIn(viewModelScope) } @@ -118,7 +117,7 @@ constructor( private fun getPager(registerId: String, loadAll: Boolean = false): Pager { val currentRegisterConfigs = retrieveRegisterConfiguration(registerId) val ruleConfigs = currentRegisterConfigs.registerCard.rules - val pageSize = currentRegisterConfigs.pageSize // Default 10 + val pageSize = currentRegisterConfigs.pageSize return Pager( config = PagingConfig(pageSize = pageSize, enablePlaceholders = false), @@ -155,41 +154,50 @@ constructor( return registerConfiguration } - private fun retrieveAllPatientRegisterData(registerId: String): Flow> { - // Ensure that we only initialize this flow once - if (allPatientRegisterData == null) { - allPatientRegisterData = getPager(registerId, true).flow.cachedIn(viewModelScope) + private fun retrieveCompleteRegisterData( + registerId: String, + forceRefresh: Boolean, + ): Flow> { + if (completeRegisterData == null || forceRefresh) { + completeRegisterData = getPager(registerId, true).flow.cachedIn(viewModelScope) } - return allPatientRegisterData!! + return completeRegisterData!! } - fun onEvent(event: RegisterEvent) = + fun onEvent(event: RegisterEvent) { + val registerId = registerUiState.value.registerId when (event) { // Search using name or patient logicalId or identifier. Modify to add more search params is RegisterEvent.SearchRegister -> { if (event.searchQuery.isBlank()) { - paginateRegisterData(registerUiState.value.registerId) + val regConfig = retrieveRegisterConfiguration(registerId) + if (regConfig.infiniteScroll) { + registerData.value = retrieveCompleteRegisterData(registerId, false) + } else { + paginateRegisterData(registerId) + } } else { filterRegisterData(event.searchQuery.query) } } is RegisterEvent.MoveToNextPage -> { currentPage.value = currentPage.value.plus(1) - paginateRegisterData(registerUiState.value.registerId) + paginateRegisterData(registerId) } is RegisterEvent.MoveToPreviousPage -> { currentPage.value.let { if (it > 0) currentPage.value = it.minus(1) } - paginateRegisterData(registerUiState.value.registerId) + paginateRegisterData(registerId) } RegisterEvent.ResetFilterRecordsCount -> _filteredRecordsCount.longValue = -1 } + } fun filterRegisterData(searchText: String) { val searchBar = registerUiState.value.registerConfiguration?.searchBar // computedRules (names of pre-computed rules) must be provided for search to work. if (searchBar?.computedRules != null) { - paginatedRegisterData.value = - retrieveAllPatientRegisterData(registerUiState.value.registerId).map { + registerData.value = + retrieveCompleteRegisterData(registerUiState.value.registerId, false).map { pagingData: PagingData -> pagingData.filter { resourceData: ResourceData -> searchBar.computedRules!!.any { ruleName -> @@ -448,22 +456,25 @@ constructor( val paramsMap: Map = params.toParamDataMap() viewModelScope.launch { val currentRegisterConfiguration = retrieveRegisterConfiguration(registerId, paramsMap) + if (currentRegisterConfiguration.infiniteScroll) { + registerData.value = + retrieveCompleteRegisterData(currentRegisterConfiguration.id, clearCache) + } else { + _totalRecordsCount.longValue = + registerRepository.countRegisterData(registerId = registerId, paramsMap = paramsMap) - _totalRecordsCount.longValue = - registerRepository.countRegisterData(registerId = registerId, paramsMap = paramsMap) - - // Only count filtered data when queries are updated - if (registerFilterState.value.fhirResourceConfig != null) { - _filteredRecordsCount.longValue = - registerRepository.countRegisterData( - registerId = registerId, - paramsMap = paramsMap, - fhirResourceConfig = registerFilterState.value.fhirResourceConfig, - ) + // Only count filtered data when queries are updated + if (registerFilterState.value.fhirResourceConfig != null) { + _filteredRecordsCount.longValue = + registerRepository.countRegisterData( + registerId = registerId, + paramsMap = paramsMap, + fhirResourceConfig = registerFilterState.value.fhirResourceConfig, + ) + } + paginateRegisterData(registerId = registerId, loadAll = false, clearCache = clearCache) } - paginateRegisterData(registerId, loadAll = false, clearCache = clearCache) - registerUiState.value = RegisterUiState( screenTitle = currentRegisterConfiguration.registerTitle ?: screenTitle, @@ -484,7 +495,9 @@ constructor( ceil( (if (registerFilterState.value.fhirResourceConfig != null) { _filteredRecordsCount.longValue - } else _totalRecordsCount.longValue) + } else { + _totalRecordsCount.longValue + }) .toDouble() .div(currentRegisterConfiguration.pageSize.toLong()), ) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/components/RegisterCardList.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/components/RegisterCardList.kt index 7cdfadc7bc..194038b148 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/components/RegisterCardList.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/components/RegisterCardList.kt @@ -16,6 +16,7 @@ package org.smartregister.fhircore.quest.ui.register.components +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -46,6 +47,8 @@ import org.smartregister.fhircore.quest.ui.shared.components.ViewRenderer import timber.log.Timber const val REGISTER_CARD_LIST_TEST_TAG = "RegisterCardListTestTag" +const val PADDING_BOTTOM_WITH_FAB = 80 +const val PADDING_BOTTOM_WITHOUT_FAB = 32 /** * This is the list used to render register data. The register data is wrapped in [ResourceData] @@ -113,15 +116,27 @@ fun RegisterCardList( // Register pagination item { - if (pagingItems.itemCount > 0 && showPagination) { - RegisterFooter( - resultCount = pagingItems.itemCount, - currentPage = currentPage.value.plus(1), - pagesCount = registerUiState.pagesCount, - fabActions = registerUiState.registerConfiguration?.fabActions, - previousButtonClickListener = { onEvent(RegisterEvent.MoveToPreviousPage) }, - nextButtonClickListener = { onEvent(RegisterEvent.MoveToNextPage) }, - ) + val fabActions = registerUiState.registerConfiguration?.fabActions + Box( + modifier = + Modifier.padding( + bottom = + if (!fabActions.isNullOrEmpty() && fabActions.first().visible) { + PADDING_BOTTOM_WITH_FAB.dp + } else { + PADDING_BOTTOM_WITHOUT_FAB.dp + }, + ), + ) { + if (pagingItems.itemCount > 0 && showPagination) { + RegisterFooter( + resultCount = pagingItems.itemCount, + currentPage = currentPage.value.plus(1), + pagesCount = registerUiState.pagesCount, + previousButtonClickListener = { onEvent(RegisterEvent.MoveToPreviousPage) }, + nextButtonClickListener = { onEvent(RegisterEvent.MoveToNextPage) }, + ) + } } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/worker/MeasureReportWorker.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/worker/MeasureReportWorker.kt index c6f272384c..3c0c481f40 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/worker/MeasureReportWorker.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/report/measure/worker/MeasureReportWorker.kt @@ -25,7 +25,6 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import androidx.work.workDataOf import com.google.android.fhir.FhirEngine -import com.google.android.fhir.search.search import com.google.android.fhir.workflow.FhirOperator import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -45,6 +44,7 @@ import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider import org.smartregister.fhircore.engine.util.extension.SDFHH_MM import org.smartregister.fhircore.engine.util.extension.SDF_YYYY_MM_DD +import org.smartregister.fhircore.engine.util.extension.batchedSearch import org.smartregister.fhircore.engine.util.extension.firstDayOfMonth import org.smartregister.fhircore.engine.util.extension.formatDate import org.smartregister.fhircore.engine.util.extension.lastDayOfMonth @@ -78,7 +78,7 @@ constructor( Timber.w("started MeasureReportWorker") fhirEngine - .search {} + .batchedSearch {} .map { it.resource } .forEach { monthList?.forEachIndexed { index, date -> diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeItemViewHolderFactory.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeItemViewHolderFactory.kt index d7a4bcf438..468f58c079 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeItemViewHolderFactory.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeItemViewHolderFactory.kt @@ -92,9 +92,10 @@ internal class EditTextQrCodeItemViewHolderFactory( editable.toString().let { if (it.isBlank()) { null - } else + } else { QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() .setValue(StringType(it)) + } } qrCodeAnswerChangeListener.onQrCodeChanged( diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeViewHolderFactory.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeViewHolderFactory.kt index 6e1c248eab..3bac623fb0 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeViewHolderFactory.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeViewHolderFactory.kt @@ -60,7 +60,9 @@ object EditTextQrCodeViewHolderFactory : prevAnswerEmpty && !newAnswerEmpty -> { if (canHaveMultipleAnswers) { questionnaireViewItem.addAnswer(newAnswer!!) - } else questionnaireViewItem.setAnswer(newAnswer!!) + } else { + questionnaireViewItem.setAnswer(newAnswer!!) + } } !prevAnswerEmpty && newAnswerEmpty -> { questionnaireViewItem.removeAnswer(previousAnswer!!) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ActionableButton.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ActionableButton.kt index 77a7b46631..103b2795cd 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ActionableButton.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ActionableButton.kt @@ -192,7 +192,9 @@ fun ActionableButton( } else { if (colorOpacity == 0.0f) { DefaultColor.copy(alpha = 0.9f) - } else statusColor.copy(alpha = colorOpacity) + } else { + statusColor.copy(alpha = colorOpacity) + } }, textAlign = TextAlign.Start, overflow = TextOverflow.Ellipsis, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt index 2783138e71..423323e660 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SyncStatusView.kt @@ -106,7 +106,9 @@ fun SyncBottomBar( val bottomRadius = if (!hideSyncCompleteStatus.value || currentSyncJobStatus is CurrentSyncJobStatus.Running) { 32.dp - } else 0.dp + } else { + 0.dp + } val height = when { syncNotificationBarExpanded -> @@ -147,7 +149,9 @@ fun SyncBottomBar( imageVector = if (syncNotificationBarExpanded) { Icons.Default.KeyboardArrowDown - } else Icons.Default.KeyboardArrowUp, + } else { + Icons.Default.KeyboardArrowUp + }, contentDescription = null, tint = when (currentSyncJobStatus) { @@ -237,7 +241,9 @@ fun SyncStatusView( imageVector = if (currentSyncJobStatus is CurrentSyncJobStatus.Succeeded) { Icons.Default.CheckCircle - } else Icons.Default.Error, + } else { + Icons.Default.Error + }, contentDescription = null, tint = when (currentSyncJobStatus) { @@ -308,7 +314,9 @@ fun SyncStatusView( stringResource( if (currentSyncJobStatus is CurrentSyncJobStatus.Failed) { org.smartregister.fhircore.engine.R.string.retry - } else org.smartregister.fhircore.engine.R.string.cancel, + } else { + org.smartregister.fhircore.engine.R.string.cancel + }, ), modifier = Modifier.padding(start = 16.dp).clickable { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewRenderer.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewRenderer.kt index 8855bb2021..276d67c769 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewRenderer.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewRenderer.kt @@ -58,7 +58,9 @@ fun ViewRenderer( val interpolatedProperties = if (areViewPropertiesInterpolated) { properties - } else properties.interpolate(resourceData.computedValuesMap) + } else { + properties.interpolate(resourceData.computedValuesMap) + } GenerateView( modifier = generateModifier(properties), properties = interpolatedProperties, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt index 713500d2ac..df61fa4bff 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt @@ -29,8 +29,8 @@ import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.navigation.NavController import androidx.navigation.NavOptions -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import com.google.android.fhir.FhirEngine +import kotlin.collections.set import org.hl7.fhir.r4.model.Binary import org.smartregister.fhircore.engine.configuration.navigation.ICON_TYPE_REMOTE import org.smartregister.fhircore.engine.configuration.navigation.NavigationMenuConfig @@ -39,15 +39,14 @@ import org.smartregister.fhircore.engine.configuration.view.ColumnProperties import org.smartregister.fhircore.engine.configuration.view.ImageProperties import org.smartregister.fhircore.engine.configuration.view.ListProperties import org.smartregister.fhircore.engine.configuration.view.RowProperties +import org.smartregister.fhircore.engine.configuration.view.ServiceCardProperties import org.smartregister.fhircore.engine.configuration.view.StackViewProperties import org.smartregister.fhircore.engine.configuration.view.ViewProperties import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger import org.smartregister.fhircore.engine.configuration.workflow.ApplicationWorkflow -import org.smartregister.fhircore.engine.data.local.register.RegisterRepository import org.smartregister.fhircore.engine.domain.model.ActionConfig import org.smartregister.fhircore.engine.domain.model.ActionParameter import org.smartregister.fhircore.engine.domain.model.ActionParameterType -import org.smartregister.fhircore.engine.domain.model.OverflowMenuItemConfig import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.ViewType import org.smartregister.fhircore.engine.util.extension.decodeJson @@ -56,6 +55,7 @@ import org.smartregister.fhircore.engine.util.extension.encodeJson import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.extension.interpolate import org.smartregister.fhircore.engine.util.extension.isIn +import org.smartregister.fhircore.engine.util.extension.loadResource import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.navigation.MainNavigationScreen @@ -63,7 +63,6 @@ import org.smartregister.fhircore.quest.navigation.NavigationArg import org.smartregister.fhircore.quest.ui.pdf.PdfLauncherFragment import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler import org.smartregister.p2p.utils.startP2PScreen -import timber.log.Timber const val PRACTITIONER_ID = "practitionerId" @@ -185,16 +184,23 @@ fun ActionConfig.handleClickEvent( navController.navigate(MainNavigationScreen.Insight.route) ApplicationWorkflow.DEVICE_TO_DEVICE_SYNC -> startP2PScreen(navController.context) ApplicationWorkflow.LAUNCH_MAP -> { - val mapFragmentDestination = MainNavigationScreen.GeoWidgetLauncher.route - - val isMapFragmentExists = navController.currentDestination?.id == mapFragmentDestination - if (isMapFragmentExists) { - navController.popBackStack(mapFragmentDestination, false) + val args = bundleOf(NavigationArg.GEO_WIDGET_ID to actionConfig.id) + // If value != null, we are navigating FROM a map; disallow same map navigation + val currentGeoWidgetId = + navController.currentBackStackEntry?.arguments?.getString(NavigationArg.GEO_WIDGET_ID) + val sameGeoWidgetNavigation = + args.getString(NavigationArg.GEO_WIDGET_ID) == + navController.previousBackStackEntry?.arguments?.getString(NavigationArg.GEO_WIDGET_ID) + if (!currentGeoWidgetId.isNullOrEmpty() && sameGeoWidgetNavigation) { + return } else { navController.navigate( - resId = mapFragmentDestination, - args = bundleOf(NavigationArg.GEO_WIDGET_ID to actionConfig.id), - navOptions = navOptions(mapFragmentDestination, inclusive = true, singleOnTop = true), + resId = MainNavigationScreen.GeoWidgetLauncher.route, + args = args, + navOptions = + navController.currentDestination?.id?.let { + navOptions(resId = it, inclusive = actionConfig.popNavigationBackStack == true) + }, ) } } @@ -258,92 +264,51 @@ fun Array?.toParamDataMap(): Map = ?.filter { it.paramType == ActionParameterType.PARAMDATA } ?.associate { it.key to it.value } ?: emptyMap() -fun List.decodeBinaryResourcesToBitmap( - coroutineScope: CoroutineScope, - registerRepository: RegisterRepository, - decodedImageMap: SnapshotStateMap, -) { - this.forEach { - val resourceId = it.icon!!.reference!!.extractLogicalIdUuid() - coroutineScope.launch() { - registerRepository.loadResource(resourceId)?.let { binary -> - decodedImageMap[resourceId] = binary.data.decodeToBitmap() - } - } - } -} - -fun Sequence.decodeBinaryResourcesToBitmap( - coroutineScope: CoroutineScope, - registerRepository: RegisterRepository, +suspend fun Sequence.resourceReferenceToBitMap( + fhirEngine: FhirEngine, decodedImageMap: SnapshotStateMap, ) { - this.forEach { - val resourceId = it.menuIconConfig!!.reference!!.extractLogicalIdUuid() - coroutineScope.launch() { - registerRepository.loadResource(resourceId)?.let { binary -> - decodedImageMap[resourceId] = binary.data.decodeToBitmap() - } + forEach { + val resourceId = it.extractLogicalIdUuid() + fhirEngine.loadResource(resourceId)?.let { binary -> + binary.data.decodeToBitmap()?.let { bitmap -> decodedImageMap[resourceId] = bitmap } } } } -suspend fun loadRemoteImagesBitmaps( - views: List, - registerRepository: RegisterRepository, - computedValuesMap: Map, +suspend fun List.decodeImageResourcesToBitmap( + fhirEngine: FhirEngine, decodedImageMap: MutableMap, ) { - suspend fun ViewProperties.loadIcons() { - when (this.viewType) { + val queue = ArrayDeque(this) + while (queue.isNotEmpty()) { + val viewProperty = queue.removeFirst() + when (viewProperty.viewType) { ViewType.IMAGE -> { - val imageProps = this as ImageProperties - if ( - !imageProps.imageConfig?.reference.isNullOrEmpty() && - imageProps.imageConfig?.type == ICON_TYPE_REMOTE - ) { - try { - val resourceId = - imageProps.imageConfig - ?.reference - ?.interpolate(computedValuesMap) - ?.extractLogicalIdUuid() - - if (resourceId != null) { - registerRepository.loadResource(resourceId)?.let { binary -> - decodedImageMap[resourceId] = binary.data.decodeToBitmap() - } + val imageProperties = (viewProperty as ImageProperties) + if (imageProperties.imageConfig != null) { + val imageConfig = imageProperties.imageConfig + if ( + ICON_TYPE_REMOTE.equals(imageConfig?.type, ignoreCase = true) && + !imageConfig?.reference.isNullOrBlank() + ) { + val resourceId = imageConfig!!.reference!! + fhirEngine.loadResource(resourceId)?.let { binary: Binary -> + binary.data.decodeToBitmap()?.let { bitmap -> decodedImageMap[resourceId] = bitmap } } - } catch (exception: Exception) { - Timber.e("Failed to decode image with error: ${exception.message}") - throw exception } } } - ViewType.ROW -> { - val container = this as RowProperties - container.children.forEach { it.loadIcons() } - } - ViewType.COLUMN -> { - val container = this as ColumnProperties - container.children.forEach { it.loadIcons() } - } - ViewType.CARD -> { - val card = this as CardViewProperties - card.content.forEach { it.loadIcons() } - } - ViewType.LIST -> { - val list = this as ListProperties - list.registerCard.views.forEach { it.loadIcons() } - } - ViewType.STACK -> { - val stack = this as StackViewProperties - stack.children.forEach { it.loadIcons() } - } + ViewType.COLUMN -> (viewProperty as ColumnProperties).children.forEach(queue::addLast) + ViewType.ROW -> (viewProperty as RowProperties).children.forEach(queue::addLast) + ViewType.SERVICE_CARD -> + (viewProperty as ServiceCardProperties).details.forEach(queue::addLast) + ViewType.CARD -> (viewProperty as CardViewProperties).content.forEach(queue::addLast) + ViewType.LIST -> (viewProperty as ListProperties).registerCard.views.forEach(queue::addLast) + ViewType.STACK -> (viewProperty as StackViewProperties).children.forEach(queue::addLast) else -> { - // Handle any other view types if needed + /** Ignore other views that cannot display images* */ } } } - views.forEach { it.loadIcons() } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/CqlContentTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/CqlContentTest.kt index d9724ab580..14a356e110 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/CqlContentTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/CqlContentTest.kt @@ -220,7 +220,9 @@ class CqlContentTest : RobolectricTest() { it.name to if (it.hasResource()) { it.resource.encodeResourceToString() - } else it.valueToString() + } else { + it.valueToString() + } } val expectedResource = resource.parseSampleResourceFromFile().convertToString(true) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/event/EventBusTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/event/EventBusTest.kt index c7da70b671..f1009313e7 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/event/EventBusTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/event/EventBusTest.kt @@ -16,15 +16,15 @@ package org.smartregister.fhircore.quest.event +import com.google.android.fhir.datacapture.extensions.logicalId import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject import kotlin.test.assertEquals -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlin.test.assertTrue import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.QuestionnaireResponse import org.junit.Before import org.junit.Rule @@ -38,39 +38,42 @@ class EventBusTest : RobolectricTest() { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) - @Inject lateinit var eventQueue: EventQueue - private lateinit var eventBus: EventBus - lateinit var emittedEvents: MutableList + @Inject lateinit var eventBus: EventBus @Before fun setUp() { hiltRule.inject() - emittedEvents = mutableListOf() - eventBus = EventBus(eventQueue) } @Test - @OptIn(ExperimentalCoroutinesApi::class) fun testTriggerEventEmitsLogoutEvent1() { - val onSubmitQuestionnaireEvent = - AppEvent.OnSubmitQuestionnaire( - QuestionnaireSubmission( - questionnaireConfig = QuestionnaireConfig(id = "submit-questionnaire"), - QuestionnaireResponse(), - ), - ) + runTest { + val onSubmitQuestionnaireEvent = + AppEvent.OnSubmitQuestionnaire( + QuestionnaireSubmission( + questionnaireConfig = QuestionnaireConfig(id = "questionnaire1"), + questionnaireResponse = QuestionnaireResponse().apply { id = "questionnaireResponse1" }, + ), + ) - runBlockingTest { - val collectJob = launch { + val job = eventBus.events - .getFor("TestTag") - .onEach { appEvent -> emittedEvents.add(appEvent) } + .getFor("thisConsumer") + .onEach { + assertTrue(it is AppEvent.OnSubmitQuestionnaire) + assertEquals( + onSubmitQuestionnaireEvent.questionnaireSubmission.questionnaireConfig.id, + it.questionnaireSubmission.questionnaireConfig.id, + ) + assertEquals( + onSubmitQuestionnaireEvent.questionnaireSubmission.questionnaireResponse.logicalId, + it.questionnaireSubmission.questionnaireResponse.logicalId, + ) + } .launchIn(this) - } + eventBus.triggerEvent(onSubmitQuestionnaireEvent) - collectJob.cancel() + job.cancel() } - - assertEquals(onSubmitQuestionnaireEvent, emittedEvents[0]) } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt index e2a47a82cf..0ec21168c4 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/geowidget/GeoWidgetLauncherViewModelTest.kt @@ -34,7 +34,6 @@ import org.hl7.fhir.r4.model.IdType import org.hl7.fhir.r4.model.Location import org.hl7.fhir.r4.model.ResourceType import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before @@ -143,18 +142,9 @@ class GeoWidgetLauncherViewModelTest : RobolectricTest() { @Test fun testRetrieveLocationsShouldReturnGeoJsonFeatureList() { runTest { - val geoJsonFeatures = viewModel.retrieveLocations(geoWidgetConfiguration) - assertTrue(geoJsonFeatures.isNotEmpty()) - assertEquals("loc1", geoJsonFeatures.first().id) - } - } - - @Test - fun testRetrieveResourcesShouldReturnListOfRepositoryResourceData() { - runTest { - val retrieveResources = viewModel.retrieveResources(geoWidgetConfiguration) - assertFalse(retrieveResources.isEmpty()) - assertEquals("loc1", retrieveResources.first().resource.logicalId) + viewModel.retrieveLocations(geoWidgetConfiguration, null) + assertTrue(viewModel.geoJsonFeatures.value.isNotEmpty()) + assertEquals("loc1", viewModel.geoJsonFeatures.value.first().id) } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModelTest.kt index aa349bee29..a8e7bf5ee7 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModelTest.kt @@ -106,7 +106,7 @@ class RegisterViewModelTest : RobolectricTest() { pageSize = 10, ) registerViewModel.paginateRegisterData(registerId, false) - val paginatedRegisterData = registerViewModel.paginatedRegisterData.value + val paginatedRegisterData = registerViewModel.registerData.value Assert.assertNotNull(paginatedRegisterData) Assert.assertTrue(registerViewModel.pagesDataCache.isNotEmpty()) } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt index df3750e8f3..f0ae3ae24c 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsKtTest.kt @@ -38,8 +38,10 @@ import io.mockk.mockk import io.mockk.slot import io.mockk.verify import javax.inject.Inject +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.Binary import org.hl7.fhir.r4.model.ContactPoint import org.hl7.fhir.r4.model.ResourceType @@ -73,6 +75,7 @@ import org.smartregister.fhircore.engine.domain.model.ResourceConfig import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.ToolBarHomeNavigation import org.smartregister.fhircore.engine.domain.model.ViewType +import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.app.fakes.Faker @@ -95,6 +98,8 @@ class ConfigExtensionsKtTest : RobolectricTest() { @Inject lateinit var fhirEngine: FhirEngine + @Inject lateinit var dispatcherProvider: DispatcherProvider + private val navController = mockk(relaxUnitFun = true, relaxed = true) private val context = mockk(relaxUnitFun = true, relaxed = true) private val navigationMenuConfig by lazy { @@ -672,14 +677,14 @@ class ConfigExtensionsKtTest : RobolectricTest() { @Test fun decodeBinaryResourcesToBitmapOnNavigationMenuClientRegistersDoneCorrectly(): Unit = runBlocking { - defaultRepository.create(addResourceTags = true, binaryImage) - val navigationMenuConfigs = sequenceOf(navigationMenuConfig) + val navigationMenuConfigs = + sequenceOf(navigationMenuConfig).mapNotNull { it.menuIconConfig?.reference } val decodedImageMap = mutableStateMapOf() - runBlocking { - navigationMenuConfigs.decodeBinaryResourcesToBitmap( - this, - registerRepository, - decodedImageMap, + withContext(dispatcherProvider.io()) { + defaultRepository.create(addResourceTags = true, binaryImage) + navigationMenuConfigs.resourceReferenceToBitMap( + fhirEngine = fhirEngine, + decodedImageMap = decodedImageMap, ) } Assert.assertTrue(decodedImageMap.isNotEmpty()) @@ -688,11 +693,14 @@ class ConfigExtensionsKtTest : RobolectricTest() { @Test fun decodeBinaryResourcesToBitmapOnOverflowMenuConfigDoneCorrectly(): Unit = runTest { - defaultRepository.create(addResourceTags = true, binaryImage) - val navigationMenuConfigs = listOf(overflowMenuItemConfig) + val navigationMenuConfigs = sequenceOf(overflowMenuItemConfig).mapNotNull { it.icon?.reference } val decodedImageMap = mutableStateMapOf() - runBlocking { - navigationMenuConfigs.decodeBinaryResourcesToBitmap(this, registerRepository, decodedImageMap) + withContext(Dispatchers.IO) { + defaultRepository.create(addResourceTags = true, binaryImage) + navigationMenuConfigs.resourceReferenceToBitMap( + fhirEngine = fhirEngine, + decodedImageMap = decodedImageMap, + ) } Assert.assertTrue(decodedImageMap.isNotEmpty()) Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) @@ -700,14 +708,12 @@ class ConfigExtensionsKtTest : RobolectricTest() { @Test fun testImageBitmapUpdatedCorrectlyGivenProfileConfiguration(): Unit = runTest { - defaultRepository.create(addResourceTags = true, binaryImage) val decodedImageMap = mutableStateMapOf() - loadRemoteImagesBitmaps( - profileConfiguration.views, - registerRepository = registerRepository, - computedValuesMap = emptyMap(), - configurationRegistry.decodedImageMap, - ) + withContext(Dispatchers.IO) { + fhirEngine.create(binaryImage) + profileConfiguration.views.decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } + Assert.assertTrue(decodedImageMap.isNotEmpty()) Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) } @@ -715,14 +721,11 @@ class ConfigExtensionsKtTest : RobolectricTest() { @Test fun testImageBitmapUpdatedCorrectlyGivenCardViewProperties(): Unit = runTest { val cardViewProperties = profileConfiguration.views[0] as CardViewProperties - defaultRepository.create(addResourceTags = true, binaryImage) val decodedImageMap = mutableStateMapOf() - loadRemoteImagesBitmaps( - listOf(cardViewProperties), - registerRepository = registerRepository, - computedValuesMap = emptyMap(), - decodedImageMap, - ) + withContext(Dispatchers.IO) { + defaultRepository.create(addResourceTags = true, binaryImage) + listOf(cardViewProperties).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) Assert.assertTrue(decodedImageMap.isNotEmpty()) } @@ -730,14 +733,12 @@ class ConfigExtensionsKtTest : RobolectricTest() { @Test fun testImageBitmapUpdatedCorrectlyGivenListViewProperties(): Unit = runTest { val cardViewProperties = profileConfiguration.views[0] as CardViewProperties - defaultRepository.create(addResourceTags = true, binaryImage) val decodedImageMap = mutableStateMapOf() - loadRemoteImagesBitmaps( - listOf(cardViewProperties.content[0]), - registerRepository = registerRepository, - computedValuesMap = emptyMap(), - decodedImageMap, - ) + withContext(Dispatchers.IO) { + defaultRepository.create(addResourceTags = true, binaryImage) + listOf(cardViewProperties.content[0]) + .decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) Assert.assertTrue(decodedImageMap.isNotEmpty()) } @@ -747,13 +748,11 @@ class ConfigExtensionsKtTest : RobolectricTest() { val cardViewProperties = profileConfiguration.views[0] as CardViewProperties val listViewProperties = cardViewProperties.content[0] as ListProperties val decodedImageMap = mutableStateMapOf() - defaultRepository.create(addResourceTags = true, binaryImage) - loadRemoteImagesBitmaps( - listOf(listViewProperties.registerCard.views[0]), - registerRepository = registerRepository, - computedValuesMap = emptyMap(), - decodedImageMap, - ) + withContext(Dispatchers.IO) { + defaultRepository.create(addResourceTags = true, binaryImage) + listOf(listViewProperties.registerCard.views[0]) + .decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) Assert.assertTrue(decodedImageMap.isNotEmpty()) } @@ -763,14 +762,11 @@ class ConfigExtensionsKtTest : RobolectricTest() { val cardViewProperties = profileConfiguration.views[0] as CardViewProperties val listViewProperties = cardViewProperties.content[0] as ListProperties val columnProperties = listViewProperties.registerCard.views[0] as ColumnProperties - defaultRepository.create(addResourceTags = true, binaryImage) val decodedImageMap = mutableStateMapOf() - loadRemoteImagesBitmaps( - listOf(columnProperties.children[0]), - registerRepository = registerRepository, - computedValuesMap = emptyMap(), - decodedImageMap, - ) + withContext(Dispatchers.IO) { + defaultRepository.create(addResourceTags = true, binaryImage) + listOf(columnProperties.children[0]).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) Assert.assertTrue(decodedImageMap.isNotEmpty()) } @@ -793,20 +789,14 @@ class ConfigExtensionsKtTest : RobolectricTest() { ), ), ) - val emptyComputedValuesMap = mutableMapOf() val decodedImageMap = mutableStateMapOf() - - loadRemoteImagesBitmaps( - listOf(rowProperties), - registerRepository = registerRepository, - computedValuesMap = emptyComputedValuesMap, - decodedImageMap = decodedImageMap, - ) + withContext(Dispatchers.IO) { + listOf(rowProperties).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } Assert.assertTrue(decodedImageMap.isEmpty()) Assert.assertTrue(!decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) } - @Test(expected = Exception::class) fun testExceptionCaughtOnDecodingBitmap() = runTest { val cardViewProperties = profileConfiguration.views[0] as CardViewProperties val listViewProperties = cardViewProperties.content[0] as ListProperties @@ -824,7 +814,6 @@ class ConfigExtensionsKtTest : RobolectricTest() { ), ), ) - val emptyComputedValuesMap = mutableMapOf() val decodedImageMap = mutableStateMapOf() coEvery { defaultRepository.loadResource(anyString()) } returns @@ -834,13 +823,10 @@ class ConfigExtensionsKtTest : RobolectricTest() { this.data = "gibberish value".toByteArray() } - loadRemoteImagesBitmaps( - listOf(rowProperties), - registerRepository = registerRepository, - computedValuesMap = emptyComputedValuesMap, - decodedImageMap = decodedImageMap, - ) + withContext(Dispatchers.IO) { + listOf(rowProperties).decodeImageResourcesToBitmap(fhirEngine, decodedImageMap) + } Assert.assertTrue(decodedImageMap.isEmpty()) - Assert.assertTrue(!decodedImageMap.containsKey("null Reference")) + Assert.assertFalse(decodedImageMap.containsKey("null Reference")) } }