Skip to content

Commit

Permalink
Merge branch 'main' into implement-reading-configuration-resources
Browse files Browse the repository at this point in the history
  • Loading branch information
SebaMutuku authored May 14, 2024
2 parents d9afc03 + a3ff6f2 commit 791a040
Show file tree
Hide file tree
Showing 86 changed files with 4,157 additions and 379 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ env:

jobs:
engine-tests:
runs-on: macos-latest
runs-on: macos-13
strategy:
matrix:
api-level: [30]
Expand Down Expand Up @@ -94,7 +94,7 @@ jobs:
run: bash <(curl -s https://codecov.io/bash) -F engine -f "engine/build/reports/jacoco/fhircoreJacocoReport/fhircoreJacocoReport.xml"

geowidget-tests:
runs-on: macos-latest
runs-on: macos-13
strategy:
matrix:
api-level: [30]
Expand Down Expand Up @@ -174,7 +174,7 @@ jobs:
run: bash <(curl -s https://codecov.io/bash) -F geowidget -f "geowidget/build/reports/jacoco/fhircoreJacocoReport/fhircoreJacocoReport.xml"

quest-tests:
runs-on: macos-latest
runs-on: macos-13
strategy:
matrix:
api-level: [30]
Expand Down Expand Up @@ -251,4 +251,4 @@ jobs:
- name: Upload Quest module test coverage report to Codecov
if: matrix.api-level == 30 # Only upload coverage on API level 30
working-directory: android
run: bash <(curl -s https://codecov.io/bash) -F quest -f "quest/build/reports/jacoco/fhircoreJacocoReport/fhircoreJacocoReport.xml"
run: bash <(curl -s https://codecov.io/bash) -F quest -f "quest/build/reports/jacoco/fhircoreJacocoReport/fhircoreJacocoReport.xml"
3 changes: 2 additions & 1 deletion android/engine/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ configurations {
}

dependencies {
implementation(libs.play.services.tasks)
implementation(libs.gms.play.services.location)
coreLibraryDesugaring(libs.core.desugar)

// Library dependencies
Expand Down Expand Up @@ -160,7 +162,6 @@ dependencies {
api(libs.jjwt)
api(libs.fhir.common.utils) { exclude(group = "org.slf4j", module = "jcl-over-slf4j") }
api(libs.runtime.livedata)
// api(libs.material3)
api(libs.foundation)
api(libs.fhir.common.utils)
api(libs.kotlinx.serialization.json)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,19 @@ sealed class ConfigType(
val parseAsResource: Boolean = false,
val multiConfig: Boolean = false,
) {
object Application : ConfigType("application")
data object Application : ConfigType("application")

object Sync : ConfigType(name = "sync", parseAsResource = true)
data object Sync : ConfigType(name = "sync", parseAsResource = true)

object Navigation : ConfigType("navigation")
data object Navigation : ConfigType("navigation")

object Register : ConfigType(name = "register", multiConfig = true)
data object Register : ConfigType(name = "register", multiConfig = true)

object MeasureReport : ConfigType(name = "measureReport", multiConfig = true)
data object MeasureReport : ConfigType(name = "measureReport", multiConfig = true)

object Profile : ConfigType(name = "profile", multiConfig = true)
data object Profile : ConfigType(name = "profile", multiConfig = true)

object GeoWidget : ConfigType(name = "geoWidget", multiConfig = true)
data object GeoWidget : ConfigType(name = "geoWidget", multiConfig = true)

object DataMigration : ConfigType(name = "dataMigration", multiConfig = true)
data object DataMigration : ConfigType(name = "dataMigration", multiConfig = true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package org.smartregister.fhircore.engine.configuration

import android.content.Context
import android.database.SQLException
import android.os.Process
import ca.uhn.fhir.context.FhirContext
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.db.ResourceNotFoundException
Expand All @@ -44,6 +43,7 @@ import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Binary
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Composition
import org.hl7.fhir.r4.model.ImplementationGuide
import org.hl7.fhir.r4.model.ListResource
import org.hl7.fhir.r4.model.MetadataResource
import org.hl7.fhir.r4.model.Resource
Expand Down Expand Up @@ -105,12 +105,6 @@ constructor(

private val jsonParser = fhirContext.newJsonParser()

init {
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
Process.killProcess(Process.myPid())
}
}

/**
* Retrieve configuration for the provided [ConfigType]. The JSON retrieved from [configsJsonMap]
* can be directly converted to a FHIR resource or hard coded custom model. The filtering assumes
Expand Down Expand Up @@ -416,7 +410,7 @@ constructor(
sharedPreferencesHelper.read(SharedPreferenceKey.APP_ID.name, null)?.let { appId ->
val parsedAppId = appId.substringBefore(TYPE_REFERENCE_DELIMITER).trim()
val patientRelatedResourceTypes = mutableListOf<ResourceType>()
val compositionResource = fetchRemoteComposition(parsedAppId)
val compositionResource = fetchRemoteCompositionByAppId(parsedAppId)
compositionResource?.let { composition ->
composition
.retrieveCompositionSections()
Expand Down Expand Up @@ -463,11 +457,43 @@ constructor(
}
}

suspend fun fetchRemoteComposition(appId: String?): Composition? {
Timber.i("Fetching configs for app $appId")
suspend fun fetchRemoteImplementationGuideByAppId(
appId: String?,
appVersionCode: Int?,
): ImplementationGuide? {
Timber.i("Fetching ImplementationGuide config for app $appId version $appVersionCode")

val urlPath =
"${ResourceType.Composition.name}?${Composition.SP_IDENTIFIER}=$appId&_count=$DEFAULT_COUNT"
"ImplementationGuide?&name=$appId&context-quantity=le$appVersionCode&_sort=-context-quantity&_count=1"
return fhirResourceDataSource.getResource(urlPath).entryFirstRep.let {
if (!it.hasResource()) {
Timber.w("No response for ImplementationGuide resource on path $urlPath")
return null
}

it.resource as ImplementationGuide
}
}

suspend fun fetchRemoteCompositionById(
id: String?,
version: String?,
): Composition? {
Timber.i("Fetching Composition config id $id version $version")
val urlPath = "Composition/$id/_history/$version"
return fhirResourceDataSource.getResource(urlPath).entryFirstRep.let {
if (!it.hasResource()) {
Timber.w("No response for composition resource on path $urlPath")
return null
}

it.resource as Composition
}
}

suspend fun fetchRemoteCompositionByAppId(appId: String?): Composition? {
Timber.i("Fetching Composition config for app $appId")
val urlPath = "Composition?identifier=$appId&_count=$DEFAULT_COUNT"
return fhirResourceDataSource.getResource(urlPath).entryFirstRep.let {
if (!it.hasResource()) {
Timber.w("No response for composition resource on path $urlPath")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ data class ApplicationConfiguration(
val languages: List<String> = listOf("en"),
val useDarkTheme: Boolean = false,
val syncInterval: Long = 15,
val syncStrategies: List<String> = listOf(),
val syncStrategy: List<SyncStrategy> = listOf(),
val loginConfig: LoginConfig = LoginConfig(),
val deviceToDeviceSync: DeviceToDeviceSyncConfig? = null,
val snackBarTheme: SnackBarThemeConfig = SnackBarThemeConfig(),
Expand All @@ -42,8 +42,19 @@ data class ApplicationConfiguration(
val taskBackgroundWorkerBatchSize: Int = 500,
val eventWorkflows: List<EventWorkflow> = emptyList(),
val logGpsLocation: List<LocationLogOptions> = emptyList(),
val usePractitionerAssignedLocationOnSync: Boolean =
true, // TODO This defaults to scheduling periodic sync, otherwise use sync location ids from
// location selector
) : Configuration()

enum class SyncStrategy {
Location,
CareTeam,
RelatedEntityLocation,
Organization,
Practitioner,
}

enum class LocationLogOptions {
QUESTIONNAIRE,
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ data class ImageProperties(
) : ViewProperties(), Parcelable {
override fun interpolate(computedValuesMap: Map<String, Any>): ViewProperties {
return this.copy(
visible = visible.interpolate(computedValuesMap),
imageConfig =
imageConfig?.copy(
reference = imageConfig.reference?.interpolate(computedValuesMap),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,7 @@ enum class ApplicationWorkflow {

/** A workflow that copies text to keyboard */
COPY_TEXT,

/** A workflow that launches location selector widget * */
LAUNCH_LOCATION_SELECTOR,
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class PreferenceDataStore @Inject constructor(@ApplicationContext val context: C
companion object Keys {
val APP_ID by lazy { stringPreferencesKey("appId") }
val LANG by lazy { stringPreferencesKey("lang") }
val SYNC_LOCATION_IDS by lazy { stringPreferencesKey("syncLocationIds") }
val MIGRATION_VERSION by lazy { intPreferencesKey("migrationVersion") }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,21 @@ import javax.inject.Singleton
import kotlinx.coroutines.flow.catch
import org.smartregister.fhircore.engine.datastore.mockdata.PractitionerDetails
import org.smartregister.fhircore.engine.datastore.mockdata.UserInfo
import org.smartregister.fhircore.engine.datastore.serializers.LocationCoordinatesSerializer
import org.smartregister.fhircore.engine.datastore.serializers.PractitionerDetailsDataStoreSerializer
import org.smartregister.fhircore.engine.datastore.serializers.SyncLocationIdDataStoreSerializer
import org.smartregister.fhircore.engine.datastore.serializers.UserInfoDataStoreSerializer
import org.smartregister.fhircore.engine.domain.model.SyncLocationToggleableState
import org.smartregister.fhircore.engine.rulesengine.services.LocationCoordinate
import timber.log.Timber

private const val PRACTITIONER_DETAILS_DATASTORE_JSON = "practitioner_details.json"
private const val USER_INFO_DATASTORE_JSON = "user_info.json"

private const val LOCATION_COORDINATES_DATASTORE_JSON = "location_coordinates.json"

private const val SYNC_LOCATION_IDS = "sync_location_ids.json"

private const val TAG = "Proto DataStore"

val Context.practitionerProtoStore: DataStore<PractitionerDetails> by
Expand All @@ -46,13 +55,25 @@ val Context.userInfoProtoStore: DataStore<UserInfo> by
serializer = UserInfoDataStoreSerializer,
)

val Context.locationCoordinatesDatastore: DataStore<LocationCoordinate> by
dataStore(
fileName = LOCATION_COORDINATES_DATASTORE_JSON,
serializer = LocationCoordinatesSerializer,
)

val Context.syncLocationIdsProtoStore: DataStore<List<SyncLocationToggleableState>> by
dataStore(
fileName = SYNC_LOCATION_IDS,
serializer = SyncLocationIdDataStoreSerializer,
)

@Singleton
class ProtoDataStore @Inject constructor(@ApplicationContext val context: Context) {

val practitioner =
context.practitionerProtoStore.data.catch { exception ->
if (exception is IOException) {
Timber.tag(TAG).e(exception, "Error reading practitioner details preferences.")
Timber.e(exception, "Error reading practitioner details preferences.")
emit(PractitionerDetails())
} else {
throw exception
Expand All @@ -71,7 +92,7 @@ class ProtoDataStore @Inject constructor(@ApplicationContext val context: Contex
val userInfo =
context.userInfoProtoStore.data.catch { exception ->
if (exception is IOException) {
Timber.tag(TAG).e(exception, "Error reading practitioner details preferences.")
Timber.e(exception, "Error reading user information details preferences.")
emit(UserInfo())
} else {
throw exception
Expand All @@ -85,4 +106,24 @@ class ProtoDataStore @Inject constructor(@ApplicationContext val context: Contex
)
}
}

val locationCoordinates =
context.locationCoordinatesDatastore.data.catch { exception ->
if (exception is IOException) {
Timber.e(exception, "Error reading location co-ordinates details.")
emit(LocationCoordinate())
} else {
throw exception
}
}

suspend fun writeLocationCoordinates(locationCoordinatesDetails: LocationCoordinate) {
context.locationCoordinatesDatastore.updateData { locationCoordinatesData ->
locationCoordinatesData.copy(
longitude = locationCoordinatesDetails.longitude,
latitude = locationCoordinatesDetails.latitude,
altitude = locationCoordinatesDetails.altitude,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.engine.datastore.serializers

import androidx.datastore.core.Serializer
import java.io.InputStream
import java.io.OutputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import org.apache.commons.lang3.SerializationException
import org.smartregister.fhircore.engine.rulesengine.services.LocationCoordinate
import timber.log.Timber

object LocationCoordinatesSerializer : Serializer<LocationCoordinate> {

override val defaultValue: LocationCoordinate
get() = LocationCoordinate()

override suspend fun readFrom(input: InputStream): LocationCoordinate {
return try {
Json.decodeFromString(
deserializer = LocationCoordinate.serializer(),
string = input.readBytes().decodeToString(),
)
} catch (serializationException: SerializationException) {
Timber.e(serializationException)
defaultValue
}
}

override suspend fun writeTo(t: LocationCoordinate, output: OutputStream) {
withContext(Dispatchers.IO) {
output.write(
Json.encodeToString(
serializer = LocationCoordinate.serializer(),
value = t,
)
.encodeToByteArray(),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ object PractitionerDetailsDataStoreSerializer : Serializer<PractitionerDetails>
deserializer = PractitionerDetails.serializer(),
string = input.readBytes().decodeToString(),
)
} catch (e: SerializationException) {
Timber.tag(SerializerConstants.PROTOSTORE_SERIALIZER_TAG).d(e)
} catch (serializationException: SerializationException) {
Timber.e(serializationException)
defaultValue
}
}
Expand Down
Loading

0 comments on commit 791a040

Please sign in to comment.