From bc433941008c93f0c3b0b293c0a18863a6a3b96a Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Wed, 8 May 2024 14:23:03 +0300 Subject: [PATCH 1/7] Adress image pr feedback. Signed-off-by: Lentumunai-Mark --- .../navigation/NavigationMenuConfig.kt | 9 ++ .../engine/util/extension/StringExtensions.kt | 8 ++ .../quest/ui/main/AppMainViewModel.kt | 12 +- .../quest/ui/profile/ProfileViewModel.kt | 27 ++-- .../quest/ui/shared/components/Image.kt | 4 +- .../quest/util/extensions/ConfigExtensions.kt | 97 +++++++++++++ .../fhircore/quest/app/fakes/Faker.kt | 19 +++ .../util/extensions/ConfigExtensionsTest.kt | 134 +++++++++++++++++- 8 files changed, 288 insertions(+), 22 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt index 935e9fad56..10bcad509f 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt @@ -21,6 +21,7 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable +import org.smartregister.fhircore.engine.configuration.Configuration import org.smartregister.fhircore.engine.domain.model.ActionConfig import org.smartregister.fhircore.engine.util.extension.interpolate @@ -53,5 +54,13 @@ data class ImageConfig( } } +@Serializable +data class ImageConfiguration( + val id: String, + override val resourceType: String, + val contentType: String, + val data: String, +) : Configuration() + const val ICON_TYPE_LOCAL = "local" const val ICON_TYPE_REMOTE = "remote" diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt index a724877053..8f8fb85945 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt @@ -16,6 +16,9 @@ package org.smartregister.fhircore.engine.util.extension +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Base64 import java.text.MessageFormat import java.text.SimpleDateFormat import java.util.Date @@ -123,3 +126,8 @@ fun String.lastOffset() = this.uppercase() + "_" + SharedPreferenceKey.LAST_OFFS fun String.spaceByUppercase() = this.split(Regex("(?=\\p{Upper})")).joinToString(separator = " ").trim() + +fun String.base64toBitmap(offset: Int = 0): Bitmap { + val decodedBytes = Base64.decode(this, Base64.DEFAULT) + return BitmapFactory.decodeByteArray(decodedBytes, offset, decodedBytes.size) +} 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 9b782be9d2..799dbea78b 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 @@ -39,7 +39,6 @@ import javax.inject.Inject import kotlin.time.Duration import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.hl7.fhir.r4.model.Binary import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.ResourceType @@ -68,7 +67,6 @@ import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.SecureSharedPreference import org.smartregister.fhircore.engine.util.SharedPreferenceKey import org.smartregister.fhircore.engine.util.SharedPreferencesHelper -import org.smartregister.fhircore.engine.util.extension.decodeToBitmap import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.extension.fetchLanguages import org.smartregister.fhircore.engine.util.extension.getActivity @@ -82,6 +80,7 @@ import org.smartregister.fhircore.quest.navigation.NavigationArg import org.smartregister.fhircore.quest.ui.report.measure.worker.MeasureReportMonthPeriodWorker import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler import org.smartregister.fhircore.quest.ui.shared.models.QuestionnaireSubmission +import org.smartregister.fhircore.quest.util.extensions.decodeBinaryResourcesToBitmap import org.smartregister.fhircore.quest.util.extensions.handleClickEvent import org.smartregister.fhircore.quest.util.extensions.schedulePeriodically @@ -131,14 +130,7 @@ constructor( it.menuIconConfig?.type == ICON_TYPE_REMOTE && !it.menuIconConfig!!.reference.isNullOrEmpty() } - .forEach { - val resourceId = it.menuIconConfig!!.reference!!.extractLogicalIdUuid() - viewModelScope.launch(dispatcherProvider.io()) { - registerRepository.loadResource(resourceId)?.let { binary -> - it.menuIconConfig!!.decodedBitmap = binary.data.decodeToBitmap() - } - } - } + .decodeBinaryResourcesToBitmap(viewModelScope, registerRepository) } suspend fun retrieveAppMainUiState(refreshAll: Boolean = true) { 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 9ec4339bda..71fbb2e775 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 @@ -48,7 +48,6 @@ import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.SnackBarMessageConfig import org.smartregister.fhircore.engine.rulesengine.ResourceDataRulesExecutor import org.smartregister.fhircore.engine.util.DispatcherProvider -import org.smartregister.fhircore.engine.util.extension.decodeToBitmap import org.smartregister.fhircore.engine.util.extension.extractId import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.extension.getActivity @@ -56,7 +55,9 @@ import org.smartregister.fhircore.engine.util.fhirpath.FhirPathDataExtractor import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.ui.profile.bottomSheet.ProfileBottomSheetFragment import org.smartregister.fhircore.quest.ui.profile.model.EligibleManagingEntity +import org.smartregister.fhircore.quest.util.extensions.decodeBinaryResourcesToBitmap import org.smartregister.fhircore.quest.util.extensions.handleClickEvent +import org.smartregister.fhircore.quest.util.extensions.loadRemoteImagesBitmaps import org.smartregister.fhircore.quest.util.extensions.toParamDataMap import timber.log.Timber @@ -97,14 +98,7 @@ constructor( ) profileConfig.overFlowMenuItems .filter { it.icon != null && !it.icon!!.reference.isNullOrEmpty() } - .forEach { - val resourceId = it.icon!!.reference!!.extractLogicalIdUuid() - viewModelScope.launch(dispatcherProvider.io()) { - registerRepository.loadResource(resourceId)?.let { binary -> - it.icon!!.decodedBitmap = binary.data.decodeToBitmap() - } - } - } + .decodeBinaryResourcesToBitmap(viewModelScope, registerRepository) } suspend fun retrieveProfileUiState( @@ -142,6 +136,21 @@ constructor( computedValuesMap = resourceData.computedValuesMap.plus(paramsMap), listResourceDataStateMap = listResourceDataStateMap, ) + if ( + listResourceDataStateMap[listProperties.id] != null && + listResourceDataStateMap[listProperties.id]?.size!! > 0 + ) { + val computedMap = listResourceDataStateMap[listProperties.id]?.get(0)?.computedValuesMap + viewModelScope.launch { + if (computedMap != null) { + loadRemoteImagesBitmaps( + profileConfiguration.views, + registerRepository, + computedMap, + ) + } + } + } } } } 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 5641bee585..d92f54b966 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 @@ -183,8 +183,8 @@ fun ClickableImageIcon( .fillMaxSize(0.9f), bitmap = imageConfig.decodedBitmap!!.asImageBitmap(), contentDescription = null, - contentScale = ContentScale.Crop, - colorFilter = ColorFilter.tint(tint ?: imageProperties.imageConfig?.color.parseColor()), + alpha = 1.0f, + contentScale = ContentScale.Fit, ) } } 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 c2079d4ffa..9ac72bb843 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 @@ -26,19 +26,36 @@ import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.navigation.NavController import androidx.navigation.NavOptions +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.hl7.fhir.r4.model.Binary +import org.smartregister.fhircore.engine.configuration.navigation.ICON_TYPE_REMOTE +import org.smartregister.fhircore.engine.configuration.navigation.ImageConfiguration import org.smartregister.fhircore.engine.configuration.navigation.NavigationMenuConfig +import org.smartregister.fhircore.engine.configuration.view.CardViewProperties +import org.smartregister.fhircore.engine.configuration.view.ColumnProperties +import org.smartregister.fhircore.engine.configuration.view.ImageProperties +import org.smartregister.fhircore.engine.configuration.view.ListProperties +import org.smartregister.fhircore.engine.configuration.view.RowProperties +import org.smartregister.fhircore.engine.configuration.view.ViewProperties import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger import org.smartregister.fhircore.engine.configuration.workflow.ApplicationWorkflow +import org.smartregister.fhircore.engine.data.local.register.RegisterRepository import org.smartregister.fhircore.engine.domain.model.ActionConfig import org.smartregister.fhircore.engine.domain.model.ActionParameter import org.smartregister.fhircore.engine.domain.model.ActionParameterType +import org.smartregister.fhircore.engine.domain.model.OverflowMenuItemConfig import org.smartregister.fhircore.engine.domain.model.ResourceData +import org.smartregister.fhircore.engine.domain.model.ViewType +import org.smartregister.fhircore.engine.util.extension.base64toBitmap import org.smartregister.fhircore.engine.util.extension.decodeJson +import org.smartregister.fhircore.engine.util.extension.decodeToBitmap import org.smartregister.fhircore.engine.util.extension.encodeJson import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.extension.interpolate import org.smartregister.fhircore.engine.util.extension.isIn import org.smartregister.fhircore.engine.util.extension.showToast +import org.smartregister.fhircore.engine.util.extension.tryDecodeJson import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.navigation.MainNavigationScreen import org.smartregister.fhircore.quest.navigation.NavigationArg @@ -198,3 +215,83 @@ fun Array?.toParamDataMap(): Map = this?.asSequence() ?.filter { it.paramType == ActionParameterType.PARAMDATA } ?.associate { it.key to it.value } ?: emptyMap() + +fun List.decodeBinaryResourcesToBitmap( + coroutineScope: CoroutineScope, + registerRepository: RegisterRepository, +) { + this.forEach { + val resourceId = it.icon!!.reference!!.extractLogicalIdUuid() + coroutineScope.launch() { + registerRepository.loadResource(resourceId)?.let { binary -> + it.icon!!.decodedBitmap = binary.data.decodeToBitmap() + } + } + } +} + +fun Sequence.decodeBinaryResourcesToBitmap( + coroutineScope: CoroutineScope, + registerRepository: RegisterRepository, +) { + this.forEach { + val resourceId = it.menuIconConfig!!.reference!!.extractLogicalIdUuid() + coroutineScope.launch() { + registerRepository.loadResource(resourceId)?.let { binary -> + it.menuIconConfig!!.decodedBitmap = binary.data.decodeToBitmap() + } + } + } +} + +suspend fun loadRemoteImagesBitmaps( + views: List, + registerRepository: RegisterRepository, + computedValuesMap: Map, +) { + suspend fun loadIcons(view: ViewProperties) { + when (view.viewType) { + ViewType.IMAGE -> { + val imageProps = view as ImageProperties + if ( + !imageProps.imageConfig?.reference.isNullOrEmpty() && + imageProps.imageConfig?.type == ICON_TYPE_REMOTE + ) { + val resourceId = + imageProps.imageConfig!! + .reference!! + .interpolate(computedValuesMap) + .extractLogicalIdUuid() + registerRepository.loadResource(resourceId)?.let { binary -> + imageProps.imageConfig?.decodedBitmap = + binary.data + .decodeToString() + .tryDecodeJson() + ?.data + ?.base64toBitmap() + } + } + } + ViewType.ROW -> { + val container = view as RowProperties + container.children.forEach { childView -> loadIcons(childView) } + } + ViewType.COLUMN -> { + val container = view as ColumnProperties + container.children.forEach { childView -> loadIcons(childView) } + } + ViewType.CARD -> { + val card = view as CardViewProperties + card.content.forEach { contentView -> loadIcons(contentView) } + } + ViewType.LIST -> { + val list = view as ListProperties + list.registerCard.views.forEach { contentView -> loadIcons(contentView) } + } + else -> { + // Handle any other view types if needed + } + } + } + views.forEach { view -> loadIcons(view) } +} diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/app/fakes/Faker.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/app/fakes/Faker.kt index 994e88e0a0..cde9695e13 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/app/fakes/Faker.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/app/fakes/Faker.kt @@ -31,6 +31,7 @@ import java.util.Date import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import org.hl7.fhir.r4.model.Basic +import org.hl7.fhir.r4.model.Binary import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.DateType import org.hl7.fhir.r4.model.Enumerations @@ -58,6 +59,14 @@ object Faker { private const val APP_DEBUG = "app/debug" + val sampleImageJSONString = + "{\n" + + " \"id\": \"d60ff460-7671-466a-93f4-c93a2ebf2077\",\n" + + " \"resourceType\": \"Binary\",\n" + + " \"contentType\": \"image/jpeg\",\n" + + " \"data\": \"iVBORw0KGgoAAAANSUhEUgAAAFMAAABTCAYAAADjsjsAAAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAZ25vbWUtc2NyZWVuc2hvdO8Dvz4AAAAtdEVYdENyZWF0aW9uIFRpbWUARnJpIDE5IEFwciAyMDI0IDA3OjIxOjM4IEFNIEVBVIqENmYAAADTSURBVHic7dDBCcAgAMBAdf/p+nQZXSIglLsJQube3xkk1uuAPzEzZGbIzJCZITNDZobMDJkZMjNkZsjMkJkhM0NmhswMmRkyM2RmyMyQmSEzQ2aGzAyZGTIzZGbIzJCZITNDZobMDJkZMjNkZsjMkJkhM0NmhswMmRkyM2RmyMyQmSEzQ2aGzAyZGTIzZGbIzJCZITNDZobMDJkZMjNkZsjMkJkhM0NmhswMmRkyM2RmyMyQmSEzQ2aGzAyZGTIzZGbIzJCZITNDZobMDJkZMjN0AXiwBCviCqIRAAAAAElFTkSuQmCC\"\n" + + "}" + fun buildTestConfigurationRegistry(): ConfigurationRegistry { val fhirResourceService = mockk() val fhirResourceDataSource = spyk(FhirResourceDataSource(fhirResourceService)) @@ -150,4 +159,14 @@ object Faker { override fun deviceOnline() = true } + + fun buildBinaryResource( + id: String = "d60ff460-7671-466a-93f4-c93a2ebf2077", + ): Binary { + return Binary().apply { + this.id = id + this.contentType = "image/jpeg" + this.data = sampleImageJSONString.toByteArray() + } + } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsTest.kt index 0f9a6fb7bd..054c0b06c7 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsTest.kt @@ -26,27 +26,46 @@ import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.NavOptions import com.google.android.fhir.logicalId +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest import io.mockk.every import io.mockk.mockk import io.mockk.slot import io.mockk.verify +import javax.inject.Inject +import junit.framework.TestCase.assertNotNull import kotlin.test.assertEquals +import kotlinx.coroutines.runBlocking import org.hl7.fhir.r4.model.ContactPoint import org.hl7.fhir.r4.model.ResourceType import org.junit.Assert import org.junit.Before +import org.junit.Rule import org.junit.Test import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig +import org.smartregister.fhircore.engine.configuration.navigation.ICON_TYPE_REMOTE +import org.smartregister.fhircore.engine.configuration.navigation.ImageConfig import org.smartregister.fhircore.engine.configuration.navigation.NavigationMenuConfig +import org.smartregister.fhircore.engine.configuration.profile.ProfileConfiguration +import org.smartregister.fhircore.engine.configuration.register.RegisterCardConfig +import org.smartregister.fhircore.engine.configuration.view.CardViewProperties +import org.smartregister.fhircore.engine.configuration.view.ColumnProperties +import org.smartregister.fhircore.engine.configuration.view.ImageProperties +import org.smartregister.fhircore.engine.configuration.view.ListProperties +import org.smartregister.fhircore.engine.configuration.view.RowProperties import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger import org.smartregister.fhircore.engine.configuration.workflow.ApplicationWorkflow +import org.smartregister.fhircore.engine.data.local.DefaultRepository +import org.smartregister.fhircore.engine.data.local.register.RegisterRepository import org.smartregister.fhircore.engine.domain.model.ActionConfig import org.smartregister.fhircore.engine.domain.model.ActionParameter import org.smartregister.fhircore.engine.domain.model.ActionParameterType import org.smartregister.fhircore.engine.domain.model.FhirResourceConfig +import org.smartregister.fhircore.engine.domain.model.OverflowMenuItemConfig import org.smartregister.fhircore.engine.domain.model.ResourceConfig import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.ToolBarHomeNavigation +import org.smartregister.fhircore.engine.domain.model.ViewType import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.app.fakes.Faker @@ -55,13 +74,98 @@ import org.smartregister.fhircore.quest.navigation.NavigationArg import org.smartregister.fhircore.quest.robolectric.RobolectricTest import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler +@HiltAndroidTest class ConfigExtensionsTest : RobolectricTest() { + @get:Rule(order = 0) val hiltAndroidRule = HiltAndroidRule(this) + + @Inject lateinit var defaultRepository: DefaultRepository + + @Inject lateinit var registerRepository: RegisterRepository private val navController = mockk(relaxUnitFun = true) private val context = mockk(relaxUnitFun = true, relaxed = true) private val navigationMenuConfig by lazy { - NavigationMenuConfig(id = "id", display = "menu", visible = true) + NavigationMenuConfig( + id = "id", + display = "menu", + visible = true, + menuIconConfig = + ImageConfig( + type = ICON_TYPE_REMOTE, + reference = "d60ff460-7671-466a-93f4-c93a2ebf2077", + ), + ) + } + private val overflowMenuItemConfig by lazy { + OverflowMenuItemConfig( + visible = "true", + icon = + ImageConfig( + type = ICON_TYPE_REMOTE, + reference = "d60ff460-7671-466a-93f4-c93a2ebf2077", + ), + ) } + private val imageProperties = + ImageProperties( + imageConfig = + ImageConfig( + type = ICON_TYPE_REMOTE, + reference = "d60ff460-7671-466a-93f4-c93a2ebf2077", + ), + ) + + private val profileConfiguration = + ProfileConfiguration( + id = "1", + appId = "a", + fhirResource = + FhirResourceConfig( + baseResource = ResourceConfig(resource = ResourceType.Patient), + relatedResources = + listOf( + ResourceConfig( + resource = ResourceType.Encounter, + ), + ResourceConfig( + resource = ResourceType.Task, + ), + ), + ), + views = + listOf( + CardViewProperties( + viewType = ViewType.CARD, + content = + listOf( + ListProperties( + viewType = ViewType.LIST, + registerCard = + RegisterCardConfig( + views = + listOf( + ColumnProperties( + viewType = ViewType.COLUMN, + children = + listOf( + RowProperties( + viewType = ViewType.ROW, + children = + listOf( + imageProperties, + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ) + + private val binaryImage = Faker.buildBinaryResource() private val patient = Faker.buildPatient() private val resourceData by lazy { ResourceData( @@ -73,6 +177,7 @@ class ConfigExtensionsTest : RobolectricTest() { @Before fun setUp() { + hiltAndroidRule.inject() every { navController.context } returns context } @@ -545,4 +650,31 @@ class ConfigExtensionsTest : RobolectricTest() { listOf(clickAction).handleClickEvent(navController, resourceData, context = context) verify { context.showToast(text, Toast.LENGTH_LONG) } } + + @Test + fun decodeBinaryResourcesToBitmapOnNavigationMenuClientRegisters(): Unit = runBlocking { + defaultRepository.create(addResourceTags = true, binaryImage) + val navigationMenuConfigs = sequenceOf(navigationMenuConfig) + runBlocking { navigationMenuConfigs.decodeBinaryResourcesToBitmap(this, registerRepository) } + assertNotNull(navigationMenuConfig.menuIconConfig!!.decodedBitmap) + } + + @Test + fun decodeBinaryResourcesToBitmapOnNavigationMenuClientRegisterss(): Unit = runBlocking { + defaultRepository.create(addResourceTags = true, binaryImage) + val navigationMenuConfigs = listOf(overflowMenuItemConfig) + runBlocking { navigationMenuConfigs.decodeBinaryResourcesToBitmap(this, registerRepository) } + assertNotNull(navigationMenuConfig.menuIconConfig!!.decodedBitmap) + } + + @Test + fun testImageBitmapUpdatedCorrectly(): Unit = runBlocking { + defaultRepository.create(addResourceTags = true, binaryImage) + loadRemoteImagesBitmaps( + profileConfiguration.views, + computedValuesMap = emptyMap(), + registerRepository = registerRepository, + ) + assertNotNull(imageProperties.imageConfig?.decodedBitmap) + } } From 3ec80b4356c9d215b29c4894061e329ef23b65d1 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Wed, 8 May 2024 14:59:16 +0300 Subject: [PATCH 2/7] spotless check fix. Signed-off-by: Lentumunai-Mark --- .../smartregister/fhircore/quest/ui/shared/components/Image.kt | 1 - 1 file changed, 1 deletion(-) 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 d92f54b966..53380e7492 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 @@ -34,7 +34,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext From 66c7b13e55326c3dc1fdbfee60d5163a87c0727e Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Wed, 8 May 2024 15:51:51 +0300 Subject: [PATCH 3/7] Resolve PR feedback comments. Signed-off-by: Lentumunai-Mark --- .../quest/util/extensions/ConfigExtensions.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) 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 9ac72bb843..c6e3c22f94 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 @@ -249,10 +249,10 @@ suspend fun loadRemoteImagesBitmaps( registerRepository: RegisterRepository, computedValuesMap: Map, ) { - suspend fun loadIcons(view: ViewProperties) { - when (view.viewType) { + suspend fun ViewProperties.loadIcons() { + when (this.viewType) { ViewType.IMAGE -> { - val imageProps = view as ImageProperties + val imageProps = this as ImageProperties if ( !imageProps.imageConfig?.reference.isNullOrEmpty() && imageProps.imageConfig?.type == ICON_TYPE_REMOTE @@ -273,25 +273,25 @@ suspend fun loadRemoteImagesBitmaps( } } ViewType.ROW -> { - val container = view as RowProperties - container.children.forEach { childView -> loadIcons(childView) } + val container = this as RowProperties + container.children.forEach { it.loadIcons() } } ViewType.COLUMN -> { - val container = view as ColumnProperties - container.children.forEach { childView -> loadIcons(childView) } + val container = this as ColumnProperties + container.children.forEach { it.loadIcons() } } ViewType.CARD -> { - val card = view as CardViewProperties - card.content.forEach { contentView -> loadIcons(contentView) } + val card = this as CardViewProperties + card.content.forEach { it.loadIcons() } } ViewType.LIST -> { - val list = view as ListProperties - list.registerCard.views.forEach { contentView -> loadIcons(contentView) } + val list = this as ListProperties + list.registerCard.views.forEach { it.loadIcons() } } else -> { // Handle any other view types if needed } } } - views.forEach { view -> loadIcons(view) } + views.forEach { it.loadIcons() } } From b1ee2caf7739bfb5934069f83922d5a8a92f5b77 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Mon, 13 May 2024 09:41:04 +0300 Subject: [PATCH 4/7] Make some image properties like alpha, type to be configurable. Signed-off-by: Lentumunai-Mark --- .../navigation/NavigationMenuConfig.kt | 20 +++++++++++++- .../quest/ui/shared/components/Image.kt | 26 +++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt index 10bcad509f..d9f7223728 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt @@ -41,9 +41,12 @@ data class NavigationMenuConfig( @Serializable @Parcelize data class ImageConfig( - val type: String = ICON_TYPE_LOCAL, + var type: String = ICON_TYPE_LOCAL, val reference: String? = null, val color: String? = null, + val alpha: Float = 1.0f, + val imageType: ImageType = ImageType.SVG, + val contentScale: ContentScaleType = ContentScaleType.FIT, @Contextual var decodedBitmap: Bitmap? = null, ) : Parcelable, java.io.Serializable { fun interpolate(computedValuesMap: Map): ImageConfig { @@ -64,3 +67,18 @@ data class ImageConfiguration( const val ICON_TYPE_LOCAL = "local" const val ICON_TYPE_REMOTE = "remote" + +enum class ImageType { + JPEG, + PNG, + SVG, +} + +enum class ContentScaleType { + FIT, + CROP, + FILLHEIGHT, + INSIDE, + NONE, + FILLBOUNDS, +} 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 53380e7492..181cb2d335 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 @@ -34,6 +34,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext @@ -44,9 +45,11 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.hl7.fhir.r4.model.ResourceType +import org.smartregister.fhircore.engine.configuration.navigation.ContentScaleType import org.smartregister.fhircore.engine.configuration.navigation.ICON_TYPE_LOCAL import org.smartregister.fhircore.engine.configuration.navigation.ICON_TYPE_REMOTE import org.smartregister.fhircore.engine.configuration.navigation.ImageConfig +import org.smartregister.fhircore.engine.configuration.navigation.ImageType import org.smartregister.fhircore.engine.configuration.view.ImageProperties import org.smartregister.fhircore.engine.configuration.view.ImageShape import org.smartregister.fhircore.engine.domain.model.ResourceData @@ -174,6 +177,11 @@ fun ClickableImageIcon( } ICON_TYPE_REMOTE -> if (imageConfig.decodedBitmap != null) { + val imageType = imageProperties.imageConfig?.imageType + val colorFilter = + if (imageType == ImageType.SVG || imageType == ImageType.PNG) tint else null + val contentScale = + convertContentScaleTypeToContentScale(imageProperties.imageConfig!!.contentScale) Image( modifier = Modifier.testTag(SIDE_MENU_ITEM_REMOTE_ICON_TEST_TAG) @@ -182,14 +190,28 @@ fun ClickableImageIcon( .fillMaxSize(0.9f), bitmap = imageConfig.decodedBitmap!!.asImageBitmap(), contentDescription = null, - alpha = 1.0f, - contentScale = ContentScale.Fit, + alpha = imageProperties.imageConfig!!.alpha, + contentScale = contentScale, + colorFilter = colorFilter?.let { ColorFilter.tint(it) }, ) } } } } +fun convertContentScaleTypeToContentScale( + contentScale: ContentScaleType, +): ContentScale { + return when (contentScale) { + ContentScaleType.FIT -> ContentScale.Fit + ContentScaleType.CROP -> ContentScale.Crop + ContentScaleType.FILLHEIGHT -> ContentScale.Crop + ContentScaleType.INSIDE -> ContentScale.Inside + ContentScaleType.NONE -> ContentScale.None + ContentScaleType.FILLBOUNDS -> ContentScale.FillBounds + } +} + @PreviewWithBackgroundExcludeGenerated @Composable fun ImagePreview() { From ac59c84edddfec3faa9ea919738d45f9a92011d9 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Tue, 14 May 2024 11:51:06 +0300 Subject: [PATCH 5/7] Test out the recursive cases. Signed-off-by: Lentumunai-Mark --- .../util/extensions/ConfigExtensionsTest.kt | 68 ++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsTest.kt index 054c0b06c7..2a25810f0c 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensionsTest.kt @@ -652,15 +652,16 @@ class ConfigExtensionsTest : RobolectricTest() { } @Test - fun decodeBinaryResourcesToBitmapOnNavigationMenuClientRegisters(): Unit = runBlocking { - defaultRepository.create(addResourceTags = true, binaryImage) - val navigationMenuConfigs = sequenceOf(navigationMenuConfig) - runBlocking { navigationMenuConfigs.decodeBinaryResourcesToBitmap(this, registerRepository) } - assertNotNull(navigationMenuConfig.menuIconConfig!!.decodedBitmap) - } + fun decodeBinaryResourcesToBitmapOnNavigationMenuClientRegistersDoneCorrectly(): Unit = + runBlocking { + defaultRepository.create(addResourceTags = true, binaryImage) + val navigationMenuConfigs = sequenceOf(navigationMenuConfig) + runBlocking { navigationMenuConfigs.decodeBinaryResourcesToBitmap(this, registerRepository) } + assertNotNull(navigationMenuConfig.menuIconConfig!!.decodedBitmap) + } @Test - fun decodeBinaryResourcesToBitmapOnNavigationMenuClientRegisterss(): Unit = runBlocking { + fun decodeBinaryResourcesToBitmapOnOverflowMenuConfigDoneCorrectly(): Unit = runBlocking { defaultRepository.create(addResourceTags = true, binaryImage) val navigationMenuConfigs = listOf(overflowMenuItemConfig) runBlocking { navigationMenuConfigs.decodeBinaryResourcesToBitmap(this, registerRepository) } @@ -668,7 +669,7 @@ class ConfigExtensionsTest : RobolectricTest() { } @Test - fun testImageBitmapUpdatedCorrectly(): Unit = runBlocking { + fun testImageBitmapUpdatedCorrectlyGivenProfileConfiguration(): Unit = runBlocking { defaultRepository.create(addResourceTags = true, binaryImage) loadRemoteImagesBitmaps( profileConfiguration.views, @@ -677,4 +678,55 @@ class ConfigExtensionsTest : RobolectricTest() { ) assertNotNull(imageProperties.imageConfig?.decodedBitmap) } + + @Test + fun testImageBitmapUpdatedCorrectlyGivenCardViewProperties(): Unit = runBlocking { + val cardViewProperties = profileConfiguration.views[0] as CardViewProperties + defaultRepository.create(addResourceTags = true, binaryImage) + loadRemoteImagesBitmaps( + listOf(cardViewProperties), + computedValuesMap = emptyMap(), + registerRepository = registerRepository, + ) + assertNotNull(imageProperties.imageConfig?.decodedBitmap) + } + + @Test + fun testImageBitmapUpdatedCorrectlyGivenListViewProperties(): Unit = runBlocking { + val cardViewProperties = profileConfiguration.views[0] as CardViewProperties + defaultRepository.create(addResourceTags = true, binaryImage) + loadRemoteImagesBitmaps( + listOf(cardViewProperties.content[0]), + computedValuesMap = emptyMap(), + registerRepository = registerRepository, + ) + assertNotNull(imageProperties.imageConfig?.decodedBitmap) + } + + @Test + fun testImageBitmapUpdatedCorrectlyGivenColumnProperties(): Unit = runBlocking { + val cardViewProperties = profileConfiguration.views[0] as CardViewProperties + val listViewProperties = cardViewProperties.content[0] as ListProperties + defaultRepository.create(addResourceTags = true, binaryImage) + loadRemoteImagesBitmaps( + listOf(listViewProperties.registerCard.views[0]), + computedValuesMap = emptyMap(), + registerRepository = registerRepository, + ) + assertNotNull(imageProperties.imageConfig?.decodedBitmap) + } + + @Test + fun testImageBitmapUpdatedCorrectlyGivenRowProperties(): Unit = runBlocking { + val cardViewProperties = profileConfiguration.views[0] as CardViewProperties + val listViewProperties = cardViewProperties.content[0] as ListProperties + val columnProperties = listViewProperties.registerCard.views[0] as ColumnProperties + defaultRepository.create(addResourceTags = true, binaryImage) + loadRemoteImagesBitmaps( + listOf(columnProperties.children[0]), + computedValuesMap = emptyMap(), + registerRepository = registerRepository, + ) + assertNotNull(imageProperties.imageConfig?.decodedBitmap) + } } From d53f91f2a64eae4cd04e0835eeffc0bf1d44007b Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Thu, 16 May 2024 15:37:21 +0300 Subject: [PATCH 6/7] Decode image data only once. Signed-off-by: Lentumunai-Mark --- .../configuration/navigation/NavigationMenuConfig.kt | 8 -------- .../fhircore/engine/util/extension/StringExtensions.kt | 4 ---- .../fhircore/quest/ui/profile/ProfileViewModel.kt | 2 +- .../fhircore/quest/util/extensions/ConfigExtensions.kt | 9 +-------- 4 files changed, 2 insertions(+), 21 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt index d9f7223728..286471dd55 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt @@ -57,14 +57,6 @@ data class ImageConfig( } } -@Serializable -data class ImageConfiguration( - val id: String, - override val resourceType: String, - val contentType: String, - val data: String, -) : Configuration() - const val ICON_TYPE_LOCAL = "local" const val ICON_TYPE_REMOTE = "remote" diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt index 8f8fb85945..987fbeeddb 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt @@ -127,7 +127,3 @@ fun String.lastOffset() = this.uppercase() + "_" + SharedPreferenceKey.LAST_OFFS fun String.spaceByUppercase() = this.split(Regex("(?=\\p{Upper})")).joinToString(separator = " ").trim() -fun String.base64toBitmap(offset: Int = 0): Bitmap { - val decodedBytes = Base64.decode(this, Base64.DEFAULT) - return BitmapFactory.decodeByteArray(decodedBytes, offset, decodedBytes.size) -} 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 71fbb2e775..d5bd1d4f62 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 @@ -141,7 +141,7 @@ constructor( listResourceDataStateMap[listProperties.id]?.size!! > 0 ) { val computedMap = listResourceDataStateMap[listProperties.id]?.get(0)?.computedValuesMap - viewModelScope.launch { + viewModelScope.launch(dispatcherProvider.io()) { if (computedMap != null) { loadRemoteImagesBitmaps( profileConfiguration.views, 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 5e703cc6fd..dc693c7a81 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 @@ -30,7 +30,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.hl7.fhir.r4.model.Binary import org.smartregister.fhircore.engine.configuration.navigation.ICON_TYPE_REMOTE -import org.smartregister.fhircore.engine.configuration.navigation.ImageConfiguration import org.smartregister.fhircore.engine.configuration.navigation.NavigationMenuConfig import org.smartregister.fhircore.engine.configuration.view.CardViewProperties import org.smartregister.fhircore.engine.configuration.view.ColumnProperties @@ -47,7 +46,6 @@ import org.smartregister.fhircore.engine.domain.model.ActionParameterType import org.smartregister.fhircore.engine.domain.model.OverflowMenuItemConfig import org.smartregister.fhircore.engine.domain.model.ResourceData import org.smartregister.fhircore.engine.domain.model.ViewType -import org.smartregister.fhircore.engine.util.extension.base64toBitmap import org.smartregister.fhircore.engine.util.extension.decodeJson import org.smartregister.fhircore.engine.util.extension.decodeToBitmap import org.smartregister.fhircore.engine.util.extension.encodeJson @@ -271,12 +269,7 @@ suspend fun loadRemoteImagesBitmaps( .interpolate(computedValuesMap) .extractLogicalIdUuid() registerRepository.loadResource(resourceId)?.let { binary -> - imageProps.imageConfig?.decodedBitmap = - binary.data - .decodeToString() - .tryDecodeJson() - ?.data - ?.base64toBitmap() + imageProps.imageConfig?.decodedBitmap = binary.data.decodeToBitmap() } } } From 6f8e6fffcc868ab380e3a749b5294b38d72c4238 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Thu, 16 May 2024 16:25:39 +0300 Subject: [PATCH 7/7] Run spotless Apply. Signed-off-by: Lentumunai-Mark --- .../engine/configuration/navigation/NavigationMenuConfig.kt | 1 - .../fhircore/engine/util/extension/StringExtensions.kt | 4 ---- .../fhircore/quest/util/extensions/ConfigExtensions.kt | 1 - 3 files changed, 6 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt index 286471dd55..760f9f2700 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/navigation/NavigationMenuConfig.kt @@ -21,7 +21,6 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable -import org.smartregister.fhircore.engine.configuration.Configuration import org.smartregister.fhircore.engine.domain.model.ActionConfig import org.smartregister.fhircore.engine.util.extension.interpolate diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt index 987fbeeddb..a724877053 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/StringExtensions.kt @@ -16,9 +16,6 @@ package org.smartregister.fhircore.engine.util.extension -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.util.Base64 import java.text.MessageFormat import java.text.SimpleDateFormat import java.util.Date @@ -126,4 +123,3 @@ fun String.lastOffset() = this.uppercase() + "_" + SharedPreferenceKey.LAST_OFFS fun String.spaceByUppercase() = this.split(Regex("(?=\\p{Upper})")).joinToString(separator = " ").trim() - 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 dc693c7a81..9ecb03972a 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 @@ -53,7 +53,6 @@ import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.extension.interpolate import org.smartregister.fhircore.engine.util.extension.isIn import org.smartregister.fhircore.engine.util.extension.showToast -import org.smartregister.fhircore.engine.util.extension.tryDecodeJson import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.navigation.MainNavigationScreen import org.smartregister.fhircore.quest.navigation.NavigationArg