From 58ec04d4df0e11ddb5ec8fd3df729b884fe9d175 Mon Sep 17 00:00:00 2001 From: Elly Kitoto Date: Mon, 14 Oct 2024 21:34:20 +0300 Subject: [PATCH] Refactor loading bitmap images (#3546) * Add date service to rules engine facts map (#3519) * Add summary mode (#3500) Co-authored-by: Elly Kitoto * Refactor PDF config properties in QuestionnaireConfig (#3498) * Refactor to use PdfConfig Initially using QuestionnaireConfig for simplicity. * Process multi QRs in HtmlPopulator * Add new tag to check if Questionnaire has been submitted * Remove subjectType since subjectReference is used * Fix test and spotless * Address review * spotless * Cleanup * spotless * Fix test --------- Co-authored-by: Elly Kitoto * Fix error validation highlighting not working when submit button is clicked bug (#3525) * Change progress bar color to blue (#3428) * update in-progress color on the map view. (#3529) Signed-off-by: Lentumunai-Mark * Add medication sort custom search param (#3534) * Add medication sort custom search param * Run spotlessApply * Fix failing time based test * Pld docs adjustment (#3540) * sync typo * correct heading title * update header definition for p2p * upgrade packages * Update Geowidget to only show the Set Location dialog if no Locations to sync by have been selected (#3526) * Don't show no results dialogue on map when locations selected on multiselect. Signed-off-by: Lentumunai-Mark * Fix failing tests. Signed-off-by: Lentumunai-Mark --------- Signed-off-by: Lentumunai-Mark * Add Exit Dialog (#3487) * Add exit dialog * Fix a typo * Show Dialog only when Map or Register is visible * PR feedback changes * Fix failing check * Remove incorrect test * Refactor dialog condition logic --------- Co-authored-by: Benjamin Mwalimu * Refactor working with bitmap images Signed-off-by: Elly Kitoto * Add the EUSM Burundi flavour and Rename the existing EUSM flavour (#3548) * - Add the EUSM Burundi flavour - Rename the exisiting EUSM flavour * - Update the app naming * - update the flavour naming * Configure submit anyway button (#3535) * Update the Burundi EUSM flavor (#3553) * - Add the EUSM Burundi flavour - Rename the exisiting EUSM flavour * - Update the app naming * - update the flavour naming * - Update the flavor naming - Update the documentation * - Update the flavors section of the documentation to clarify on the naming convention * Bum p up data capture version (#3547) * Map search bug fixes (#3516) * - Updating the infinite scroll paging - Updating the behaviour after clearing search bar on the maps * - spotless apply * - Fix breaking tests on geo widget module * - Fix tests * - Run spotless * - Import `LazyPagingItems` * - Adding tests * - Update the APK naming * Ignore test to be refactored later Signed-off-by: Elly Kitoto * - Run spotless * Update kujaku version. Signed-off-by: Lentumunai-Mark --------- Signed-off-by: Elly Kitoto Signed-off-by: Lentumunai-Mark Co-authored-by: Lentumunai Mark <90028422+Lentumunai-Mark@users.noreply.github.com> Co-authored-by: Elly Kitoto Co-authored-by: Lentumunai-Mark * Fix loading related resources Signed-off-by: Elly Kitoto --------- Signed-off-by: Lentumunai-Mark Signed-off-by: Elly Kitoto Co-authored-by: Rkareko <47570855+Rkareko@users.noreply.github.com> Co-authored-by: FikriMilano Co-authored-by: Lentumunai Mark <90028422+Lentumunai-Mark@users.noreply.github.com> Co-authored-by: Peter Lubell-Doughtie Co-authored-by: Hamza Ahmed Khan <70560433+hamza-vd@users.noreply.github.com> Co-authored-by: Benjamin Mwalimu Co-authored-by: Lentumunai-Mark --- .../configuration/ConfigurationRegistry.kt | 3 - .../configuration/QuestionnaireConfig.kt | 2 + .../configuration/view/ListProperties.kt | 1 + .../configuration/view/StackViewProperties.kt | 2 +- .../engine/data/local/DefaultRepository.kt | 22 ++-- .../rulesengine/ResourceDataRulesExecutor.kt | 4 +- .../engine/rulesengine/RulesFactory.kt | 24 +++-- .../local/register/RegisterRepositoryTest.kt | 44 ++++++++ .../engine/rulesengine/RulesFactoryTest.kt | 31 +++++- android/gradle/libs.versions.toml | 4 +- android/quest/build.gradle.kts | 15 ++- .../ui/main/components/AppDrawerTest.kt | 1 + .../main/components/TopScreenSectionTest.kt | 5 + .../ui/profile/ProfileScreenTest.kt | 2 +- .../MemberProfileBottomSheetViewTest.kt | 1 + .../components/RegisterCardListTest.kt | 4 + .../shared/components/ActionableButtonTest.kt | 1 + .../ui/shared/components/ExtendedFabTest.kt | 4 + .../ui/shared/components/ServiceCardTest.kt | 4 + .../ui/shared/components/ViewGeneratorTest.kt | 2 +- .../res/drawable/ic_app_logo.png | Bin .../res/drawable/ic_launcher.png | Bin .../src/eusmMg/res/drawable/ic_app_logo.png | Bin 0 -> 47799 bytes .../src/eusmMg/res/drawable/ic_launcher.png | Bin 0 -> 2442 bytes .../fhircore/quest/data/DataMigration.kt | 2 +- .../ui/bottomsheet/SummaryBottomSheetView.kt | 4 +- .../ui/geowidget/GeoWidgetLauncherFragment.kt | 2 + .../ui/geowidget/GeoWidgetLauncherScreen.kt | 3 + .../geowidget/GeoWidgetLauncherViewModel.kt | 10 ++ .../fhircore/quest/ui/main/AppMainActivity.kt | 26 +++++ .../quest/ui/main/AppMainViewModel.kt | 19 ---- .../quest/ui/main/components/AppDrawer.kt | 102 +++++++++++------- .../ui/main/components/TopScreenSection.kt | 35 +++--- .../quest/ui/profile/ProfileFragment.kt | 15 ++- .../quest/ui/profile/ProfileScreen.kt | 22 +++- .../quest/ui/profile/ProfileUiState.kt | 4 - .../quest/ui/profile/ProfileViewModel.kt | 100 +++++++---------- .../MemberProfileBottomSheetView.kt | 5 + .../ui/questionnaire/QuestionnaireActivity.kt | 1 + .../quest/ui/register/RegisterFragment.kt | 20 ++-- .../quest/ui/register/RegisterScreen.kt | 6 ++ .../quest/ui/register/RegisterViewModel.kt | 9 ++ .../register/components/RegisterCardList.kt | 6 +- .../ui/shared/components/ActionableButton.kt | 8 ++ .../quest/ui/shared/components/CardView.kt | 15 ++- .../quest/ui/shared/components/ExtendedFab.kt | 6 ++ .../quest/ui/shared/components/Image.kt | 64 ++++++----- .../quest/ui/shared/components/List.kt | 10 +- .../quest/ui/shared/components/SearchBar.kt | 4 +- .../quest/ui/shared/components/ServiceCard.kt | 32 +++--- .../quest/ui/shared/components/StackView.kt | 13 ++- .../ui/shared/components/ViewGenerator.kt | 22 ++-- .../ui/shared/components/ViewRenderer.kt | 15 ++- .../quest/util/extensions/ConfigExtensions.kt | 10 +- android/quest/src/main/res/values/strings.xml | 2 + .../quest/ui/main/AppMainActivityTest.kt | 1 - .../quest/ui/profile/ProfileFragmentTest.kt | 3 +- .../util/extensions/ConfigExtensionsKtTest.kt | 24 +++-- docs/engineering/app/automated-releases.mdx | 9 +- .../configuring/add-application-flavors.mdx | 10 +- 60 files changed, 508 insertions(+), 307 deletions(-) rename android/quest/src/{eusm => eusmBi}/res/drawable/ic_app_logo.png (100%) rename android/quest/src/{eusm => eusmBi}/res/drawable/ic_launcher.png (100%) create mode 100644 android/quest/src/eusmMg/res/drawable/ic_app_logo.png create mode 100644 android/quest/src/eusmMg/res/drawable/ic_launcher.png 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 42f4a5bf0e..2c099a831a 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 @@ -18,8 +18,6 @@ package org.smartregister.fhircore.engine.configuration import android.content.Context import android.database.SQLException -import android.graphics.Bitmap -import androidx.compose.runtime.mutableStateMapOf import ca.uhn.fhir.context.ConfigurationException import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.parser.DataFormatException @@ -98,7 +96,6 @@ constructor( val configsJsonMap = mutableMapOf() val configCacheMap = mutableMapOf() - val decodedImageMap = mutableStateMapOf() val localizationHelper: LocalizationHelper by lazy { LocalizationHelper(this) } private val supportedFileExtensions = listOf("json", "properties") private var _isNonProxy = BuildConfig.IS_NON_PROXY_APK diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt index 89ff99b756..ff865ce566 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt @@ -66,6 +66,7 @@ data class QuestionnaireConfig( val managingEntityRelationshipCode: String? = null, val uniqueIdAssignment: UniqueIdAssignmentConfig? = null, val linkIds: List? = null, + val showSubmitAnywayButton: String = "false", ) : java.io.Serializable, Parcelable { fun interpolate(computedValuesMap: Map) = @@ -100,6 +101,7 @@ data class QuestionnaireConfig( uniqueIdAssignment?.copy(linkId = uniqueIdAssignment.linkId.interpolate(computedValuesMap)), linkIds = linkIds?.onEach { it.linkId.interpolate(computedValuesMap) }, saveButtonText = saveButtonText?.interpolate(computedValuesMap), + showSubmitAnywayButton = showSubmitAnywayButton.interpolate(computedValuesMap), ) } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ListProperties.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ListProperties.kt index 30ba712769..2f4db021bf 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ListProperties.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/ListProperties.kt @@ -69,4 +69,5 @@ data class ListResourceConfig( val sortConfig: SortConfig? = null, val fhirPathExpression: String? = null, val relatedResources: List = emptyList(), + val isRevInclude: Boolean = true, ) : Parcelable, java.io.Serializable diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/StackViewProperties.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/StackViewProperties.kt index 65fff51f32..860a518339 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/StackViewProperties.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/view/StackViewProperties.kt @@ -36,7 +36,7 @@ data class StackViewProperties( override val clickable: String = "false", override val visible: String = "true", val opacity: Float = 0f, - val size: Int? = 0, + val size: Int = 0, val children: List = emptyList(), ) : ViewProperties(), Parcelable { override fun interpolate(computedValuesMap: Map): StackViewProperties { 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 acba6336e1..abccc74501 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 @@ -1070,14 +1070,20 @@ constructor( 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, - ) + val maxSublistIndex = min(toIndex, searchResults.size) + + if (fromIndex < maxSublistIndex) { + with(searchResults.subList(fromIndex, maxSublistIndex)) { + mapResourceToRepositoryResourceData( + relatedResourcesConfig = relatedResourcesConfig, + configComputedRuleValues = configComputedRuleValues, + secondaryResourceConfigs = secondaryResourceConfigs, + filterActiveResources = filterActiveResources, + baseResourceConfig = baseResourceConfig, + ) + } + } else { + emptyList() } } else { searchResults.mapResourceToRepositoryResourceData( 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 97bf938c12..47b2dabfb3 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 @@ -20,6 +20,7 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateMap import com.google.android.fhir.datacapture.extensions.logicalId +import javax.inject.Inject import org.hl7.fhir.r4.model.Resource import org.jeasy.rules.api.Facts import org.smartregister.fhircore.engine.configuration.view.ListProperties @@ -29,7 +30,6 @@ import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.RuleConfig import org.smartregister.fhircore.engine.domain.model.ViewType import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid -import javax.inject.Inject /** * This class is used to fire rules used to extract and manipulate data from FHIR resources. @@ -115,7 +115,7 @@ class ResourceDataRulesExecutor @Inject constructor(val rulesFactory: RulesFacto this.forEach { baseListResource -> val relatedResourcesQueue = ArrayDeque>>().apply { - addFirst(Pair(baseListResource, listOf(listResourceConfig))) + addFirst(Pair(baseListResource, listResourceConfig.relatedResources)) } val listItemRelatedResources = mutableMapOf>() 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 1ff6e122b1..d9177e10af 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 @@ -183,6 +183,7 @@ constructor( relatedResourceKey: String, referenceFhirPathExpression: String?, relatedResourcesMap: Map>? = null, + isRevInclude: Boolean = true, ): List { val value: List = relatedResourcesMap?.get(relatedResourceKey) @@ -192,14 +193,23 @@ constructor( emptyList() } - return if (referenceFhirPathExpression.isNullOrEmpty()) { - value + if (referenceFhirPathExpression.isNullOrEmpty()) { + return value + } + + // Reverse search; look for related resource that references the provided resource + return if (isRevInclude) { + value.filter { res -> + fhirPathDataExtractor.extractData(res, referenceFhirPathExpression).all { + resource.logicalId == it.primitiveValue().extractLogicalIdUuid() + } + } } else { - value.filter { - resource.logicalId == - fhirPathDataExtractor - .extractValue(it, referenceFhirPathExpression) - .extractLogicalIdUuid() + // Forward search; extract value provided resource, then search resources with matching id + value.filter { res -> + fhirPathDataExtractor.extractData(resource, referenceFhirPathExpression).all { + res.logicalId == it.primitiveValue().extractLogicalIdUuid() + } } } } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/register/RegisterRepositoryTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/register/RegisterRepositoryTest.kt index 1dc0027224..142cebf0f6 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/register/RegisterRepositoryTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/data/local/register/RegisterRepositoryTest.kt @@ -183,6 +183,50 @@ class RegisterRepositoryTest : RobolectricTest() { } } + @Ignore("Refactor this test") + @Test + fun countRegisterDataWithParamsAndRelatedEntityLocationFilter() { + runTest { + val paramsList = + arrayListOf( + ActionParameter( + key = "paramsName", + paramType = ActionParameterType.PARAMDATA, + value = "testing1", + dataType = DataType.STRING, + linkId = null, + ), + ActionParameter( + key = "paramName2", + paramType = ActionParameterType.PARAMDATA, + value = "testing2", + dataType = DataType.STRING, + linkId = null, + ), + ) + paramsList + .asSequence() + .filter { it.paramType == ActionParameterType.PARAMDATA && it.value.isNotEmpty() } + .associate { it.key to it.value } + val paramsMap = emptyMap() + val searchSlot = slot() + every { + registerRepository.retrieveRegisterConfiguration(PATIENT_REGISTER, emptyMap()) + } returns + RegisterConfiguration( + appId = "app", + id = PATIENT_REGISTER, + fhirResource = fhirResourceConfig(), + filterDataByRelatedEntityLocation = true, + ) + coEvery { fhirEngine.count(capture(searchSlot)) } returns 20 + val recordsCount = + registerRepository.countRegisterData(registerId = PATIENT_REGISTER, paramsMap = paramsMap) + Assert.assertEquals(ResourceType.Group, searchSlot.captured.type) + Assert.assertEquals(20, recordsCount) + } + } + @Test fun testLoadRegisterDataWithForwardAndReverseIncludedResources() = runTest(timeout = 90.seconds) { diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/rulesengine/RulesFactoryTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/rulesengine/RulesFactoryTest.kt index 95b342dabb..d303149695 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/rulesengine/RulesFactoryTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/rulesengine/RulesFactoryTest.kt @@ -72,6 +72,7 @@ import org.smartregister.fhircore.engine.rule.CoroutineTestRule import org.smartregister.fhircore.engine.rulesengine.services.LocationService import org.smartregister.fhircore.engine.util.DispatcherProvider 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.plusYears import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor @@ -298,6 +299,29 @@ class RulesFactoryTest : RobolectricTest() { Assert.assertEquals("careplan-1", result[0].logicalId) } + @Test + fun retrieveRelatedResourcesReturnsCorrectResourceWithForwardInclude() { + val patient = Faker.buildPatient() + val group = + Group().apply { + id = "grp1" + addMember( + Group.GroupMemberComponent().apply { entity = patient.asReference() }, + ) + } + populateFactsWithResources(group) + val result = + rulesEngineService.retrieveRelatedResources( + resource = group, + relatedResourceKey = ResourceType.Patient.name, + referenceFhirPathExpression = "Group.member.entity.reference", + isRevInclude = false, + ) + Assert.assertEquals(1, result.size) + Assert.assertEquals("Patient", result[0].resourceType.name) + Assert.assertEquals(patient.logicalId, result[0].logicalId) + } + @Test fun retrieveRelatedResourcesWithoutReferenceReturnsResources() { populateFactsWithResources() @@ -887,13 +911,16 @@ class RulesFactoryTest : RobolectricTest() { Assert.assertTrue(result.isEmpty()) } - private fun populateFactsWithResources() { + private fun populateFactsWithResources(vararg resource: Resource = emptyArray()) { val carePlanRelatedResource = mutableListOf(Faker.buildCarePlan()) - val patientRelatedResource = mutableListOf(Faker.buildPatient()) + val patient = Faker.buildPatient() + val patientRelatedResource = mutableListOf(patient) + val facts = ReflectionHelpers.getField(rulesFactory, "facts") facts.apply { put(carePlanRelatedResource[0].resourceType.name, carePlanRelatedResource) put(patientRelatedResource[0].resourceType.name, patientRelatedResource) + resource.forEach { put(it.resourceType.name, it) } } ReflectionHelpers.setField(rulesFactory, "facts", facts) } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 6dea09e86e..0cc0cac3ef 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -26,7 +26,7 @@ easyRulesCore = "4.1.1-SNAPSHOT" espresso-core = "3.6.1" fhir-sdk-contrib-barcode = "0.1.0-beta3-preview7-rc1-SNAPSHOT" fhir-sdk-contrib-locationwidget = "0.1.0-alpha01-preview2-rc1-SNAPSHOT" -fhir-sdk-data-capture = "1.1.0-preview14-rc3-SNAPSHOT" +fhir-sdk-data-capture = "1.2.0-preview-1-SNAPSHOT" fhir-sdk-engine = "1.0.0-preview14-rc3-SNAPSHOT" fhir-sdk-knowledge = "0.1.0-alpha03-preview5-rc1-SNAPSHOT" fhir-sdk-workflow = "0.1.0-alpha04-preview10-rc1-SNAPSHOT" @@ -49,7 +49,7 @@ 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.6-2-SNAPSHOT" +kujaku-library = "0.10.7-SNAPSHOT" kujaku-mapbox-sdk-turf = "7.2.0" leakcanary-android = "2.10" lifecycle= "2.8.5" diff --git a/android/quest/build.gradle.kts b/android/quest/build.gradle.kts index 537bdce8f5..1d22c6e27b 100644 --- a/android/quest/build.gradle.kts +++ b/android/quest/build.gradle.kts @@ -321,11 +321,18 @@ android { manifestPlaceholders["appLabel"] = "PSI WFA" } - create("eusm") { + create("eusmMg") { dimension = "apps" - applicationIdSuffix = ".eusm" - versionNameSuffix = "-eusm" - manifestPlaceholders["appLabel"] = "EUSM" + applicationIdSuffix = ".eusmMg" + versionNameSuffix = "-eusmMg" + manifestPlaceholders["appLabel"] = "EUSM Madagascar" + } + + create("eusmBi") { + dimension = "apps" + applicationIdSuffix = ".eusmBi" + versionNameSuffix = "-eusmBi" + manifestPlaceholders["appLabel"] = "EUSM Burundi" } create("demoEir") { diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/main/components/AppDrawerTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/main/components/AppDrawerTest.kt index 72a8151442..9f11816f26 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/main/components/AppDrawerTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/main/components/AppDrawerTest.kt @@ -184,6 +184,7 @@ class AppDrawerTest { appVersionPair = Pair(1, "0.0.1"), onCountUnSyncedResources = {}, unSyncedResourceCount = remember { mutableIntStateOf(0) }, + decodeImage = null, ) } } 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 0210d52b4e..11316b4c6f 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 @@ -52,6 +52,7 @@ class TopScreenSectionTest { navController = TestNavHostController(LocalContext.current), isSearchBarVisible = true, onClick = {}, + decodeImage = null, ) } @@ -83,6 +84,7 @@ class TopScreenSectionTest { navController = TestNavHostController(LocalContext.current), isSearchBarVisible = true, onClick = {}, + decodeImage = null, ) } @@ -116,6 +118,7 @@ class TopScreenSectionTest { navController = TestNavHostController(LocalContext.current), isSearchBarVisible = true, onClick = {}, + decodeImage = null, ) } @@ -135,6 +138,7 @@ class TopScreenSectionTest { navController = TestNavHostController(LocalContext.current), isSearchBarVisible = true, onClick = {}, + decodeImage = null, ) } composeTestRule.onNodeWithTag(TRAILING_QR_SCAN_ICON_BUTTON_TEST_TAG).assertDoesNotExist() @@ -150,6 +154,7 @@ class TopScreenSectionTest { navController = TestNavHostController(LocalContext.current), isSearchBarVisible = true, onClick = {}, + decodeImage = null, ) } composeTestRule.onNodeWithTag(TRAILING_QR_SCAN_ICON_BUTTON_TEST_TAG).assertIsDisplayed() diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/profile/ProfileScreenTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/profile/ProfileScreenTest.kt index 92d6d47417..f3ef783bfa 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/profile/ProfileScreenTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/profile/ProfileScreenTest.kt @@ -75,8 +75,8 @@ class ProfileScreenTest { ProfileScreen( navController = rememberNavController(), profileUiState = profileUiState, - onEvent = {}, snackStateFlow = snackBarStateFlow, + onEvent = {}, ) } } diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/profile/components/MemberProfileBottomSheetViewTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/profile/components/MemberProfileBottomSheetViewTest.kt index 035be7a946..3cc6f2a24f 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/profile/components/MemberProfileBottomSheetViewTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/profile/components/MemberProfileBottomSheetViewTest.kt @@ -50,6 +50,7 @@ class MemberProfileBottomSheetViewTest { buttonProperties = buttonProperties, onViewProfile = { /*Do nothing*/}, resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), + decodeImage = null, ) } } diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/components/RegisterCardListTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/components/RegisterCardListTest.kt index 0eb987fcc1..b8b15cb234 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/components/RegisterCardListTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/components/RegisterCardListTest.kt @@ -60,6 +60,7 @@ class RegisterCardListTest { registerUiState = RegisterUiState(), currentPage = mutableStateOf(1), onSearchByQrSingleResultAction = {}, + decodeImage = decodeImage, ) } @@ -85,6 +86,7 @@ class RegisterCardListTest { registerUiState = RegisterUiState(), currentPage = mutableStateOf(1), onSearchByQrSingleResultAction = {}, + decodeImage = decodeImage, ) } @@ -117,6 +119,7 @@ class RegisterCardListTest { currentPage = mutableStateOf(1), showPagination = true, onSearchByQrSingleResultAction = {}, + decodeImage = decodeImage, ) } @@ -143,6 +146,7 @@ class RegisterCardListTest { registerUiState = RegisterUiState(), currentPage = mutableStateOf(1), onSearchByQrSingleResultAction = {}, + decodeImage = decodeImage, ) } diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ActionableButtonTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ActionableButtonTest.kt index 8a84deab04..33995a3828 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ActionableButtonTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ActionableButtonTest.kt @@ -123,6 +123,7 @@ class ActionableButtonTest { ), resourceData = ResourceData("id", ResourceType.Patient, computedValuesMap), navController = TestNavHostController(LocalContext.current), + decodeImage = null, ) } } diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ExtendedFabTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ExtendedFabTest.kt index 79dc736a41..ff7e86c534 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ExtendedFabTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ExtendedFabTest.kt @@ -63,6 +63,7 @@ class ExtendedFabTest { resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = TestNavHostController(LocalContext.current), lazyListState = null, + decodeImage = null, ) } } @@ -90,6 +91,7 @@ class ExtendedFabTest { resourceData = null, navController = TestNavHostController(LocalContext.current), lazyListState = null, + decodeImage = null, ) } composeRule @@ -150,6 +152,7 @@ class ExtendedFabTest { resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = TestNavHostController(LocalContext.current), lazyListState = null, + decodeImage = null, ) } composeRule.run { @@ -188,6 +191,7 @@ class ExtendedFabTest { resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = TestNavHostController(LocalContext.current), lazyListState = null, + decodeImage = null, ) } composeRule.run { diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ServiceCardTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ServiceCardTest.kt index 32bcd09ae9..840b68933f 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ServiceCardTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ServiceCardTest.kt @@ -46,6 +46,7 @@ class ServiceCardTest { serviceCardProperties = initTestServiceCardProperties(), resourceData = resourceData, navController = TestNavHostController(LocalContext.current), + decodeImage = decodeImage, ) } composeRule @@ -62,6 +63,7 @@ class ServiceCardTest { initTestServiceCardProperties(serviceStatus = ServiceStatus.OVERDUE.name, text = "1"), resourceData = resourceData, navController = TestNavHostController(LocalContext.current), + decodeImage = decodeImage, ) } composeRule.onNodeWithText("1", useUnmergedTree = true).assertExists().assertIsDisplayed() @@ -74,6 +76,7 @@ class ServiceCardTest { serviceCardProperties = initTestServiceCardProperties(visible = "false"), resourceData = resourceData, navController = TestNavHostController(LocalContext.current), + decodeImage = decodeImage, ) } composeRule.onNodeWithText("Next visit 09-10-2022", useUnmergedTree = true).assertDoesNotExist() @@ -86,6 +89,7 @@ class ServiceCardTest { serviceCardProperties = initTestServiceCardProperties(showVerticalDivider = false), resourceData = resourceData, navController = TestNavHostController(LocalContext.current), + decodeImage = decodeImage, ) } composeRule.onNodeWithTag(DIVIDER_TEST_TAG).assertDoesNotExist() diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ViewGeneratorTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ViewGeneratorTest.kt index 5a0f402ff8..54c19c934a 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ViewGeneratorTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/shared/components/ViewGeneratorTest.kt @@ -383,7 +383,7 @@ class ViewGeneratorTest { ), resourceData = resourceData, navController = TestNavHostController(LocalContext.current), - decodedImageMap = decodedImageMap, + decodeImage = decodedImageMap, ) } composeRule diff --git a/android/quest/src/eusm/res/drawable/ic_app_logo.png b/android/quest/src/eusmBi/res/drawable/ic_app_logo.png similarity index 100% rename from android/quest/src/eusm/res/drawable/ic_app_logo.png rename to android/quest/src/eusmBi/res/drawable/ic_app_logo.png diff --git a/android/quest/src/eusm/res/drawable/ic_launcher.png b/android/quest/src/eusmBi/res/drawable/ic_launcher.png similarity index 100% rename from android/quest/src/eusm/res/drawable/ic_launcher.png rename to android/quest/src/eusmBi/res/drawable/ic_launcher.png diff --git a/android/quest/src/eusmMg/res/drawable/ic_app_logo.png b/android/quest/src/eusmMg/res/drawable/ic_app_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c054c39f8ed402fcbe2e01db5461de1dd6f1c83c GIT binary patch literal 47799 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

H&>V_DdDk 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 b7U5`uOJ$yk`*0Y7fJ^hCj%vjN%UAygc+}kT literal 0 HcmV?d00001 diff --git a/android/quest/src/eusmMg/res/drawable/ic_launcher.png b/android/quest/src/eusmMg/res/drawable/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8da44beb41fa1a0b5e47cacd92ba96f63df9715f GIT binary patch literal 2442 zcmV;533c{~P)n8a&06)t!gc;Oo? zo$~WC#x;x;jJh(#3Y~*xxi~UiVg2(e#={IdBfy9l6TF!{^@oeOYs)l{xb_k8|r zU^o~wK0Z!kV`Bn3zz4$-kH=S=+7c$!j7Fm$;2ZSo#HUqNRrpgC7*lmF(%^;#c)Pp1 z`)bo#!sJC1McE0@=bL5tX5+%^F9A5o$g)i5&!2zX6aglOS2mk%IbXaN3PWx|c5X~^ zT!-yhz;e}BWATY>(M7xnb?tmYfU zg#d*K5DW$r`}Xbo1h_LpfW=*1U2Ci)0G`v6$dJvIpc8=C!2@nVb;(Pb*y%pS< zA;6NuhYvSeNq|?pqjdKxL-g2N5!(8CfPVU~AjQ(El>|ua-o5)aa945ygi{KAquWpK zMAH|(`_;e0^!#942v7_-<7rI*b;@^#oN;>M%!m-67?5OzzWJJ;?7@`wXIW9{Cme!~ zxD!HvVuYU#_Vx!(gy`18Zu&73-~8$@ZTN?m{?s3(m$`3mBOLd?%fro?$Oo=(cWQG3jFKMAalMD^A#M1IQPTkbPD)YBPXL67 zV?(3#^<#djXTm<#HmH5Be!)d+vhW#>??a;*`raD>TE)LJ6V@Jb(@E~2PqGMWa$N7m z=UrN;Lda_DaM6#rAL)z87mcb)S^!xYqu;UzUdtrZF!6W3;B}7I1xx{Ei1{hTuL+o%8Tr4{_3J;O;$tzx2Oww{x;miZkX>LzE|DlIJlybSRc zBOTR(OUW1wed(V*Z7|R;$)zO#JnVZ18AisB|wlL?DH@A=w~NG*3M`#K6}JV-{(1pT51Ady#LsLB9>1p-oq^w zNghQlK>;2=ZG8bYzbXU}0tf+w02cuhqhyhmEQA0#mI@x_&KskXr{9iJQc;Bf#S{Rm zlvty|Ec!R6BHE-*2v8&e&PHTy$rx)iUpqRiO_ZkoeKf7o-gA;+{B#}&Q6!~ITPK8^u2CQh=1+Q$otXvw!FFC9Yix%*Ipdtb#DIg!?ebnz1N&w~s zvw@fgWRbZ0?1)yTg18@^Chlh!cKxwmGK{~y6Vv)1AwX7#iiC~bK(&PE15BO=z@v?1 zqK5VU!zfz1{hD|}02B6#A>V8iSi;Chun<7x1B3uV03kr}1VG1oCL(L=LHXd|8L_O0 zjM%&g(Bn^P2^TEB(M%!N81^nYc({j6ox`GX+i)4 z0{D0ya0mZgA$a2SNI_+hLI48-7?}w_WDgfast~|{0O-I`DNzXC;J!u`A;??-L??Wi z1(0Nwes^ZXFtDpW--@dcV4nH_u^P$(?8;G|j;*E038VTUA5aySYaW2rQ0zM}iu=3s zS4DiK6wA|*I>a(CwkhQ*J)Erl%!lc!7|h-=iPpA_|DPqbK?9?euj;3om|0qgc>oAA ziYE4QSNzftgpJ(Tr1x-dxU{w?!JBYK19rzF|Uz-$!TK(B9i(K7?FqHVgE=@hP=OgbE6uB)z=7XV(5=3lQ5FHr^I zkYrQO>?ssD+Uz(x#Qn$oTHL>xL(iMR)Eslv3*@{YRK4NMH&nvi)ZwCU_XMcbA(73U z(9Q&%FW^kM$rBduJvvM)^&@jIL^k8_kOg&OmWw+&oRWnN7&tBlURW%EbQ~Ij3EopI zyd*28w5kPpW*qtf99xECh*6e4-#`_1mnWee0A==er?NOVbRitxEd;Q@;oYVR(An9! zRtR9M1?<_g=XTQsIB?)V17AEW1Sm`Zuh$#dwQJYSW(a`SZ!B52Zr#+9=3 z#Glt~*sx(^dwcsctn|R(;GpF3ct#ls{j;2)J+ss4jDm}{wzg;2uV4Q;aKwfextaP- zNhrB|)v8sMxB=s)%F4=5aad~FvSrJ?EiEmZ%Mo~XaKK#L0Zzb;&e3u+y?@d$B6@FK zUEPP->iFlCj2jpYWe#w#5}be=aHMlJF9uc7DZj6(s_KKawY49qsj2yBb#--RIReiP z4!{LC(YaYvQ3S(HmgsSAS(#(0&P7p(7bZ-YFk!-k2`dfzALz}|HIY8@PXGV_07*qo IM6N<$f`n?Az5oCK literal 0 HcmV?d00001 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 a565ea5d29..09ec272474 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 @@ -212,7 +212,7 @@ constructor( if (migrationConfig.createLocalChangeEntitiesAfterPurge) { defaultRepository.addOrUpdate(resource = updatedResource as Resource) } else { - defaultRepository.createRemote(resource = *arrayOf(updatedResource as Resource)) + defaultRepository.createRemote(resource = arrayOf(updatedResource as Resource)) } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/bottomsheet/SummaryBottomSheetView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/bottomsheet/SummaryBottomSheetView.kt index 08eee50cc7..4842fc5186 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/bottomsheet/SummaryBottomSheetView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/bottomsheet/SummaryBottomSheetView.kt @@ -17,8 +17,6 @@ package org.smartregister.fhircore.quest.ui.bottomsheet import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.remember import androidx.navigation.NavController import org.smartregister.fhircore.engine.configuration.view.ViewProperties import org.smartregister.fhircore.engine.domain.model.ResourceData @@ -34,6 +32,6 @@ fun SummaryBottomSheetView( viewProperties = properties, resourceData = resourceData, navController = navController, - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } 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 18c1046818..348231fcf0 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 @@ -155,6 +155,7 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { navController = findNavController(), unSyncedResourceCount = appMainViewModel.unSyncedResourcesCount, onCountUnSyncedResources = appMainViewModel::updateUnSyncedResourcesCount, + decodeImage = { geoWidgetLauncherViewModel.getImageBitmap(it) }, ) }, snackbarHost = { snackBarHostState -> @@ -189,6 +190,7 @@ class GeoWidgetLauncherFragment : Fragment(), OnSyncListener { isFirstTimeSync = geoWidgetLauncherViewModel.isFirstTime(), appDrawerUIState = appDrawerUIState, onAppMainEvent = appMainViewModel::onEvent, + decodeImage = { TODO("Return bitmap") }, ) } } 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 e2d016c7b9..0c91a2b3c8 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 @@ -16,6 +16,7 @@ package org.smartregister.fhircore.quest.ui.geowidget +import android.graphics.Bitmap import android.view.View import android.widget.FrameLayout import androidx.compose.foundation.layout.Box @@ -58,6 +59,7 @@ fun GeoWidgetLauncherScreen( search: (String) -> Unit, isFirstTimeSync: Boolean, appDrawerUIState: AppDrawerUIState, + decodeImage: ((String) -> Bitmap?)?, onAppMainEvent: (AppMainEvent) -> Unit, ) { val context = LocalContext.current @@ -87,6 +89,7 @@ fun GeoWidgetLauncherScreen( isFilterIconEnabled = false, topScreenSection = geoWidgetConfiguration.topScreenSection, navController = navController, + decodeImage = decodeImage, ) { event -> when (event) { ToolbarClickEvent.Navigate -> 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 6420fff3d7..3d34c607e5 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 @@ -17,6 +17,8 @@ package org.smartregister.fhircore.quest.ui.geowidget import android.content.Context +import android.graphics.Bitmap +import androidx.compose.runtime.mutableStateMapOf import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -29,6 +31,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonPrimitive import org.hl7.fhir.r4.model.Enumerations @@ -55,6 +58,7 @@ import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyn import org.smartregister.fhircore.geowidget.model.GeoJsonFeature import org.smartregister.fhircore.geowidget.model.Geometry import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler +import org.smartregister.fhircore.quest.util.extensions.referenceToBitmap @HiltViewModel class GeoWidgetLauncherViewModel @@ -80,6 +84,8 @@ constructor( val geoJsonFeatures: MutableStateFlow> = MutableStateFlow(emptyList()) + private val decodedImageMap = mutableStateMapOf() + fun retrieveLocations(geoWidgetConfig: GeoWidgetConfiguration, searchText: String?) { viewModelScope.launch { val totalCount = @@ -265,6 +271,10 @@ constructor( .read(SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name, null) .isNullOrEmpty() && applicationConfiguration.usePractitionerAssignedLocationOnSync + fun getImageBitmap(reference: String) = runBlocking { + reference.referenceToBitmap(defaultRepository.fhirEngine, decodedImageMap) + } + private companion object { const val KEY_LATITUDE = "positionLatitude" const val KEY_LONGITUDE = "positionLongitude" 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 81c1f8371b..c1c47cdbfd 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 @@ -24,6 +24,7 @@ import android.os.Bundle import android.provider.Settings import android.view.View import android.widget.Toast +import androidx.activity.OnBackPressedCallback import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -51,6 +52,8 @@ import org.smartregister.fhircore.engine.domain.model.LauncherType import org.smartregister.fhircore.engine.rulesengine.services.LocationCoordinate 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.base.BaseMultiLanguageActivity import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.extension.parcelable @@ -152,6 +155,7 @@ open class AppMainActivity : BaseMultiLanguageActivity(), QuestionnaireHandler, } setupLocationServices() + overrideOnBackPressListener() findViewById(R.id.mainScreenProgressBar).apply { visibility = View.GONE } findViewById(R.id.mainScreenProgressBarText).apply { visibility = View.GONE } @@ -300,4 +304,26 @@ open class AppMainActivity : BaseMultiLanguageActivity(), QuestionnaireHandler, } } } + + private fun overrideOnBackPressListener() { + onBackPressedDispatcher.addCallback( + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val navHostFragment = + (supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment) + if (navHostFragment.childFragmentManager.backStackEntryCount == 0) { + AlertDialogue.showAlert( + this@AppMainActivity, + alertIntent = AlertIntent.CONFIRM, + title = getString(R.string.exit_app), + message = getString(R.string.exit_app_message), + cancellable = false, + confirmButtonListener = { finish() }, + neutralButtonListener = { dialog -> dialog.dismiss() }, + ) + } else navHostFragment.navController.navigateUp() + } + }, + ) + } } 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 0a7fce88db..51a0b66342 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 @@ -52,7 +52,6 @@ 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.configuration.app.SyncStrategy -import org.smartregister.fhircore.engine.configuration.navigation.ICON_TYPE_REMOTE import org.smartregister.fhircore.engine.configuration.navigation.NavigationConfiguration import org.smartregister.fhircore.engine.configuration.navigation.NavigationMenuConfig import org.smartregister.fhircore.engine.configuration.report.measure.MeasureReportConfiguration @@ -86,7 +85,6 @@ import org.smartregister.fhircore.quest.ui.report.measure.worker.MeasureReportMo 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.handleClickEvent -import org.smartregister.fhircore.quest.util.extensions.resourceReferenceToBitMap import org.smartregister.fhircore.quest.util.extensions.schedulePeriodically @HiltViewModel @@ -133,23 +131,6 @@ constructor( configurationRegistry.retrieveConfigurations(ConfigType.MeasureReport) } - fun retrieveIconsAsBitmap() { - 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) { if (refreshAll) { appMainUiState.value = 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 2e2d6b3362..e9c82a8d02 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 @@ -17,6 +17,7 @@ package org.smartregister.fhircore.quest.ui.main.components import android.content.Context +import android.graphics.Bitmap import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -124,6 +125,7 @@ fun AppDrawer( appVersionPair: Pair? = null, unSyncedResourceCount: MutableIntState, onCountUnSyncedResources: () -> Unit, + decodeImage: ((String) -> Bitmap?)?, ) { val context = LocalContext.current val (versionCode, versionName) = remember { appVersionPair ?: context.appVersion() } @@ -154,6 +156,7 @@ fun AppDrawer( unSyncedResourceCount = unSyncedResourceCount, onSideMenuClick = onSideMenuClick, openDrawer = openDrawer, + decodeImage = decodeImage, ) }, ) { innerPadding -> @@ -180,14 +183,19 @@ fun AppDrawer( imageConfig = navigationMenu.menuIconConfig, title = navigationMenu.display, endText = appUiState.registerCountMap[navigationMenu.id]?.toString() ?: "", - showEndText = navigationMenu.showCount, endTextColor = MenuItemColor, - ) { - openDrawer(false) - onSideMenuClick( - AppMainEvent.TriggerWorkflow(navController = navController, navMenu = navigationMenu), - ) - } + showEndText = navigationMenu.showCount, + onSideMenuClick = { + openDrawer(false) + onSideMenuClick( + AppMainEvent.TriggerWorkflow( + navController = navController, + navMenu = navigationMenu, + ), + ) + }, + decodeImage = decodeImage, + ) } item { @@ -197,6 +205,7 @@ fun AppDrawer( onSideMenuClick = onSideMenuClick, openDrawer = openDrawer, navController = navController, + decodeImage = decodeImage, ) if (navigationConfiguration.staticMenu.isNotEmpty()) Divider(color = DividerColor) } @@ -210,14 +219,19 @@ fun AppDrawer( imageConfig = navigationMenu.menuIconConfig, title = navigationMenu.display, endText = appUiState.registerCountMap[navigationMenu.id]?.toString() ?: "", - showEndText = navigationMenu.showCount, endTextColor = MenuItemColor, - ) { - openDrawer(false) - onSideMenuClick( - AppMainEvent.TriggerWorkflow(navController = navController, navMenu = navigationMenu), - ) - } + showEndText = navigationMenu.showCount, + onSideMenuClick = { + openDrawer(false) + onSideMenuClick( + AppMainEvent.TriggerWorkflow( + navController = navController, + navMenu = navigationMenu, + ), + ) + }, + decodeImage = decodeImage, + ) } } } @@ -231,6 +245,7 @@ private fun NavBottomSection( unSyncedResourceCount: MutableIntState, onSideMenuClick: (AppMainEvent) -> Unit, openDrawer: (Boolean) -> Unit, + decodeImage: ((String) -> Bitmap?)?, ) { val currentSyncJobStatus = appDrawerUIState.currentSyncJobStatus val context = LocalContext.current @@ -285,6 +300,7 @@ private fun NavBottomSection( unSyncedResourceCount = unSyncedResourceCount, openDrawer = openDrawer, onSideMenuClick = onSideMenuClick, + decodeImage = decodeImage, ) } else { SyncStatusView( @@ -301,6 +317,7 @@ private fun NavBottomSection( unSyncedResourceCount = unSyncedResourceCount, openDrawer = openDrawer, onSideMenuClick = onSideMenuClick, + decodeImage = decodeImage, ) } } @@ -313,6 +330,7 @@ private fun DefaultSyncStatus( context: Context, unSyncedResourceCount: MutableIntState, openDrawer: (Boolean) -> Unit, + decodeImage: ((String) -> Bitmap?)?, onSideMenuClick: (AppMainEvent) -> Unit, ) { val allDataSynced = unSyncedResourceCount.intValue == 0 @@ -330,6 +348,7 @@ private fun DefaultSyncStatus( SideMenuItem( modifier = Modifier, imageConfig = ImageConfig(type = ICON_TYPE_LOCAL, reference = "ic_sync"), + mainTextColor = if (allDataSynced) Color.White else Color.Unspecified, title = stringResource( if (allDataSynced) { @@ -346,17 +365,18 @@ private fun DefaultSyncStatus( }, subTitleTextColor = SubtitleTextColor, endText = appUiState.lastSyncTime, + endTextColor = if (allDataSynced) SubtitleTextColor else Color.Unspecified, padding = 0, showEndText = true, - endTextColor = if (allDataSynced) SubtitleTextColor else Color.Unspecified, - mainTextColor = if (allDataSynced) Color.White else Color.Unspecified, mainTextBold = !allDataSynced, startIcon = if (allDataSynced) null else Icons.Default.Error, startIconColor = if (allDataSynced) null else WarningColor, - ) { - openDrawer(false) - onSideMenuClick(AppMainEvent.SyncData(context)) - } + onSideMenuClick = { + openDrawer(false) + onSideMenuClick(AppMainEvent.SyncData(context)) + }, + decodeImage = decodeImage, + ) } } @@ -364,6 +384,7 @@ private fun DefaultSyncStatus( private fun OtherPatientsItem( navigationConfiguration: NavigationConfiguration, onSideMenuClick: (AppMainEvent) -> Unit, + decodeImage: ((String) -> Bitmap?)?, openDrawer: (Boolean) -> Unit, navController: NavController, ) { @@ -375,24 +396,26 @@ private fun OtherPatientsItem( stringResource(org.smartregister.fhircore.engine.R.string.other_patients) }, endText = "", + endTextColor = SubtitleTextColor, showEndText = false, endImageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - endTextColor = SubtitleTextColor, - ) { - openDrawer(false) - onSideMenuClick( - AppMainEvent.OpenRegistersBottomSheet( - registersList = navigationConfiguration.bottomSheetRegisters?.registers, - navController = navController, - title = - if (navigationConfiguration.bottomSheetRegisters?.display.isNullOrEmpty()) { - context.getString(org.smartregister.fhircore.engine.R.string.other_patients) - } else { - navigationConfiguration.bottomSheetRegisters?.display - }, - ), - ) - } + onSideMenuClick = { + openDrawer(false) + onSideMenuClick( + AppMainEvent.OpenRegistersBottomSheet( + registersList = navigationConfiguration.bottomSheetRegisters?.registers, + navController = navController, + title = + if (navigationConfiguration.bottomSheetRegisters?.display.isNullOrEmpty()) { + context.getString(org.smartregister.fhircore.engine.R.string.other_patients) + } else { + navigationConfiguration.bottomSheetRegisters?.display + }, + ), + ) + }, + decodeImage = decodeImage, + ) } @Composable @@ -492,6 +515,7 @@ private fun SideMenuItem( startIcon: ImageVector? = null, startIconColor: Color? = null, onSideMenuClick: () -> Unit, + decodeImage: ((String) -> Bitmap?)?, ) { Row( horizontalArrangement = Arrangement.SpaceBetween, @@ -521,6 +545,7 @@ private fun SideMenuItem( imageProperties = ImageProperties(imageConfig = imageConfig, size = 32), tint = MenuItemColor, navController = rememberNavController(), + decodeImage = decodeImage, ) } Column { @@ -596,6 +621,7 @@ fun AppDrawerPreview() { appVersionPair = Pair(1, "0.0.1"), unSyncedResourceCount = remember { mutableIntStateOf(0) }, onCountUnSyncedResources = {}, + decodeImage = null, ) } } @@ -634,6 +660,7 @@ fun AppDrawerWithUnSyncedDataPreview() { appVersionPair = Pair(1, "0.0.1"), unSyncedResourceCount = remember { mutableIntStateOf(10) }, onCountUnSyncedResources = {}, + decodeImage = null, ) } } @@ -676,6 +703,7 @@ fun AppDrawerOnSyncCompletePreview() { appVersionPair = Pair(1, "0.0.1"), unSyncedResourceCount = remember { mutableIntStateOf(0) }, onCountUnSyncedResources = {}, + decodeImage = null, ) } } @@ -718,6 +746,7 @@ fun AppDrawerOnSyncFailedPreview() { appVersionPair = Pair(1, "0.0.1"), unSyncedResourceCount = remember { mutableIntStateOf(0) }, onCountUnSyncedResources = {}, + decodeImage = null, ) } } @@ -761,6 +790,7 @@ fun AppDrawerOnSyncRunningPreview() { appVersionPair = Pair(1, "0.0.1"), unSyncedResourceCount = remember { mutableIntStateOf(0) }, onCountUnSyncedResources = {}, + decodeImage = null, ) } } 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 acb6571cc7..c56728f026 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 @@ -16,6 +16,7 @@ package org.smartregister.fhircore.quest.ui.main.components +import android.graphics.Bitmap import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -40,17 +41,13 @@ import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.FilterAlt import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -113,6 +110,7 @@ fun TopScreenSection( performSearchOnValueChanged: Boolean = true, isFilterIconEnabled: Boolean = false, topScreenSection: TopScreenSectionConfig? = null, + decodeImage: ((String) -> Bitmap?)?, onClick: (ToolbarClickEvent) -> Unit = {}, ) { val currentContext = LocalContext.current @@ -140,7 +138,7 @@ fun TopScreenSection( Icon( when (toolBarHomeNavigation) { ToolBarHomeNavigation.OPEN_DRAWER -> Icons.Filled.Menu - ToolBarHomeNavigation.NAVIGATE_BACK -> Icons.Filled.ArrowBack + ToolBarHomeNavigation.NAVIGATE_BACK -> Icons.AutoMirrored.Filled.ArrowBack }, contentDescription = DRAWER_MENU, tint = Color.White, @@ -157,7 +155,13 @@ fun TopScreenSection( // if menu icons are more than two then we will add a overflow menu for other menu icons // to support m3 guidelines // https://m3.material.io/components/top-app-bar/guidelines#b1b64842-7d88-4c3f-8ffb-4183fe648c9e - SetupToolbarIcons(topScreenSection?.menuIcons, navController, modifier, onClick) + SetupToolbarIcons( + menuIcons = topScreenSection?.menuIcons, + navController = navController, + modifier = modifier, + onClick = onClick, + decodeImage = decodeImage, + ) if (isFilterIconEnabled) Spacer(modifier = Modifier.width(24.dp)) @@ -237,9 +241,7 @@ fun TopScreenSection( when { !searchQuery.isBlank() -> { IconButton( - onClick = { - onSearchTextChanged(SearchQuery.emptyText, performSearchOnValueChanged) - }, + onClick = { onSearchTextChanged(SearchQuery.emptyText, true) }, modifier = modifier.testTag(TRAILING_ICON_BUTTON_TEST_TAG), ) { Icon( @@ -289,18 +291,17 @@ fun SetupToolbarIcons( navController: NavController, modifier: Modifier, onClick: (ToolbarClickEvent) -> Unit, + decodeImage: ((String) -> Bitmap?)?, ) { if (menuIcons?.isNotEmpty() == true && menuIcons.size > 2) { - var menuExpanded by remember { mutableStateOf(false) } Row { RenderMenuIcons( menuIcons = menuIcons.take(2), navController = navController, modifier = modifier, onClick = onClick, + decodeImage = decodeImage, ) - // FIXME - Do not use material 3 library for now. We have to use dropdown menu to render the - // other menu icons } } else { menuIcons?.let { @@ -309,6 +310,7 @@ fun SetupToolbarIcons( navController = navController, modifier = modifier, onClick = onClick, + decodeImage = decodeImage, ) } } @@ -320,6 +322,7 @@ fun RenderMenuIcons( navController: NavController, modifier: Modifier, onClick: (ToolbarClickEvent) -> Unit, + decodeImage: ((String) -> Bitmap?)?, ) { LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { items(menuIcons) { @@ -331,6 +334,7 @@ fun RenderMenuIcons( modifier .clickable { onClick(ToolbarClickEvent.Actions(it.actions)) } .testTag(TOP_ROW_TOGGLE_ICON_TEST_tAG), + decodeImage = decodeImage, ) } } @@ -361,6 +365,7 @@ fun TopScreenSectionWithFilterItemOverNinetyNinePreview() { ), ), navController = rememberNavController(), + decodeImage = null, ) } @@ -377,6 +382,7 @@ fun TopScreenSectionWithFilterCountNinetyNinePreview() { onClick = {}, isSearchBarVisible = true, navController = rememberNavController(), + decodeImage = null, ) } @@ -401,6 +407,7 @@ fun TopScreenSectionNoFilterIconPreview() { ImageProperties(imageConfig = ImageConfig(reference = "ic_service_points")), ), ), + decodeImage = null, ) } @@ -426,6 +433,7 @@ fun TopScreenSectionWithFilterIconAndToggleIconPreview() { ImageProperties(imageConfig = ImageConfig(reference = "ic_service_points")), ), ), + decodeImage = null, ) } @@ -442,5 +450,6 @@ fun TopScreenSectionWithToggleIconPreview() { onClick = {}, isSearchBarVisible = true, navController = rememberNavController(), + decodeImage = null, ) } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileFragment.kt index faeabe4d2e..f627153a43 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileFragment.kt @@ -63,20 +63,17 @@ class ProfileFragment : Fragment() { with(profileFragmentArgs) { lifecycleScope.launch { profileViewModel.run { - decodeBinaryResourceIconsToBitmap(profileId) retrieveProfileUiState(profileId, resourceId, resourceConfig, params) } } } profileViewModel.refreshProfileDataLiveData.observe(viewLifecycleOwner) { - viewLifecycleOwner.lifecycleScope.launch { - if (it == true) { - with(profileFragmentArgs) { - profileViewModel.retrieveProfileUiState(profileId, resourceId, resourceConfig, params) - } - profileViewModel.refreshProfileDataLiveData.value = null + if (it == true) { + with(profileFragmentArgs) { + profileViewModel.retrieveProfileUiState(profileId, resourceId, resourceConfig, params) } + profileViewModel.refreshProfileDataLiveData.value = null } } @@ -87,9 +84,9 @@ class ProfileFragment : Fragment() { ProfileScreen( navController = findNavController(), profileUiState = profileViewModel.profileUiState.value, - onEvent = profileViewModel::onEvent, snackStateFlow = profileViewModel.snackBarStateFlow, - decodedImageMap = configurationRegistry.decodedImageMap, + onEvent = profileViewModel::onEvent, + decodeImage = { profileViewModel.getImageBitmap(it) }, ) } } 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 04a946015a..1bcc672edd 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 @@ -43,7 +43,7 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable @@ -89,8 +89,8 @@ fun ProfileScreen( navController: NavController, profileUiState: ProfileUiState, snackStateFlow: SharedFlow, - decodedImageMap: MutableMap = mutableMapOf(), onEvent: (ProfileEvent) -> Unit, + decodeImage: ((String) -> Bitmap?)?, ) { val scaffoldState = rememberScaffoldState() val lazyListState = rememberLazyListState() @@ -111,6 +111,7 @@ fun ProfileScreen( lazyListState = lazyListState, onEvent = onEvent, collapsible = false, + decodeImage = decodeImage, ) } else { CustomProfileTopAppBar( @@ -118,6 +119,7 @@ fun ProfileScreen( profileUiState = profileUiState, onEvent = onEvent, lazyListState = lazyListState, + decodeImage = decodeImage, ) } }, @@ -129,6 +131,7 @@ fun ProfileScreen( resourceData = profileUiState.resourceData, navController = navController, lazyListState = lazyListState, + decodeImage = decodeImage, ) } }, @@ -174,7 +177,7 @@ fun ProfileScreen( resourceData = profileUiState.resourceData ?: ResourceData("", ResourceType.Patient, emptyMap()), navController = navController, - decodedImageMap = profileUiState.decodedImageMap, + decodeImage = decodeImage, ) } } @@ -189,6 +192,7 @@ fun CustomProfileTopAppBar( profileUiState: ProfileUiState, onEvent: (ProfileEvent) -> Unit, lazyListState: LazyListState, + decodeImage: ((String) -> Bitmap?)?, ) { val topBarConfig = remember { profileUiState.profileConfiguration?.topAppBar ?: TopBarConfig() } @@ -205,6 +209,7 @@ fun CustomProfileTopAppBar( collapsible = topBarConfig.collapsible, onEvent = onEvent, lazyListState = lazyListState, + decodeImage = decodeImage, ) if (topBarConfig.collapsible) { AnimatedVisibility(visible = lazyListState.isScrollingDown()) { @@ -214,6 +219,7 @@ fun CustomProfileTopAppBar( profileUiState = profileUiState, navController = navController, titleContentPadding = 16, + decodeImage = decodeImage, ) } } else { @@ -223,6 +229,7 @@ fun CustomProfileTopAppBar( profileUiState = profileUiState, navController = navController, titleContentPadding = 0, + decodeImage = decodeImage, ) } } @@ -235,6 +242,7 @@ private fun RenderSimpleAppTopBar( profileUiState: ProfileUiState, navController: NavController, titleContentPadding: Int, + decodeImage: ((String) -> Bitmap?)?, ) { Column( modifier = @@ -249,7 +257,7 @@ private fun RenderSimpleAppTopBar( resourceData = profileUiState.resourceData ?: ResourceData("", ResourceType.Patient, emptyMap()), navController = navController, - decodedImageMap = profileUiState.decodedImageMap, + decodeImage = decodeImage, ) } } @@ -263,6 +271,7 @@ private fun SimpleTopAppBar( profileUiState: ProfileUiState, lazyListState: LazyListState, collapsible: Boolean, + decodeImage: ((String) -> Bitmap?)?, onEvent: (ProfileEvent) -> Unit, ) { TopAppBar( @@ -289,7 +298,7 @@ private fun SimpleTopAppBar( navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon( - Icons.Filled.ArrowBack, + Icons.AutoMirrored.Filled.ArrowBack, null, modifier = modifier.testTag(PROFILE_TOP_BAR_ICON_TEST_TAG), ) @@ -300,6 +309,7 @@ private fun SimpleTopAppBar( profileUiState = profileUiState, onEvent = onEvent, navController = navController, + decodeImage = decodeImage, ) }, elevation = elevation.dp, @@ -312,6 +322,7 @@ private fun ProfileTopAppBarMenuAction( onEvent: (ProfileEvent) -> Unit, navController: NavController, modifier: Modifier = Modifier, + decodeImage: ((String) -> Bitmap?)?, ) { if (!profileUiState.profileConfiguration?.overFlowMenuItems.isNullOrEmpty()) { var showOverflowMenu by remember { mutableStateOf(false) } @@ -363,6 +374,7 @@ private fun ProfileTopAppBarMenuAction( tint = contentColor, navController = navController, resourceData = profileUiState.resourceData!!, + decodeImage = decodeImage, ) if (overflowMenuItemConfig.icon != null) Spacer(modifier = Modifier.width(4.dp)) Text(text = overflowMenuItemConfig.title, color = contentColor) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileUiState.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileUiState.kt index bbd3ac27fe..624f8b8b15 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileUiState.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/ProfileUiState.kt @@ -16,9 +16,6 @@ package org.smartregister.fhircore.quest.ui.profile -import android.graphics.Bitmap -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.snapshots.SnapshotStateMap import org.smartregister.fhircore.engine.configuration.app.SnackBarThemeConfig import org.smartregister.fhircore.engine.configuration.profile.ProfileConfiguration import org.smartregister.fhircore.engine.domain.model.ResourceData @@ -28,5 +25,4 @@ data class ProfileUiState( val profileConfiguration: ProfileConfiguration? = null, val snackBarTheme: SnackBarThemeConfig = SnackBarThemeConfig(), val showDataLoadProgressIndicator: Boolean = true, - val decodedImageMap: SnapshotStateMap = mutableStateMapOf(), ) 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 1673f814d2..ceec5ca81d 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 @@ -16,6 +16,7 @@ package org.smartregister.fhircore.quest.ui.profile +import android.graphics.Bitmap import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.SnapshotStateList @@ -30,8 +31,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import org.hl7.fhir.r4.model.Binary import org.hl7.fhir.r4.model.Group import org.hl7.fhir.r4.model.ResourceType import org.smartregister.fhircore.engine.configuration.ConfigType @@ -55,9 +56,8 @@ 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.decodeImageResourcesToBitmap import org.smartregister.fhircore.quest.util.extensions.handleClickEvent -import org.smartregister.fhircore.quest.util.extensions.resourceReferenceToBitMap +import org.smartregister.fhircore.quest.util.extensions.referenceToBitmap import org.smartregister.fhircore.quest.util.extensions.toParamDataMap import timber.log.Timber @@ -84,73 +84,45 @@ constructor( private val listResourceDataStateMap = mutableStateMapOf>() - /** - * This function retrieves an image that was synced from the backend as a [Binary] resource, the - * content of the Binary resource is a base64 encoding of the actual image. The encoded imaged is - * then transformed into bitmap for use in an Image Composable (returns null if the referenced - * resource doesn't exist) - */ - suspend fun decodeBinaryResourceIconsToBitmap(profileId: String) { - val profileConfig = - configurationRegistry.retrieveConfiguration( - configId = profileId, - configType = ConfigType.Profile, - ) - 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, - ) - } - } + private val decodedImageMap = mutableStateMapOf() - suspend fun retrieveProfileUiState( + fun retrieveProfileUiState( profileId: String, resourceId: String, fhirResourceConfig: FhirResourceConfig? = null, paramsList: Array? = emptyArray(), ) { - if (resourceId.isNotEmpty()) { - val repositoryResourceData = - registerRepository.loadProfileData(profileId, resourceId, fhirResourceConfig, paramsList) - val paramsMap: Map = paramsList.toParamDataMap() - val profileConfigs = retrieveProfileConfiguration(profileId, paramsMap) - val resourceData = - resourceDataRulesExecutor - .processResourceData( - repositoryResourceData = repositoryResourceData, - ruleConfigs = profileConfigs.rules, - params = paramsMap, - ) - .copy(listResourceDataMap = listResourceDataStateMap) - - profileUiState.value = - ProfileUiState( - resourceData = resourceData, - profileConfiguration = profileConfigs, - snackBarTheme = applicationConfiguration.snackBarTheme, - showDataLoadProgressIndicator = false, - decodedImageMap = configurationRegistry.decodedImageMap, - ) + viewModelScope.launch { + if (resourceId.isNotEmpty()) { + val repositoryResourceData = + registerRepository.loadProfileData(profileId, resourceId, fhirResourceConfig, paramsList) + val paramsMap: Map = paramsList.toParamDataMap() + val profileConfigs = retrieveProfileConfiguration(profileId, paramsMap) + val resourceData = + resourceDataRulesExecutor + .processResourceData( + repositoryResourceData = repositoryResourceData, + ruleConfigs = profileConfigs.rules, + params = paramsMap, + ) + .copy(listResourceDataMap = listResourceDataStateMap) - profileConfigs.views.retrieveListProperties().forEach { listProperties -> - resourceDataRulesExecutor.processListResourceData( - listProperties = listProperties, - relatedResourcesMap = repositoryResourceData.relatedResourcesMap, - computedValuesMap = resourceData.computedValuesMap.plus(paramsMap), - listResourceDataStateMap = listResourceDataStateMap, - ) - } + profileUiState.value = + ProfileUiState( + resourceData = resourceData, + profileConfiguration = profileConfigs, + snackBarTheme = applicationConfiguration.snackBarTheme, + showDataLoadProgressIndicator = false, + ) - withContext(dispatcherProvider.io()) { - profileConfigs.views.decodeImageResourcesToBitmap( - fhirEngine = registerRepository.fhirEngine, - decodedImageMap = configurationRegistry.decodedImageMap, - ) + profileConfigs.views.retrieveListProperties().forEach { listProperties -> + resourceDataRulesExecutor.processListResourceData( + listProperties = listProperties, + relatedResourcesMap = repositoryResourceData.relatedResourcesMap, + computedValuesMap = resourceData.computedValuesMap.plus(paramsMap), + listResourceDataStateMap = listResourceDataStateMap, + ) + } } } } @@ -292,4 +264,8 @@ constructor( } } } + + fun getImageBitmap(reference: String) = runBlocking { + reference.referenceToBitmap(registerRepository.fhirEngine, decodedImageMap) + } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/components/MemberProfileBottomSheetView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/components/MemberProfileBottomSheetView.kt index e88d5162a6..bbd421e62c 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/components/MemberProfileBottomSheetView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/profile/components/MemberProfileBottomSheetView.kt @@ -18,6 +18,7 @@ package org.smartregister.fhircore.quest.ui.profile.components +import android.graphics.Bitmap import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -72,6 +73,7 @@ fun MemberProfileBottomSheetView( resourceData: ResourceData, navController: NavController = rememberNavController(), onViewProfile: () -> Unit, + decodeImage: ((String) -> Bitmap?)?, ) { Column { // Top section displays the name, gender and age for member @@ -115,6 +117,7 @@ fun MemberProfileBottomSheetView( buttonProperties = it, resourceData = resourceData, navController = navController, + decodeImage = decodeImage, ) } Spacer(modifier = modifier.height(8.dp)) @@ -147,6 +150,7 @@ private fun MemberProfileBottomSheetViewPreview() { navController = rememberNavController(), onViewProfile = { /*Do nothing*/}, resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), + decodeImage = null, ) } @@ -166,5 +170,6 @@ private fun MemberProfileBottomSheetViewWithFormDataPreview() { navController = rememberNavController(), onViewProfile = { /*Do nothing*/}, resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), + decodeImage = null, ) } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 97e6fd1886..6595e8bba4 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -284,6 +284,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { .showAsterisk(this.questionnaireConfig.showRequiredTextAsterisk) .showRequiredText(this.questionnaireConfig.showRequiredText) .setIsReadOnly(questionnaireConfig.isSummary()) + .setShowSubmitAnywayButton(questionnaireConfig.showSubmitAnywayButton.toBooleanStrict()) .apply { if (questionnaireResponse != null) { questionnaireResponse 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 03d1bb1acf..032a90cf1e 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 @@ -87,16 +87,16 @@ class RegisterFragment : Fragment(), OnSyncListener { container: ViewGroup?, savedInstanceState: Bundle?, ): View { - appMainViewModel.retrieveIconsAsBitmap() - with(registerFragmentArgs) { - lifecycleScope.launchWhenCreated { - registerViewModel.retrieveRegisterUiState( - registerId = registerId, - screenTitle = screenTitle, - params = params, - clearCache = false, - ) + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { + registerViewModel.retrieveRegisterUiState( + registerId = registerId, + screenTitle = screenTitle, + params = params, + clearCache = false, + ) + } } } return ComposeView(requireContext()).apply { @@ -150,6 +150,7 @@ class RegisterFragment : Fragment(), OnSyncListener { navController = findNavController(), unSyncedResourceCount = appMainViewModel.unSyncedResourcesCount, onCountUnSyncedResources = appMainViewModel::updateUnSyncedResourcesCount, + decodeImage = { registerViewModel.getImageBitmap(it) }, ) }, bottomBar = { @@ -180,6 +181,7 @@ class RegisterFragment : Fragment(), OnSyncListener { pagingItems = pagingItems, navController = findNavController(), toolBarHomeNavigation = registerFragmentArgs.toolBarHomeNavigation, + decodeImage = { registerViewModel.getImageBitmap(it) }, ) } } 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 78ecbd3b93..c08daccc6e 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 @@ -16,6 +16,7 @@ package org.smartregister.fhircore.quest.ui.register +import android.graphics.Bitmap import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -99,6 +100,7 @@ fun RegisterScreen( pagingItems: LazyPagingItems, navController: NavController, toolBarHomeNavigation: ToolBarHomeNavigation = ToolBarHomeNavigation.OPEN_DRAWER, + decodeImage: ((String) -> Bitmap?)?, ) { val lazyListState: LazyListState = rememberLazyListState() Scaffold( @@ -126,6 +128,7 @@ fun RegisterScreen( isFilterIconEnabled = filterActions?.isNotEmpty() ?: false, topScreenSection = registerUiState.registerConfiguration?.topScreenSection, navController = navController, + decodeImage = decodeImage, ) { event -> when (event) { ToolbarClickEvent.Navigate -> @@ -151,6 +154,7 @@ fun RegisterScreen( fabActions = fabActions, navController = navController, lazyListState = lazyListState, + decodeImage = decodeImage, ) } }, @@ -213,6 +217,7 @@ fun RegisterScreen( } } }, + decodeImage = decodeImage, ) } else { registerUiState.registerConfiguration?.noResults?.let { noResultConfig -> @@ -310,6 +315,7 @@ fun RegisterScreenWithDataPreview() { currentPage = currentPage, pagingItems = pagingItems, navController = rememberNavController(), + decodeImage = null, ) } } 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 1aa74b6290..21886825ce 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 @@ -16,9 +16,11 @@ package org.smartregister.fhircore.quest.ui.register +import android.graphics.Bitmap import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -38,6 +40,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Enumerations.DataType import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -62,6 +65,7 @@ import org.smartregister.fhircore.engine.util.SharedPreferencesHelper import org.smartregister.fhircore.engine.util.extension.encodeJson import org.smartregister.fhircore.quest.data.register.RegisterPagingSource import org.smartregister.fhircore.quest.data.register.model.RegisterPagingSourceState +import org.smartregister.fhircore.quest.util.extensions.referenceToBitmap import org.smartregister.fhircore.quest.util.extensions.toParamDataMap import timber.log.Timber @@ -93,6 +97,7 @@ constructor( val applicationConfiguration: ApplicationConfiguration by lazy { configurationRegistry.retrieveConfiguration(ConfigType.Application, paramsMap = emptyMap()) } + private val decodedImageMap = mutableStateMapOf() /** * This function paginates the register data. An optional [clearCache] resets the data in the @@ -514,4 +519,8 @@ constructor( suspend fun emitSnackBarState(snackBarMessageConfig: SnackBarMessageConfig) { _snackBarStateFlow.emit(snackBarMessageConfig) } + + fun getImageBitmap(reference: String) = runBlocking { + reference.referenceToBitmap(registerRepository.fhirEngine, decodedImageMap) + } } 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 194038b148..4121a7af8d 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 android.graphics.Bitmap import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -25,8 +26,6 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.Divider import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp @@ -66,6 +65,7 @@ fun RegisterCardList( currentPage: MutableState, showPagination: Boolean = false, onSearchByQrSingleResultAction: (ResourceData) -> Unit, + decodeImage: ((String) -> Bitmap?)?, ) { LazyColumn(modifier = Modifier.testTag(REGISTER_CARD_LIST_TEST_TAG), state = lazyListState) { items( @@ -81,7 +81,7 @@ fun RegisterCardList( viewProperties = registerCardConfig.views, resourceData = pagingItems[index]!!, navController = navController, - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = decodeImage, ) } Divider(color = DividerColor, thickness = 1.dp) 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 103b2795cd..a7f318414d 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 @@ -16,6 +16,7 @@ package org.smartregister.fhircore.quest.ui.shared.components +import android.graphics.Bitmap import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -70,6 +71,7 @@ fun ActionableButton( buttonProperties: ButtonProperties, resourceData: ResourceData, navController: NavController, + decodeImage: ((String) -> Bitmap?)?, ) { if (buttonProperties.visible.toBoolean()) { val status = buttonProperties.status @@ -162,6 +164,7 @@ fun ActionableButton( tint = iconTintColor, resourceData = resourceData, navController = navController, + decodeImage = decodeImage, ) } else { Icon( @@ -219,6 +222,7 @@ fun ActionableButtonPreview() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), + decodeImage = null, ) } @@ -235,6 +239,7 @@ fun ActionableButtonTinyButtonPreview() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), + decodeImage = null, ) } @@ -257,6 +262,7 @@ fun DisabledActionableButtonPreview() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), + decodeImage = null, ) } } @@ -276,6 +282,7 @@ fun SmallActionableButtonPreview() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), + decodeImage = null, ) ActionableButton( modifier = Modifier.weight(1.0f), @@ -288,6 +295,7 @@ fun SmallActionableButtonPreview() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), + decodeImage = null, ) } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/CardView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/CardView.kt index 50942c3266..9ae9070326 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/CardView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/CardView.kt @@ -27,9 +27,6 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -62,7 +59,7 @@ fun CardView( viewProperties: CardViewProperties, resourceData: ResourceData, navController: NavController, - decodedImageMap: SnapshotStateMap, + decodeImage: ((String) -> Bitmap?)?, ) { // Check if card is visible if (viewProperties.visible.toBoolean()) { @@ -114,7 +111,7 @@ fun CardView( viewProperties = viewProperties.content, resourceData = resourceData, navController = navController, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, ) } } @@ -153,7 +150,7 @@ private fun CardViewWithoutPaddingPreview() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } @@ -190,7 +187,7 @@ private fun CardViewWithPaddingPreview() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } @@ -213,7 +210,7 @@ private fun CardViewWithoutPaddingAndHeaderPreview() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } @@ -281,7 +278,7 @@ private fun CardViewImageWithItems() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ExtendedFab.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ExtendedFab.kt index baff4a9a17..269a56fc0e 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ExtendedFab.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ExtendedFab.kt @@ -16,6 +16,7 @@ package org.smartregister.fhircore.quest.ui.shared.components +import android.graphics.Bitmap import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -56,6 +57,7 @@ fun ExtendedFab( resourceData: ResourceData? = null, navController: NavController, lazyListState: LazyListState?, + decodeImage: ((String) -> Bitmap?)?, ) { val firstFabAction = remember { fabActions.first() } val firstFabEnabled = @@ -90,6 +92,7 @@ fun ExtendedFab( tint = if (firstFabEnabled) Color.White else DefaultColor, navController = navController, resourceData = resourceData, + decodeImage = decodeImage, ) } if (text.isNotEmpty()) { @@ -126,6 +129,7 @@ fun PreviewDisabledExtendedFab() { ), navController = rememberNavController(), lazyListState = rememberLazyListState(), + decodeImage = null, ) } @@ -143,6 +147,7 @@ fun PreviewExtendedFab() { ), navController = rememberNavController(), lazyListState = rememberLazyListState(), + decodeImage = null, ) } @@ -160,5 +165,6 @@ fun PreviewExtendedFabJustIcon() { ), navController = rememberNavController(), lazyListState = rememberLazyListState(), + decodeImage = null, ) } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/Image.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/Image.kt index 38e626aa8d..4e628518b3 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/Image.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/Image.kt @@ -58,7 +58,6 @@ import org.smartregister.fhircore.engine.domain.model.ViewType import org.smartregister.fhircore.engine.ui.theme.DangerColor import org.smartregister.fhircore.engine.util.annotation.PreviewWithBackgroundExcludeGenerated import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid -import org.smartregister.fhircore.engine.util.extension.interpolate import org.smartregister.fhircore.engine.util.extension.parseColor import org.smartregister.fhircore.engine.util.extension.retrieveResourceId import org.smartregister.fhircore.quest.ui.main.components.SIDE_MENU_ICON @@ -76,7 +75,7 @@ fun Image( imageProperties: ImageProperties = ImageProperties(viewType = ViewType.IMAGE, size = 24), navController: NavController, resourceData: ResourceData? = null, - decodedImageMap: MutableMap = mutableMapOf(), + decodeImage: ((String) -> Bitmap?)?, ) { val imageConfig = imageProperties.imageConfig val colorTint = tint ?: imageProperties.imageConfig?.color.parseColor() @@ -95,27 +94,25 @@ fun Image( ) ClickableImageIcon( imageProperties = imageProperties, - imageConfig = imageConfig, tint = colorTint, paddingEnd = paddingEnd, navController = navController, resourceData = resourceData, modifier = modifier, context = context, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, ) } } else { ClickableImageIcon( imageProperties = imageProperties, - imageConfig = imageConfig, tint = colorTint, paddingEnd = paddingEnd, navController = navController, resourceData = resourceData, modifier = modifier, context = context, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, ) } } @@ -125,13 +122,12 @@ fun Image( fun ClickableImageIcon( modifier: Modifier = Modifier, imageProperties: ImageProperties, - imageConfig: ImageConfig, tint: Color, paddingEnd: Int?, navController: NavController, resourceData: ResourceData? = null, context: Context? = null, - decodedImageMap: MutableMap = mutableMapOf(), + decodeImage: ((String) -> Bitmap?)?, ) { Box( contentAlignment = Alignment.Center, @@ -167,34 +163,33 @@ fun ClickableImageIcon( }, ), ) { - when (imageConfig.type) { - ICON_TYPE_LOCAL -> { - LocalContext.current.retrieveResourceId(imageConfig.reference)?.let { drawableId -> - Icon( - modifier = - Modifier.testTag(SIDE_MENU_ITEM_LOCAL_ICON_TEST_TAG) - .conditional(paddingEnd != null, { padding(end = paddingEnd?.dp!!) }) - .align(Alignment.Center) - .fillMaxSize(0.9f), - painter = painterResource(id = drawableId), - contentDescription = SIDE_MENU_ICON, - tint = tint, - ) + val imageConfig = + imageProperties.imageConfig?.interpolate( + resourceData?.computedValuesMap ?: emptyMap(), + ) + if (imageConfig != null) { + when (imageConfig.type) { + ICON_TYPE_LOCAL -> { + LocalContext.current.retrieveResourceId(imageConfig.reference)?.let { drawableId -> + Icon( + modifier = + Modifier.testTag(SIDE_MENU_ITEM_LOCAL_ICON_TEST_TAG) + .conditional(paddingEnd != null, { padding(end = paddingEnd?.dp!!) }) + .align(Alignment.Center) + .fillMaxSize(0.9f), + painter = painterResource(id = drawableId), + contentDescription = SIDE_MENU_ICON, + tint = tint, + ) + } } - } - ICON_TYPE_REMOTE -> - if (decodedImageMap.isNotEmpty()) { - val imageType = imageProperties.imageConfig?.imageType + ICON_TYPE_REMOTE -> { + val imageType = imageConfig.imageType val colorFilter = if (imageType == ImageType.SVG || imageType == ImageType.PNG) tint else null - val contentScale = - convertContentScaleTypeToContentScale(imageProperties.imageConfig!!.contentScale) + val contentScale = convertContentScaleTypeToContentScale(imageConfig.contentScale) val decodedImage = - decodedImageMap[ - imageConfig.reference - ?.interpolate(resourceData!!.computedValuesMap) - ?.extractLogicalIdUuid(), - ] + imageConfig.reference?.extractLogicalIdUuid()?.let { decodeImage?.invoke(it) } if (decodedImage != null) { Image( modifier = @@ -204,12 +199,13 @@ fun ClickableImageIcon( .fillMaxSize(0.9f), bitmap = decodedImage.asImageBitmap(), contentDescription = null, - alpha = imageProperties.imageConfig!!.alpha, + alpha = imageConfig.alpha, contentScale = contentScale, colorFilter = colorFilter?.let { ColorFilter.tint(it) }, ) } } + } } } } @@ -242,6 +238,7 @@ fun ImagePreview() { tint = DangerColor.copy(0.1f), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), + decodeImage = null, ) } @@ -262,5 +259,6 @@ fun ClickableImageWithTextPreview() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), + decodeImage = null, ) } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/List.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/List.kt index 8f8eec434c..3ba37ba761 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/List.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/List.kt @@ -35,8 +35,6 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.Divider import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity @@ -70,7 +68,7 @@ fun List( viewProperties: ListProperties, resourceData: ResourceData, navController: NavController, - decodedImageMap: SnapshotStateMap = mutableStateMapOf(), + decodeImage: ((String) -> Bitmap?)?, ) { val density = LocalDensity.current val currentListResourceData = resourceData.listResourceDataMap?.get(viewProperties.id) @@ -138,7 +136,7 @@ fun List( viewProperties = interpolatedChildViewProperties, resourceData = listResourceData, navController = navController, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, areViewPropertiesInterpolated = true, // Prevents double interpolation (in this function and inside the // ViewRenderer) which is a waste @@ -161,7 +159,7 @@ fun List( viewProperties = viewProperties.registerCard.views, resourceData = listResourceData, navController = navController, - decodedImageMap = mutableStateMapOf(), + decodeImage = decodeImage, ) } } @@ -231,6 +229,7 @@ private fun ListWithHorizontalOrientationPreview() { baseResourceType = ResourceType.Patient, computedValuesMap = emptyMap(), ), + decodeImage = null, ) } } @@ -282,6 +281,7 @@ private fun ListWithVerticalOrientationPreview() { baseResourceType = ResourceType.Patient, computedValuesMap = emptyMap(), ), + decodeImage = null, ) } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SearchBar.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SearchBar.kt index 4020c31808..54b085ec52 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SearchBar.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/SearchBar.kt @@ -30,7 +30,7 @@ import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -72,7 +72,7 @@ fun SearchBar( leadingIcon = { IconButton(onClick = onBackPress) { Icon( - Icons.Filled.ArrowBack, + Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, modifier = modifier.padding(16.dp), ) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ServiceCard.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ServiceCard.kt index 5e555fad0a..1bf739a715 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ServiceCard.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ServiceCard.kt @@ -16,6 +16,7 @@ package org.smartregister.fhircore.quest.ui.shared.components +import android.graphics.Bitmap import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -40,7 +41,6 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -83,6 +83,7 @@ fun ServiceCard( serviceCardProperties: ServiceCardProperties, resourceData: ResourceData, navController: NavController, + decodeImage: ((String) -> Bitmap?)?, ) { val serviceMemberIconsTint = serviceCardProperties.serviceMemberIconsTint.parseColor() if (serviceCardProperties.showVerticalDivider) { @@ -125,6 +126,7 @@ fun ServiceCard( serviceCardProperties = serviceCardProperties, navController = navController, resourceData = resourceData, + decodeImage = decodeImage, ) } } else { @@ -162,6 +164,7 @@ fun ServiceCard( serviceCardProperties = serviceCardProperties, navController = navController, resourceData = resourceData, + decodeImage = decodeImage, ) } } @@ -204,9 +207,7 @@ private fun RowScope.RenderDetails( horizontalArrangement = Arrangement.End, ) { memberIcons.forEach { - if ( - it.isNotEmpty() && ServiceMemberIcon.values().map { icon -> icon.name }.contains(it) - ) { + if (it.isNotEmpty() && ServiceMemberIcon.entries.map { icon -> icon.name }.contains(it)) { Icon( painter = painterResource(id = ServiceMemberIcon.valueOf(it).icon), contentDescription = null, @@ -244,6 +245,7 @@ private fun RowScope.RenderActionButtons( serviceCardProperties: ServiceCardProperties, navController: NavController, resourceData: ResourceData, + decodeImage: ((String) -> Bitmap?)?, ) { Box(modifier = Modifier.weight(weight).padding(start = 6.dp)) { if (serviceCardProperties.serviceButton != null || serviceCardProperties.services != null) { @@ -261,6 +263,7 @@ private fun RowScope.RenderActionButtons( buttonProperties = serviceCardProperties.serviceButton!!, resourceData = resourceData, navController = navController, + decodeImage = decodeImage, ) } } @@ -283,6 +286,7 @@ private fun RowScope.RenderActionButtons( buttonProperties = buttonProperties, resourceData = resourceData, navController = navController, + decodeImage = decodeImage, ) } } @@ -410,7 +414,7 @@ private fun ServiceCardServiceOverduePreview() { viewProperties = viewProperties, resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } @@ -466,7 +470,7 @@ private fun ServiceCardServiceOverdueWithBackgroundColorPreview() { viewProperties = viewProperties, resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } @@ -522,7 +526,7 @@ private fun ServiceCardServiceOverdueWithNoBackgroundColorAndStatusPreview() { viewProperties = viewProperties, resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } @@ -578,7 +582,7 @@ private fun ServiceCardServiceDuePreview() { viewProperties = viewProperties, resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } @@ -633,7 +637,7 @@ private fun ServiceCardServiceUpcomingPreview() { viewProperties = viewProperties, resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } @@ -669,7 +673,7 @@ private fun ServiceCardServiceFamilyMemberPreview() { viewProperties = viewProperties, resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } @@ -715,7 +719,7 @@ private fun ServiceCardServiceWithTinyServiceButtonPreview() { viewProperties = viewProperties, resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } @@ -769,7 +773,7 @@ private fun ServiceCardServiceCompletedPreview() { viewProperties = viewProperties, resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } @@ -817,7 +821,7 @@ private fun ServiceCardANCServiceDuePreview() { viewProperties = viewProperties, resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } @@ -875,7 +879,7 @@ private fun ServiceCardANCServiceOverduePreview() { viewProperties = viewProperties, resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/StackView.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/StackView.kt index 47c29bccf6..adb448e6a2 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/StackView.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/StackView.kt @@ -21,8 +21,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -44,15 +42,15 @@ fun StackView( stackViewProperties: StackViewProperties, resourceData: ResourceData, navController: NavController, - decodedImageMap: SnapshotStateMap = mutableStateMapOf(), + decodeImage: ((String) -> Bitmap?)?, ) { val backgroundColor = stackViewProperties.backgroundColor.parseColor() - val size = stackViewProperties.size Box( modifier = - Modifier.background(backgroundColor.copy(alpha = stackViewProperties.opacity)) - .size(size!!.dp) + modifier + .background(backgroundColor.copy(alpha = stackViewProperties.opacity)) + .size(stackViewProperties.size.dp) .testTag(STACK_VIEW_TEST_TAG), contentAlignment = castViewAlignment(stackViewProperties.alignment), ) { @@ -62,7 +60,7 @@ fun StackView( properties = child.interpolate(resourceData.computedValuesMap), resourceData = resourceData, navController = navController, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, ) } } @@ -101,5 +99,6 @@ private fun PreviewStack() { baseResourceType = ResourceType.Patient, computedValuesMap = emptyMap(), ), + decodeImage = null, ) } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewGenerator.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewGenerator.kt index 4ffcfc9bd9..4c3fc0d129 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewGenerator.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/shared/components/ViewGenerator.kt @@ -34,9 +34,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Divider import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -73,7 +71,7 @@ fun GenerateView( properties: ViewProperties, resourceData: ResourceData, navController: NavController, - decodedImageMap: SnapshotStateMap = mutableStateMapOf(), + decodeImage: ((String) -> Bitmap?)?, ) { if (properties.visible.toBoolean()) { when (properties.viewType) { @@ -90,6 +88,7 @@ fun GenerateView( buttonProperties = properties as ButtonProperties, resourceData = resourceData, navController = navController, + decodeImage = decodeImage, ) ViewType.COLUMN -> { val children = (properties as ColumnProperties).children @@ -112,7 +111,7 @@ fun GenerateView( properties = properties, resourceData = resourceData, navController = navController, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, ) } } @@ -151,7 +150,7 @@ fun GenerateView( properties = child.interpolate(resourceData.computedValuesMap), resourceData = resourceData, navController = navController, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, ) if (properties.showDivider.toBoolean() && index < children.lastIndex) { Divider( @@ -185,7 +184,7 @@ fun GenerateView( properties = properties.interpolate(resourceData.computedValuesMap), resourceData = resourceData, navController = navController, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, ) } } @@ -224,7 +223,7 @@ fun GenerateView( properties = child.interpolate(resourceData.computedValuesMap), resourceData = resourceData, navController = navController, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, ) } } @@ -236,6 +235,7 @@ fun GenerateView( serviceCardProperties = properties as ServiceCardProperties, resourceData = resourceData, navController = navController, + decodeImage = decodeImage, ) ViewType.CARD -> CardView( @@ -243,7 +243,7 @@ fun GenerateView( viewProperties = properties as CardViewProperties, resourceData = resourceData, navController = navController, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, ) ViewType.PERSONAL_DATA -> PersonalDataView( @@ -262,7 +262,7 @@ fun GenerateView( viewProperties = properties as ListProperties, resourceData = resourceData, navController = navController, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, ) ViewType.IMAGE -> Image( @@ -270,7 +270,7 @@ fun GenerateView( imageProperties = properties as ImageProperties, resourceData = resourceData, navController = navController, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, ) ViewType.STACK -> StackView( @@ -278,7 +278,7 @@ fun GenerateView( stackViewProperties = properties as StackViewProperties, resourceData = resourceData, navController = navController, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, ) } } 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 276d67c769..8c6fec4ca0 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 @@ -18,9 +18,6 @@ package org.smartregister.fhircore.quest.ui.shared.components import android.graphics.Bitmap import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.hl7.fhir.r4.model.ResourceType @@ -51,7 +48,7 @@ fun ViewRenderer( viewProperties: List, resourceData: ResourceData, navController: NavController, - decodedImageMap: SnapshotStateMap, + decodeImage: ((String) -> Bitmap?)?, areViewPropertiesInterpolated: Boolean = false, ) { viewProperties.forEach { properties -> @@ -66,7 +63,7 @@ fun ViewRenderer( properties = interpolatedProperties, resourceData = resourceData, navController = navController, - decodedImageMap = decodedImageMap, + decodeImage = decodeImage, ) } } @@ -101,7 +98,7 @@ private fun PreviewWeightedViewsInRow() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } @@ -157,7 +154,7 @@ private fun PreviewWrappedViewsInRow() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } @@ -195,7 +192,7 @@ private fun PreviewSameSizedViewInRow() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } @@ -314,6 +311,6 @@ private fun PreviewCardViewWithRows() { ), resourceData = ResourceData("id", ResourceType.Patient, emptyMap()), navController = rememberNavController(), - decodedImageMap = remember { mutableStateMapOf() }, + decodeImage = null, ) } 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 68c9075c46..3e2c6b58e2 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 @@ -261,16 +261,18 @@ fun Array?.toParamDataMap(): Map = ?.filter { it.paramType == ActionParameterType.PARAMDATA } ?.associate { it.key to it.value } ?: emptyMap() -suspend fun Sequence.resourceReferenceToBitMap( +suspend fun String.referenceToBitmap( fhirEngine: FhirEngine, decodedImageMap: SnapshotStateMap, -) { - forEach { - val resourceId = it.extractLogicalIdUuid() + forceRefresh: Boolean = false, +): Bitmap? { + val resourceId = this.extractLogicalIdUuid() + if (!decodedImageMap.containsKey(resourceId) || forceRefresh) { fhirEngine.loadResource(resourceId)?.let { binary -> binary.data.decodeToBitmap()?.let { bitmap -> decodedImageMap[resourceId] = bitmap } } } + return decodedImageMap[resourceId] } suspend fun List.decodeImageResourcesToBitmap( diff --git a/android/quest/src/main/res/values/strings.xml b/android/quest/src/main/res/values/strings.xml index adb04213d9..77345f7495 100644 --- a/android/quest/src/main/res/values/strings.xml +++ b/android/quest/src/main/res/values/strings.xml @@ -133,4 +133,6 @@ Place your camera over the entire QR Code to start scanning Failed to get GPS location \u0020\u002a + Exit App + Are you sure you want to exit from the app? diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainActivityTest.kt index da6fbea110..cde4637068 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainActivityTest.kt @@ -37,7 +37,6 @@ import io.mockk.slot import io.mockk.spyk import java.io.Serializable import java.time.OffsetDateTime -import kotlin.test.assertNotNull import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.QuestionnaireResponse import org.junit.Assert diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/profile/ProfileFragmentTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/profile/ProfileFragmentTest.kt index 207e7b8f6d..089ab4260e 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/profile/ProfileFragmentTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/profile/ProfileFragmentTest.kt @@ -27,6 +27,7 @@ import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.runs @@ -147,7 +148,7 @@ class ProfileFragmentTest : RobolectricTest() { questionnaireResponse = questionnaireResponse, ) - coEvery { profileViewModel.retrieveProfileUiState(any(), any(), any(), any()) } just runs + every { profileViewModel.retrieveProfileUiState(any(), any(), any(), any()) } just runs coEvery { profileViewModel.emitSnackBarState(any()) } just runs runBlocking { profileFragment.handleQuestionnaireSubmission(questionnaireSubmission) } 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 df1ca86bd5..df659b410d 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 @@ -682,13 +682,15 @@ class ConfigExtensionsKtTest : RobolectricTest() { val decodedImageMap = mutableStateMapOf() withContext(dispatcherProvider.io()) { defaultRepository.create(addResourceTags = true, binaryImage) - navigationMenuConfigs.resourceReferenceToBitMap( - fhirEngine = fhirEngine, - decodedImageMap = decodedImageMap, - ) + navigationMenuConfigs.forEach { + it.referenceToBitmap( + fhirEngine = fhirEngine, + decodedImageMap = decodedImageMap, + ) + } + Assert.assertTrue(decodedImageMap.isNotEmpty()) + Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) } - Assert.assertTrue(decodedImageMap.isNotEmpty()) - Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) } @Test @@ -697,10 +699,12 @@ class ConfigExtensionsKtTest : RobolectricTest() { val decodedImageMap = mutableStateMapOf() withContext(Dispatchers.IO) { defaultRepository.create(addResourceTags = true, binaryImage) - navigationMenuConfigs.resourceReferenceToBitMap( - fhirEngine = fhirEngine, - decodedImageMap = decodedImageMap, - ) + navigationMenuConfigs.forEach { + it.referenceToBitmap( + fhirEngine = fhirEngine, + decodedImageMap = decodedImageMap, + ) + } } Assert.assertTrue(decodedImageMap.isNotEmpty()) Assert.assertTrue(decodedImageMap.containsKey("d60ff460-7671-466a-93f4-c93a2ebf2077")) diff --git a/docs/engineering/app/automated-releases.mdx b/docs/engineering/app/automated-releases.mdx index 56d9c4b4ec..a86c3d1bb7 100644 --- a/docs/engineering/app/automated-releases.mdx +++ b/docs/engineering/app/automated-releases.mdx @@ -62,12 +62,15 @@ To add a flavor, add an entry to the `productFlavors` map in [`android/opensrp/b ``` create("newFlavor") { dimension = "apps" - applicationIdSuffix = ".new-flavor" - versionNameSuffix = "-new-flavor" + applicationIdSuffix = ".newFlavor" + versionNameSuffix = "-newFlavor" manifestPlaceholders["appLabel"] = "New Flavor App Name" } ``` - Note, flavor names should always be camel cased + +:::tip +Remember to use **[camelCase](https://en.wikipedia.org/wiki/Camel_case)** when adding the **flavor name** e.g `newFlavor`, **`applicationIdSuffix`** e.g `.newFlavor` and **`versionNameSuffix`** e.g `-newFlavor` +::: You can add the following resources: diff --git a/docs/engineering/app/configuring/add-application-flavors.mdx b/docs/engineering/app/configuring/add-application-flavors.mdx index 75264870b3..5a8ba0370d 100644 --- a/docs/engineering/app/configuring/add-application-flavors.mdx +++ b/docs/engineering/app/configuring/add-application-flavors.mdx @@ -35,14 +35,18 @@ productFlavors { The product flavors are easily modigied to the specified app flavor e.g. ``` -create("afyayangu") { +create("afyaYangu") { dimension = "apps" - applicationIdSuffix = ".afyayangu" - versionNameSuffix = "-afyayangu" + applicationIdSuffix = ".afyaYangu" + versionNameSuffix = "-afyaYangu" manifestPlaceholders["appLabel"] = "Afya Yangu" } ``` +:::tip +Remember to use **[camelCase](https://en.wikipedia.org/wiki/Camel_case)** when adding the **flavor name** e.g `newFlavor`, **`applicationIdSuffix`** e.g `.newFlavor` and **`versionNameSuffix`** e.g `-newFlavor` +::: + ### Config properties of productFlavors |Property | Description | Required | Default | |--|--|:--:|:--:|