diff --git a/android/buildSrc/src/main/kotlin/Deps.kt b/android/buildSrc/src/main/kotlin/Deps.kt index b7441d0dad..06bf0c8746 100644 --- a/android/buildSrc/src/main/kotlin/Deps.kt +++ b/android/buildSrc/src/main/kotlin/Deps.kt @@ -5,9 +5,9 @@ object Deps { object sdk_versions { - const val compile_sdk = 34 + const val compile_sdk = 35 const val min_sdk = 26 - const val target_sdk = 34 + const val target_sdk = 35 } const val build_tool_version = "30.0.3" diff --git a/android/dataclerk/src/main/java/org/dtree/fhircore/dataclerk/ui/main/AppDataStore.kt b/android/dataclerk/src/main/java/org/dtree/fhircore/dataclerk/ui/main/AppDataStore.kt index da95ef463b..a708cf8e75 100644 --- a/android/dataclerk/src/main/java/org/dtree/fhircore/dataclerk/ui/main/AppDataStore.kt +++ b/android/dataclerk/src/main/java/org/dtree/fhircore/dataclerk/ui/main/AppDataStore.kt @@ -212,7 +212,7 @@ data class AddressData( } } -internal fun Patient.toPatientItem(configuration: ApplicationConfiguration): PatientItem { +internal suspend fun Patient.toPatientItem(configuration: ApplicationConfiguration): PatientItem { val phone = if (hasTelecom()) telecom[0].value else "N/A" val isActive = active val gender = if (hasGenderElement()) genderElement.valueAsString else "" diff --git a/android/engine/build.gradle.kts b/android/engine/build.gradle.kts index bdca91c203..667aa002fa 100644 --- a/android/engine/build.gradle.kts +++ b/android/engine/build.gradle.kts @@ -112,7 +112,6 @@ dependencies { implementation("androidx.cardview:cardview:1.0.0") implementation("joda-time:joda-time:2.10.14") implementation("androidx.paging:paging-runtime-ktx:3.3.0") - implementation("com.github.bumptech.glide:glide:4.16.0") implementation("id.zelory:compressor:3.0.1") implementation(group = "javax.xml.stream", name = "stax-api", version = "1.0-2") diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt index 669fe4512b..dbae580a50 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt @@ -39,7 +39,10 @@ import java.util.Base64 import javax.inject.Inject import javax.inject.Singleton import javax.net.ssl.SSLHandshakeException +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.data.remote.auth.OAuthService import org.smartregister.fhircore.engine.data.remote.model.response.OAuthResponse @@ -264,6 +267,14 @@ constructor( fun sessionActive(): Boolean = isTokenActive(getAccessToken()) + suspend fun isSessionActive(): Boolean = + withContext(dispatcherProvider.io()) { + suspendCoroutine { + val active = sessionActive() + it.resume(active) + } + } + fun invalidateSession(onSessionInvalidated: () -> Unit) { findAccount()?.let { account -> accountManager.run { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/util/DataMapper.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/util/DataMapper.kt index 451891578e..fbf6a13235 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/util/DataMapper.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/util/DataMapper.kt @@ -22,5 +22,5 @@ package org.smartregister.fhircore.engine.domain.util */ interface DataMapper { - fun transformInputToOutputModel(inputModel: InputModel): OutputModel + suspend fun transformInputToOutputModel(inputModel: InputModel): OutputModel } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt index 195d733fed..36b970c577 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt @@ -21,6 +21,8 @@ import com.google.android.fhir.get import java.util.Date import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.CarePlan import org.hl7.fhir.r4.model.Encounter.EncounterStatus @@ -89,7 +91,9 @@ constructor(val fhirEngine: FhirEngine, val transformSupportServices: TransformS ) } - fhirPathEngine.evaluateToBoolean(input, null, subject, it.expression.expression) + withContext(Dispatchers.Default) { + fhirPathEngine.evaluateToBoolean(input, null, subject, it.expression.expression) + } } ) { val source = diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivity.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivity.kt index 5e2223f850..c3577bb041 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivity.kt @@ -33,7 +33,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.whenResumed +import androidx.lifecycle.withResumed import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import com.google.android.fhir.datacapture.QuestionnaireFragment @@ -43,7 +43,6 @@ import com.google.android.fhir.datacapture.validation.Invalid import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject -import kotlin.system.measureTimeMillis import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.Encounter @@ -60,11 +59,9 @@ import org.smartregister.fhircore.engine.ui.base.AlertDialogue.showProgressAlert import org.smartregister.fhircore.engine.ui.base.BaseMultiLanguageActivity import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider import org.smartregister.fhircore.engine.util.extension.FieldType -import org.smartregister.fhircore.engine.util.extension.decodeResourceFromString import org.smartregister.fhircore.engine.util.extension.distinctifyLinkId import org.smartregister.fhircore.engine.util.extension.encodeResourceToString import org.smartregister.fhircore.engine.util.extension.find -import org.smartregister.fhircore.engine.util.extension.generateMissingItems import org.smartregister.fhircore.engine.util.extension.showToast import timber.log.Timber @@ -131,7 +128,7 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList } updateViews() - fragment.whenResumed { loadProgress.dismiss() } + fragment.withResumed { loadProgress.dismiss() } } } @@ -169,23 +166,14 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList private suspend fun renderFragment() { tracer.startTrace(QUESTIONNAIRE_TRACE) val questionnaireString = parser.encodeResourceToString(questionnaire) - var questionnaireResponse: QuestionnaireResponse? + val questionnaireResponse: QuestionnaireResponse if (clientIdentifier != null) { setBarcode(questionnaire, clientIdentifier!!, true) questionnaireResponse = questionnaireViewModel.generateQuestionnaireResponse(questionnaire, intent) } else { questionnaireResponse = - intent - .getStringExtra(QUESTIONNAIRE_RESPONSE) - ?.decodeResourceFromString() - ?.apply { generateMissingItems(this@QuestionnaireActivity.questionnaire) } - if (questionnaireType.isReadOnly()) { - requireNotNull(questionnaireResponse) - } else { - questionnaireResponse = - questionnaireViewModel.generateQuestionnaireResponse(questionnaire, intent) - } + questionnaireViewModel.generateQuestionnaireResponse(questionnaire, intent) } val questionnaireFragmentBuilder = @@ -294,9 +282,6 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList questionnaireConfig = resultPair.first questionnaire = resultPair.second - - val t = measureTimeMillis { populateInitialValues(questionnaire) } - Timber.d("populateInitialValues took $t ms : cachedxxx") } .onFailure { Timber.e(it) @@ -408,8 +393,6 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList ) } - open fun populateInitialValues(questionnaire: Questionnaire) = Unit - open fun postSaveSuccessful( questionnaireResponse: QuestionnaireResponse, extras: List? = null, @@ -525,7 +508,6 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList groupIdentifier: String? = null, formName: String, questionnaireType: QuestionnaireType = QuestionnaireType.DEFAULT, - questionnaireResponse: QuestionnaireResponse? = null, backReference: String? = null, launchContexts: Map = emptyMap(), populationResources: ArrayList = ArrayList(), @@ -538,9 +520,6 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList Pair(QUESTIONNAIRE_BACK_REFERENCE_KEY, backReference), ) .apply { - questionnaireResponse?.let { - putString(QUESTIONNAIRE_RESPONSE, it.encodeResourceToString()) - } val resourcesList = populationResources.map { it.encodeResourceToString() } if (resourcesList.isNotEmpty()) { putStringArrayList( diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModel.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModel.kt index 979343bba8..08aa69467b 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModel.kt @@ -31,6 +31,7 @@ import com.google.android.fhir.datacapture.mapping.ResourceMapper import com.google.android.fhir.datacapture.mapping.StructureMapExtractionContext import com.google.android.fhir.search.Operation import com.google.android.fhir.search.Order +import com.google.android.fhir.search.revInclude import com.google.android.fhir.search.search import dagger.hilt.android.lifecycle.HiltViewModel import java.time.LocalDate @@ -83,7 +84,6 @@ import org.smartregister.fhircore.engine.util.extension.assertSubject import org.smartregister.fhircore.engine.util.extension.deleteRelatedResources import org.smartregister.fhircore.engine.util.extension.extractId import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid -import org.smartregister.fhircore.engine.util.extension.filterByResourceTypeId import org.smartregister.fhircore.engine.util.extension.find import org.smartregister.fhircore.engine.util.extension.isExtractionCandidate import org.smartregister.fhircore.engine.util.extension.isIn @@ -426,18 +426,22 @@ constructor( suspend fun carePlanAndPatientMetaExtraction(source: Resource) { try { - /** Get a FHIR [Resource] in the local storage. */ - var resource = fhirEngine.get(source.resourceType, source.id) - /** Increment [Resource.meta] versionId of [source]. */ - resource.meta.versionId?.toInt()?.plus(1)?.let { - /** Append passed [Resource.meta] to the [source]. */ - resource.addTags(source.meta.tag) - /** Assign [Resource.meta] versionId of [source]. */ - resource = resource.copy().apply { meta.versionId = "$it" } - /** Delete a FHIR [source] in the local storage. */ - fhirEngine.delete(resource.resourceType, resource.id) - /** Recreate a FHIR [source] in the local storage. */ - fhirEngine.create(resource) + withContext(dispatcherProvider.io()) { + /** Get a FHIR [Resource] in the local storage. */ + var resource = fhirEngine.get(source.resourceType, source.id) + /** Increment [Resource.meta] versionId of [source]. */ + resource.meta.versionId?.toInt()?.plus(1)?.let { + /** Append passed [Resource.meta] to the [source]. */ + resource.addTags(source.meta.tag) + /** Assign [Resource.meta] versionId of [source]. */ + resource = resource.copy().apply { meta.versionId = "$it" } + fhirEngine.withTransaction { + /** Delete a FHIR [source] in the local storage. */ + fhirEngine.delete(resource.resourceType, resource.id) + /** Recreate a FHIR [source] in the local storage. */ + fhirEngine.create(resource) + } + } } } catch (e: Exception) { Timber.e(e) @@ -546,112 +550,73 @@ constructor( } private suspend fun loadScheduledAppointments(patientId: String): Iterable { - return fhirEngine - .search { - filter( - Appointment.STATUS, - { value = of(Appointment.AppointmentStatus.BOOKED.toCode()) }, - { value = of(Appointment.AppointmentStatus.WAITLIST.toCode()) }, - operation = Operation.OR, - ) - } - .map { it.resource } - // filter on patient subject - .filter { appointment -> - appointment.participant.any { - it.hasActor() && - it.actor.referenceElement.resourceType == ResourceType.Patient.name && - it.actor.referenceElement.idPart == patientId - } - } - .filter { - (it.status == Appointment.AppointmentStatus.BOOKED || - it.status == Appointment.AppointmentStatus.WAITLIST) && - it.hasStart() && - it.start.after( - Date.from( - LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant().minusSeconds(30), - ), - ) - } - } - - private suspend fun getLastActiveCarePlan(patientId: String): CarePlan? { - val carePlans = + return withContext(dispatcherProvider.io()) { fhirEngine - .search { - filterByResourceTypeId(CarePlan.SUBJECT, ResourceType.Patient, patientId) + .search { filter( - CarePlan.STATUS, - { value = of(CarePlan.CarePlanStatus.COMPLETED.toCoding()) }, + Appointment.STATUS, + { value = of(Appointment.AppointmentStatus.BOOKED.toCode()) }, + { value = of(Appointment.AppointmentStatus.WAITLIST.toCode()) }, operation = Operation.OR, ) } .map { it.resource } - return carePlans.sortedByDescending { it.meta.lastUpdated }.firstOrNull() + // filter on patient subject + .filter { appointment -> + appointment.participant.any { + it.hasActor() && + it.actor.referenceElement.resourceType == ResourceType.Patient.name && + it.actor.referenceElement.idPart == patientId + } + } + .filter { + (it.status == Appointment.AppointmentStatus.BOOKED || + it.status == Appointment.AppointmentStatus.WAITLIST) && + it.hasStart() && + it.start.after( + Date.from( + LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant().minusSeconds(30), + ), + ) + } + } } private suspend fun getActiveListResource(patient: String): ListResource? { val list = - fhirEngine - .search { - filter(ListResource.SUBJECT, { value = "Patient/$patient" }) - filter(ListResource.STATUS, { value = of(ListResource.ListStatus.CURRENT.toCode()) }) - sort(ListResource.TITLE, Order.DESCENDING) - count = 1 - from = 0 - } - .map { it.resource } + withContext(dispatcherProvider.io()) { + fhirEngine + .search { + filter(ListResource.SUBJECT, { value = "Patient/$patient" }) + filter(ListResource.STATUS, { value = of(ListResource.ListStatus.CURRENT.toCode()) }) + sort(ListResource.TITLE, Order.DESCENDING) + count = 1 + from = 0 + } + .map { it.resource } + } return list.firstOrNull() } suspend fun loadLatestAppointmentWithNoStartDate(patientId: String): Appointment? { - return fhirEngine - .search { - filter(Appointment.STATUS, { value = of(Appointment.AppointmentStatus.BOOKED.toCode()) }) - } - .map { it.resource } - // filter on patient subject - .filter { appointment -> - appointment.participant.any { - it.hasActor() && - it.actor.referenceElement.resourceType == ResourceType.Patient.name && - it.actor.referenceElement.idPart == patientId - } - } - .filterNot { it.hasStart() && it.status == Appointment.AppointmentStatus.BOOKED } - .sortedBy { it.created } - .firstOrNull() - } - - suspend fun loadTracing(patientId: String): List { - val tasks = + return withContext(dispatcherProvider.io()) { fhirEngine - .search { - filter(Task.SUBJECT, { value = "Patient/$patientId" }) - filter( - TokenClientParam("code"), - { - value = - of(CodeableConcept().addCoding(Coding("http://snomed.info/sct", "225368008", null))) - }, - ) - filter( - Task.STATUS, - { value = of(Task.TaskStatus.READY.toCode()) }, - { value = of(Task.TaskStatus.INPROGRESS.toCode()) }, - operation = Operation.OR, - ) - filter( - Task.PERIOD, - { - value = of(DateTimeType.now()) - prefix = ParamPrefixEnum.GREATERTHAN - }, - ) + .search { + filter(Appointment.STATUS, { value = of(Appointment.AppointmentStatus.BOOKED.toCode()) }) } .map { it.resource } - return tasks.filter { it.status in arrayOf(TaskStatus.READY, TaskStatus.INPROGRESS) } + // filter on patient subject + .filter { appointment -> + appointment.participant.any { + it.hasActor() && + it.actor.referenceElement.resourceType == ResourceType.Patient.name && + it.actor.referenceElement.idPart == patientId + } + } + .filterNot { it.hasStart() && it.status == Appointment.AppointmentStatus.BOOKED } + .sortedBy { it.created } + .firstOrNull() + } } fun saveResource(resource: Resource) { @@ -687,52 +652,113 @@ constructor( val resourcesList = getPopulationResourcesFromIntent(intent).toMutableList() intent.getStringExtra(QuestionnaireActivity.QUESTIONNAIRE_ARG_PATIENT_KEY)?.let { patientId -> - fhirEngine.withTransaction { - loadPatient(patientId)?.apply { resourcesList.add(this) } - ?: defaultRepository.loadResource(patientId)?.apply { resourcesList.add(this) } - - val bundleIndex = resourcesList.indexOfFirst { x -> x is Bundle } - if (bundleIndex != -1) { - val currentBundle = resourcesList[bundleIndex] as Bundle - - if (TracingHelpers.requireTracingTasks(questionnaireConfig.identifier)) { - val bundle = Bundle() - bundle.id = TracingHelpers.tracingBundleId - val tasks = loadTracing(patientId) - tasks.forEach { bundle.addEntry(Bundle.BundleEntryComponent().setResource(it)) } - - val list = getActiveListResource(patientId) - if (list != null) { - bundle.addEntry(Bundle.BundleEntryComponent().setResource(list)) + withContext(dispatcherProvider.io()) { + fhirEngine.withTransaction { + loadPatient(patientId)?.apply { resourcesList.add(this) } + ?: defaultRepository.loadResource(patientId)?.apply { resourcesList.add(this) } + + val bundleIndex = resourcesList.indexOfFirst { x -> x is Bundle } + if (bundleIndex != -1) { + val currentBundle = resourcesList[bundleIndex] as Bundle + + val patientSearchResults = + fhirEngine + .search { + filter(Patient.RES_ID, { value = of(patientId) }) + count = 1 + + if (TracingHelpers.requireTracingTasks(questionnaireConfig.identifier)) { + revInclude(Task.SUBJECT) { + filter( + TokenClientParam("code"), + { + value = + of( + CodeableConcept() + .addCoding( + Coding( + "http://snomed.info/sct", + "225368008", + null, + ), + ), + ) + }, + ) + filter( + Task.STATUS, + { value = of(Task.TaskStatus.READY.toCode()) }, + { value = of(Task.TaskStatus.INPROGRESS.toCode()) }, + operation = Operation.OR, + ) + filter( + Task.PERIOD, + { + value = of(DateTimeType.now()) + prefix = ParamPrefixEnum.GREATERTHAN + }, + ) + } + } + + revInclude(CarePlan.SUBJECT) { + filter( + CarePlan.STATUS, + { value = of(CarePlan.CarePlanStatus.COMPLETED.toCoding()) }, + operation = Operation.OR, + ) + } + } + .firstOrNull() + + if (TracingHelpers.requireTracingTasks(questionnaireConfig.identifier)) { + val bundle = Bundle() + bundle.id = TracingHelpers.tracingBundleId + val tasks = + patientSearchResults?.revIncluded?.get(ResourceType.Task to Task.SUBJECT.paramName) + ?: emptyList() + tasks.forEach { bundle.addEntry(Bundle.BundleEntryComponent().setResource(it)) } + + val list = getActiveListResource(patientId) + if (list != null) { + bundle.addEntry(Bundle.BundleEntryComponent().setResource(list)) + } + + currentBundle.addEntry( + Bundle.BundleEntryComponent().setResource(bundle).apply { + id = TracingHelpers.tracingBundleId + }, + ) } - currentBundle.addEntry( - Bundle.BundleEntryComponent().setResource(bundle).apply { - id = TracingHelpers.tracingBundleId - }, - ) - } + val appointmentToPopulate = loadLatestAppointmentWithNoStartDate(patientId) + if (appointmentToPopulate != null) { + currentBundle.addEntry( + Bundle.BundleEntryComponent().setResource(appointmentToPopulate), + ) + } + // Add appointments that may need to be closed + loadScheduledAppointments(patientId).forEach { + currentBundle.addEntry(Bundle.BundleEntryComponent().setResource(it)) + } - val appointmentToPopulate = loadLatestAppointmentWithNoStartDate(patientId) - if (appointmentToPopulate != null) { - currentBundle.addEntry(Bundle.BundleEntryComponent().setResource(appointmentToPopulate)) - } - // Add appointments that may need to be closed - loadScheduledAppointments(patientId).forEach { - currentBundle.addEntry(Bundle.BundleEntryComponent().setResource(it)) - } + val lastCarePlan = + patientSearchResults + ?.revIncluded + ?.get(ResourceType.CarePlan to CarePlan.SUBJECT.paramName) + ?.sortedByDescending { it.meta.lastUpdated } + ?.firstOrNull() + if (lastCarePlan != null) { + currentBundle.addEntry(Bundle.BundleEntryComponent().setResource(lastCarePlan)) + } - val lastCarePlan = getLastActiveCarePlan(patientId) - if (lastCarePlan != null) { - currentBundle.addEntry(Bundle.BundleEntryComponent().setResource(lastCarePlan)) + resourcesList[bundleIndex] = currentBundle } - resourcesList[bundleIndex] = currentBundle - } - - // for situations where patient RelatedPersons not passed through intent extras - if (resourcesList.none { it.resourceType == ResourceType.RelatedPerson }) { - loadRelatedPerson(patientId)?.forEach { resourcesList.add(it) } + // for situations where patient RelatedPersons not passed through intent extras + if (resourcesList.none { it.resourceType == ResourceType.RelatedPerson }) { + loadRelatedPerson(patientId)?.forEach { resourcesList.add(it) } + } } } } @@ -746,7 +772,10 @@ constructor( val populationResourcesList = getPopulationResources(intent, questionnaire.logicalId) val populationResourceTypeResourceMap = populationResourcesList.associateBy { it.resourceType.name.lowercase() } - val questResponse = ResourceMapper.populate(questionnaire, populationResourceTypeResourceMap) + val questResponse = + withContext(dispatcherProvider.default()) { + ResourceMapper.populate(questionnaire, populationResourceTypeResourceMap) + } questResponse.contained = populationResourcesList.toList() questResponse.questionnaire = "${questionnaire.resourceType}/${questionnaire.logicalId}" return questResponse diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtension.kt index 286b95eb54..19d563e8fa 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/FhirEngineExtension.kt @@ -23,10 +23,8 @@ import com.google.android.fhir.SearchResult import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.get -import com.google.android.fhir.search.Operation import com.google.android.fhir.search.Search import com.google.android.fhir.search.SearchQuery -import com.google.android.fhir.search.filter.TokenParamFilterCriterion import com.google.android.fhir.search.search import com.google.android.fhir.workflow.FhirOperator import org.hl7.fhir.r4.model.Composition @@ -111,15 +109,7 @@ suspend fun FhirEngine.addDateTimeIndex() { suspend inline fun FhirEngine.getResourcesByIds( list: List, -): List { - if (list.isEmpty()) return listOf() - val paramQueries: List<(TokenParamFilterCriterion.() -> Unit)> = - list.map { id -> { value = of(id) } } - return this.search { - filter(Resource.RES_ID, *paramQueries.toTypedArray(), operation = Operation.OR) - } - .map { it.resource } -} +): List = list.map { this.get(it) } suspend fun FhirEngine.forceTagsUpdate(source: Resource) { try { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt index 6c3e47d3a5..b5aef8dd54 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt @@ -19,7 +19,6 @@ package org.smartregister.fhircore.engine.util.extension import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.parser.IParser import ca.uhn.fhir.rest.gclient.ReferenceClientParam -import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem import com.google.android.fhir.datacapture.extensions.logicalId import java.util.Date import java.util.LinkedList @@ -139,20 +138,6 @@ fun JSONObject.updateFrom(updated: JSONObject) { keys.forEach { key -> updated.opt(key)?.run { put(key, this) } } } -fun QuestionnaireResponse.generateMissingItems(questionnaire: Questionnaire) = - questionnaire.item.generateMissingItems(this.item) - -fun List.generateMissingItems( - qrItems: MutableList, -) { - this.forEachIndexed { index, qItem -> - // generate complete hierarchy if response item missing otherwise check for nested items - if (qrItems.isEmpty() || qItem.linkId != qrItems[index].linkId) { - qrItems.add(index, qItem.createQuestionnaireResponseItem()) - } else qItem.item.generateMissingItems(qrItems[index].item) - } -} - /** * Set all questions that are not of type [Questionnaire.QuestionnaireItemType.GROUP] to readOnly if * [readOnly] is true. This also generates the correct FHIRPath population expression for each @@ -260,7 +245,7 @@ fun Resource.setPropertySafely(name: String, value: Base) = fun generateUniqueId() = UUID.randomUUID().toString() -fun Base.extractWithFhirPath(expression: String) = +suspend fun Base.extractWithFhirPath(expression: String) = FhirPathDataExtractor.extractData(this, expression).firstOrNull()?.primitiveValue() ?: "" fun ArrayList.asBaseResources(): ArrayList { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/fhirpath/FhirPathDataExtractor.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/fhirpath/FhirPathDataExtractor.kt index feca62281a..4b5590b352 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/fhirpath/FhirPathDataExtractor.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/fhirpath/FhirPathDataExtractor.kt @@ -18,6 +18,8 @@ package org.smartregister.fhircore.engine.util.fhirpath import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.utils.FHIRPathEngine @@ -29,9 +31,11 @@ object FhirPathDataExtractor { private val fhirPathEngine: FHIRPathEngine = FHIRPathEngine(HapiWorkerContext(fhirContext, fhirContext.validationSupport)) - fun extractData(base: Base, expressions: Map): Map> = - expressions.map { Pair(it.key, fhirPathEngine.evaluate(base, it.value)) }.toMap() + suspend fun extractData(base: Base, expressions: Map): Map> = + withContext(Dispatchers.Default) { + expressions.map { Pair(it.key, fhirPathEngine.evaluate(base, it.value)) }.toMap() + } - fun extractData(base: Base, expression: String): List = - fhirPathEngine.evaluate(base, expression) + suspend fun extractData(base: Base, expression: String): List = + withContext(Dispatchers.Default) { fhirPathEngine.evaluate(base, expression) } } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivityTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivityTest.kt index 0bf62d29fd..ff86945d15 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivityTest.kt @@ -187,7 +187,6 @@ class QuestionnaireActivityTest : ActivityRobolectricTest() { clientIdentifier = "1234", formName = "my-form", questionnaireType = QuestionnaireType.READ_ONLY, - questionnaireResponse = questionnaireResponse, populationResources = populationResources, ) Assert.assertEquals("my-form", result.getString(QuestionnaireActivity.QUESTIONNAIRE_ARG_FORM)) @@ -199,12 +198,6 @@ class QuestionnaireActivityTest : ActivityRobolectricTest() { QuestionnaireType.READ_ONLY.name, result.getString(QuestionnaireActivity.QUESTIONNAIRE_ARG_TYPE), ) - Assert.assertEquals( - FhirContext.forCached(FhirVersionEnum.R4) - .newJsonParser() - .encodeResourceToString(questionnaireResponse), - result.getString(QuestionnaireActivity.QUESTIONNAIRE_RESPONSE), - ) Assert.assertEquals( FhirContext.forCached(FhirVersionEnum.R4).newJsonParser().encodeResourceToString(patient), result.getStringArrayList(QuestionnaireActivity.QUESTIONNAIRE_POPULATION_RESOURCES)?.get(0), @@ -379,10 +372,6 @@ class QuestionnaireActivityTest : ActivityRobolectricTest() { putExtra(QuestionnaireActivity.QUESTIONNAIRE_TITLE_KEY, "Patient registration") putExtra(QuestionnaireActivity.QUESTIONNAIRE_ARG_FORM, "patient-registration") putExtra(QuestionnaireActivity.QUESTIONNAIRE_ARG_TYPE, QuestionnaireType.READ_ONLY.name) - putExtra( - QuestionnaireActivity.QUESTIONNAIRE_RESPONSE, - QuestionnaireResponse().encodeResourceToString(), - ) } val questionnaireString = parser.encodeResourceToString(Questionnaire()) diff --git a/android/quest/build.gradle.kts b/android/quest/build.gradle.kts index 78c38dd0f9..0d18ad5fc8 100644 --- a/android/quest/build.gradle.kts +++ b/android/quest/build.gradle.kts @@ -208,6 +208,9 @@ dependencies { kapt("androidx.room:room-compiler:2.6.1") implementation("androidx.room:room-ktx:2.6.1") + implementation("com.github.bumptech.glide:glide:4.16.0") + kapt("com.github.bumptech.glide:compiler:4.16.0") + androidTestImplementation(Deps.atsl.ext_junit) androidTestImplementation(Deps.atsl.espresso) debugImplementation("androidx.compose.ui:ui-test-manifest") diff --git a/android/quest/src/main/AndroidManifest.xml b/android/quest/src/main/AndroidManifest.xml index 63cb197191..9031fed68c 100644 --- a/android/quest/src/main/AndroidManifest.xml +++ b/android/quest/src/main/AndroidManifest.xml @@ -8,10 +8,8 @@ android:hardwareAccelerated="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" - android:largeHeap="true" android:networkSecurityConfig="@xml/network_security_config" android:supportsRtl="true" - android:theme="@style/AppTheme.NoActionBar" tools:ignore="UnusedAttribute"> diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/QuestGlideModule.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/QuestGlideModule.kt new file mode 100644 index 0000000000..538540908e --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/QuestGlideModule.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.quest + +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule + +@GlideModule class QuestGlideModule : AppGlideModule() 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 4b6b852289..48abcf9d1a 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 @@ -165,9 +165,11 @@ open class AppMainActivity : BaseMultiLanguageActivity(), OnSyncListener { } private fun scheduleAuthWorkers() { - val isAuthenticated = tokenAuthenticator.sessionActive() - if (isAuthenticated) { - with(configService) { scheduleAuditEvent(applicationContext) } + lifecycleScope.launch { + val isAuthenticated = tokenAuthenticator.isSessionActive() + if (isAuthenticated) { + with(configService) { scheduleAuditEvent(applicationContext) } + } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileViewModel.kt index b10c622ccf..d37b5369dd 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/PatientProfileViewModel.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.hl7.fhir.r4.model.ResourceType import org.smartregister.fhircore.engine.appfeature.AppFeature @@ -194,17 +195,22 @@ constructor( it.task.isGuardianVisit(applicationConfiguration.taskFilterTagViaMetaCodingSystem) }, ) - _patientProfileViewDataFlow.value = - profileViewDataMapper.transformInputToOutputModel(newProfileData) - as ProfileViewData.PatientProfileViewData + viewModelScope.launch { + _patientProfileViewDataFlow.update { + profileViewDataMapper.transformInputToOutputModel(newProfileData) + as ProfileViewData.PatientProfileViewData + } + } } } fun undoGuardianVisitTasksFilter() { if (patientProfileData != null) { - _patientProfileViewDataFlow.value = - profileViewDataMapper.transformInputToOutputModel(patientProfileData!!) - as ProfileViewData.PatientProfileViewData + viewModelScope.launch { + _patientProfileViewDataFlow.value = + profileViewDataMapper.transformInputToOutputModel(patientProfileData!!) + as ProfileViewData.PatientProfileViewData + } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/tranfer/TransferOutViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/tranfer/TransferOutViewModel.kt index 2585ee75f0..a577217987 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/tranfer/TransferOutViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/profile/tranfer/TransferOutViewModel.kt @@ -33,6 +33,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -155,7 +156,8 @@ constructor( } suspend fun generateResponse(questionnaire: Questionnaire): QuestionnaireResponse { - val questResponse = ResourceMapper.populate(questionnaire, mapOf()) + val questResponse = + withContext(Dispatchers.Default) { ResourceMapper.populate(questionnaire, mapOf()) } return questResponse } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/PatientItemMapper.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/PatientItemMapper.kt index a024107aaa..d4852b3ef9 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/PatientItemMapper.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/patient/register/PatientItemMapper.kt @@ -36,7 +36,7 @@ import org.smartregister.fhircore.quest.data.patient.model.PatientItem class PatientItemMapper @Inject constructor(@ApplicationContext val context: Context) : DataMapper { - override fun transformInputToOutputModel(inputModel: Patient): PatientItem { + override suspend fun transformInputToOutputModel(inputModel: Patient): PatientItem { val name = inputModel.extractName() val gender = inputModel.extractGender(context)?.first() ?: "" val age = inputModel.extractAge() diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/task/PatientTaskItemMapper.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/task/PatientTaskItemMapper.kt index 0082df7f36..894d2ac0ef 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/task/PatientTaskItemMapper.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/task/PatientTaskItemMapper.kt @@ -37,7 +37,7 @@ constructor( @ApplicationContext val context: Context, ) : DataMapper { - override fun transformInputToOutputModel(inputModel: PatientTask): PatientTaskItem { + override suspend fun transformInputToOutputModel(inputModel: PatientTask): PatientTaskItem { val patient = inputModel.patient val task = inputModel.task diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/MeasureReportPatientViewDataMapper.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/MeasureReportPatientViewDataMapper.kt index 4073d0ee9f..2342b378fd 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/MeasureReportPatientViewDataMapper.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/MeasureReportPatientViewDataMapper.kt @@ -29,7 +29,7 @@ class MeasureReportPatientViewDataMapper @Inject constructor(@ApplicationContext val context: Context) : DataMapper { - override fun transformInputToOutputModel( + override suspend fun transformInputToOutputModel( inputModel: RegisterData.AncRegisterData, ): MeasureReportPatientViewData { return MeasureReportPatientViewData( diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/ProfileViewDataMapper.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/ProfileViewDataMapper.kt index ebec1ca9a1..9bdc26e327 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/ProfileViewDataMapper.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/ProfileViewDataMapper.kt @@ -57,7 +57,7 @@ class ProfileViewDataMapper @Inject constructor(@ApplicationContext val context: private val simpleDateFormat = SimpleDateFormat("dd MMM", Locale.getDefault()) - override fun transformInputToOutputModel(inputModel: ProfileData): ProfileViewData { + override suspend fun transformInputToOutputModel(inputModel: ProfileData): ProfileViewData { return when (inputModel) { is ProfileData.AncProfileData -> ProfileViewData.PatientProfileViewData( diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/RegisterViewDataMapper.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/RegisterViewDataMapper.kt index 376067a377..a647aa77c4 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/RegisterViewDataMapper.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/mappers/RegisterViewDataMapper.kt @@ -38,7 +38,7 @@ import org.smartregister.fhircore.quest.ui.shared.models.ServiceMember class RegisterViewDataMapper @Inject constructor(@ApplicationContext val context: Context) : DataMapper { - override fun transformInputToOutputModel( + override suspend fun transformInputToOutputModel( inputModel: RegisterData, ): RegisterViewData.ListItemView { return when (inputModel) { diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/patient/register/PatientMapperTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/patient/register/PatientMapperTest.kt index 66d513c3d7..d10a12f94c 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/patient/register/PatientMapperTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/patient/register/PatientMapperTest.kt @@ -21,6 +21,7 @@ import dagger.hilt.android.testing.HiltAndroidTest import java.util.Calendar import java.util.Date import javax.inject.Inject +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.DateType import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.StringType @@ -43,7 +44,7 @@ class PatientMapperTest : RobolectricTest() { } @Test - fun testMapToDomainModel() { + fun testMapToDomainModel() = runTest { val dto = buildPatient("123456", "123456", "Doe", "John", 12, listOf("+12345678")) val patientItem = patientItemMapper.transformInputToOutputModel(dto) with(patientItem) { @@ -63,7 +64,7 @@ class PatientMapperTest : RobolectricTest() { } @Test - fun testMapToDomainModelWithoutIdentifier() { + fun testMapToDomainModelWithoutIdentifier() = runTest { val dto = buildPatient("123456", null, "Doe", "John", 12, listOf("+1234", "+5678")) val patientItem = patientItemMapper.transformInputToOutputModel(dto)