Skip to content

Commit

Permalink
Enhance loading of images stored in Binary resource
Browse files Browse the repository at this point in the history
* Adress image pr feedback.

Signed-off-by: Lentumunai-Mark <[email protected]>

* spotless check fix.

Signed-off-by: Lentumunai-Mark <[email protected]>

* Resolve PR  feedback comments.

Signed-off-by: Lentumunai-Mark <[email protected]>

* Make some image properties like alpha, type to be configurable.

Signed-off-by: Lentumunai-Mark <[email protected]>

* Test out the recursive cases.

Signed-off-by: Lentumunai-Mark <[email protected]>

* Decode image data only once.

Signed-off-by: Lentumunai-Mark <[email protected]>

* Run spotless Apply.

Signed-off-by: Lentumunai-Mark <[email protected]>

---------

Signed-off-by: Lentumunai-Mark <[email protected]>
Co-authored-by: Benjamin Mwalimu <[email protected]>
Co-authored-by: Elly Kitoto <[email protected]>
  • Loading branch information
3 people authored May 17, 2024
1 parent 69552a7 commit 3a2d8a6
Show file tree
Hide file tree
Showing 7 changed files with 355 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,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<String, Any>): ImageConfig {
Expand All @@ -55,3 +58,18 @@ data class ImageConfig(

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,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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<Binary>(resourceId)?.let { binary ->
it.menuIconConfig!!.decodedBitmap = binary.data.decodeToBitmap()
}
}
}
.decodeBinaryResourcesToBitmap(viewModelScope, registerRepository)
}

suspend fun retrieveAppMainUiState(refreshAll: Boolean = true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,16 @@ 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
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

Expand Down Expand Up @@ -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<Binary>(resourceId)?.let { binary ->
it.icon!!.decodedBitmap = binary.data.decodeToBitmap()
}
}
}
.decodeBinaryResourcesToBitmap(viewModelScope, registerRepository)
}

suspend fun retrieveProfileUiState(
Expand Down Expand Up @@ -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(dispatcherProvider.io()) {
if (computedMap != null) {
loadRemoteImagesBitmaps(
profileConfiguration.views,
registerRepository,
computedMap,
)
}
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,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
Expand Down Expand Up @@ -175,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)
Expand All @@ -183,14 +190,28 @@ fun ClickableImageIcon(
.fillMaxSize(0.9f),
bitmap = imageConfig.decodedBitmap!!.asImageBitmap(),
contentDescription = null,
contentScale = ContentScale.Crop,
colorFilter = ColorFilter.tint(tint ?: imageProperties.imageConfig?.color.parseColor()),
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,28 @@ 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.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.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
Expand Down Expand Up @@ -206,3 +220,78 @@ fun Array<ActionParameter>?.toParamDataMap(): Map<String, String> =
this?.asSequence()
?.filter { it.paramType == ActionParameterType.PARAMDATA }
?.associate { it.key to it.value } ?: emptyMap()

fun List<OverflowMenuItemConfig>.decodeBinaryResourcesToBitmap(
coroutineScope: CoroutineScope,
registerRepository: RegisterRepository,
) {
this.forEach {
val resourceId = it.icon!!.reference!!.extractLogicalIdUuid()
coroutineScope.launch() {
registerRepository.loadResource<Binary>(resourceId)?.let { binary ->
it.icon!!.decodedBitmap = binary.data.decodeToBitmap()
}
}
}
}

fun Sequence<NavigationMenuConfig>.decodeBinaryResourcesToBitmap(
coroutineScope: CoroutineScope,
registerRepository: RegisterRepository,
) {
this.forEach {
val resourceId = it.menuIconConfig!!.reference!!.extractLogicalIdUuid()
coroutineScope.launch() {
registerRepository.loadResource<Binary>(resourceId)?.let { binary ->
it.menuIconConfig!!.decodedBitmap = binary.data.decodeToBitmap()
}
}
}
}

suspend fun loadRemoteImagesBitmaps(
views: List<ViewProperties>,
registerRepository: RegisterRepository,
computedValuesMap: Map<String, Any>,
) {
suspend fun ViewProperties.loadIcons() {
when (this.viewType) {
ViewType.IMAGE -> {
val imageProps = this as ImageProperties
if (
!imageProps.imageConfig?.reference.isNullOrEmpty() &&
imageProps.imageConfig?.type == ICON_TYPE_REMOTE
) {
val resourceId =
imageProps.imageConfig!!
.reference!!
.interpolate(computedValuesMap)
.extractLogicalIdUuid()
registerRepository.loadResource<Binary>(resourceId)?.let { binary ->
imageProps.imageConfig?.decodedBitmap = binary.data.decodeToBitmap()
}
}
}
ViewType.ROW -> {
val container = this as RowProperties
container.children.forEach { it.loadIcons() }
}
ViewType.COLUMN -> {
val container = this as ColumnProperties
container.children.forEach { it.loadIcons() }
}
ViewType.CARD -> {
val card = this as CardViewProperties
card.content.forEach { it.loadIcons() }
}
ViewType.LIST -> {
val list = this as ListProperties
list.registerCard.views.forEach { it.loadIcons() }
}
else -> {
// Handle any other view types if needed
}
}
}
views.forEach { it.loadIcons() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<FhirResourceService>()
val fhirResourceDataSource = spyk(FhirResourceDataSource(fhirResourceService))
Expand Down Expand Up @@ -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()
}
}
}
Loading

0 comments on commit 3a2d8a6

Please sign in to comment.