Skip to content

Commit

Permalink
Merge branch 'google:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
jingtang10 authored Jul 11, 2024
2 parents 7611d6e + a36e7a6 commit 9d596aa
Show file tree
Hide file tree
Showing 38 changed files with 348 additions and 129 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ on:
- cron: "32 13 * * 2"

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref || || github.run_id }}
group: ${{ github.workflow }}-${{ github.head_ref || github.ref || github.run_id }}
cancel-in-progress: true

jobs:
Expand Down
39 changes: 1 addition & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,7 @@ healthcare applications using the [HL7® FHIR® standard](https://www.hl7.org/fh
aims to accelerate the adoption of FHIR by making it easy to incorporate FHIR into new and existing
mobile applications.

## Libraries

The SDK contains the following libraries:

| Library | Latest release | Code | Docs | Min SDK | Summary |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Data Capture Library | [![Google Maven](https://badgen.net/maven/v/metadata-url/dl.google.com/dl/android/maven2/com/google/android/fhir/data-capture/maven-metadata.xml)](https://maven.google.com/web/index.html?#com.google.android.fhir:data-capture) | [code](https://github.com/google/android-fhir/tree/master/datacapture)| [docs](https://google.github.io/android-fhir/use/SDCL/) | Android 7.0 (API level 24) | Collect, validate, and process healthcare data on Android |
| FHIR Engine Library | [![Google Maven](https://badgen.net/maven/v/metadata-url/dl.google.com/dl/android/maven2/com/google/android/fhir/engine/maven-metadata.xml)](https://maven.google.com/web/index.html?#com.google.android.fhir:engine) | [code](https://github.com/google/android-fhir/tree/master/engine) | [docs](https://google.github.io/android-fhir/use/FEL/) | Android 7.0 (API level 24) | Store and manage FHIR resources locally on Android and synchronize with FHIR server |
| Workflow Library | [![Google Maven](https://badgen.net/maven/v/metadata-url/dl.google.com/dl/android/maven2/com/google/android/fhir/workflow/maven-metadata.xml)](https://maven.google.com/web/index.html?#com.google.android.fhir:workflow) | [code](https://github.com/google/android-fhir/tree/master/workflow) | [docs](https://google.github.io/android-fhir/use/WFL/) | Android 7.0 (API level 24) | Provide decision support and analytics in clinical workflow on Android including implementation of specific FHIR operations ($measure_evaluate and $apply) |

## Demo apps

This repository also contains the following demo apps:

| Demo app | Code | Docs |
| ----------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------- |
| FHIR Engine Demo App | [code](https://github.com/google/android-fhir/tree/master/demo) | [docs](https://google.github.io/android-fhir/use/FEL/Demo-app/) |
| Structured Data Capture Catalog App | [code](https://github.com/google/android-fhir/tree/master/catalog) | [docs](https://google.github.io/android-fhir/use/SDCL/Demo-app/) |

**These applications are provided for demo purposes only. Do NOT use in production.**

## Contributing

The development of the SDK began as a collaborative effort between Google, [The World Health Organization](https://www.who.int/), and [Ona](https://ona.io/). Since then, a consortium of application developers have been contributing to the project.

To contribute to the project, please see [Contributing](https://google.github.io/android-fhir/contrib/) to get started.

## Feedback and getting help

You can create a [GitHub issue](https://github.com/google/android-fhir/issues) for bugs and feature requests.

In case you find any security bug, please do NOT create a GitHub issue. Instead, email us at <android-fhir-sdk-feedback@google.com>.

If you want to provide any feedback or discuss use cases you can:

* email us at <android-fhir-sdk-feedback@google.com>
* start a [GitHub discussion](https://github.com/google/android-fhir/discussions)
* start a new topic in [android](https://chat.fhir.org/#narrow/stream/276344-android), [questionnaire](https://chat.fhir.org/#narrow/stream/179255-questionnaire), [implementers](https://chat.fhir.org/#narrow/stream/179166-implementers), or [WHO SMART guidelines](https://chat.fhir.org/#narrow/stream/310477-who-smart-guidelines) stream in the [FHIR Zulip chat](https://chat.fhir.org/)
See [documentation](https://google.github.io/android-fhir/).

## Disclaimer

Expand Down
2 changes: 1 addition & 1 deletion buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repositories {
dependencies {
implementation("com.diffplug.spotless:spotless-plugin-gradle:6.22.0")

implementation("com.android.tools.build:gradle:8.1.4")
implementation("com.android.tools.build:gradle:8.5.0")

implementation("app.cash.licensee:licensee-gradle-plugin:1.8.0")
implementation("com.osacky.flank.gradle:fladle:0.17.4")
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Plugins.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ object Plugins {
"com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:${Versions.kspPlugin}"

object Versions {
const val androidGradlePlugin = "8.0.2"
const val androidGradlePlugin = "8.5.0"
const val benchmarkPlugin = "1.1.0"
const val dokka = "1.9.20"
const val kspPlugin = "1.9.22-1.0.18"
Expand Down
8 changes: 4 additions & 4 deletions buildSrc/src/main/kotlin/Sdk.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Google LLC
* Copyright 2023-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,10 +15,10 @@
*/

object Sdk {
const val compileSdk = 33
const val targetSdk = 31
const val COMPILE_SDK = 33
const val TARGET_SDK = 31

// Engine and SDC must support API 24.
// Remove desugaring when upgrading it to 26.
const val minSdk = 24
const val MIN_SDK = 26
}
6 changes: 3 additions & 3 deletions catalog/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ configureRuler()

android {
namespace = "com.google.android.fhir.catalog"
compileSdk = Sdk.compileSdk
compileSdk = Sdk.COMPILE_SDK
defaultConfig {
applicationId = Releases.Catalog.applicationId
minSdk = Sdk.minSdk
targetSdk = Sdk.targetSdk
minSdk = Sdk.MIN_SDK
targetSdk = Sdk.TARGET_SDK
versionCode = Releases.Catalog.versionCode
versionName = Releases.Catalog.versionName
testInstrumentationRunner = Dependencies.androidJunitRunner
Expand Down
2 changes: 1 addition & 1 deletion codelabs/datacapture/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ android {

defaultConfig {
applicationId = "com.google.android.fhir.codelabs.datacapture"
minSdk = 24
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
Expand Down
2 changes: 1 addition & 1 deletion codelabs/engine/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ android {

defaultConfig {
applicationId = "com.google.android.fhir.codelabs.engine"
minSdk = 24
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
Expand Down
4 changes: 2 additions & 2 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ createJacocoTestReportTask()

android {
namespace = "com.google.android.fhir.common"
compileSdk = Sdk.compileSdk
defaultConfig { minSdk = Sdk.minSdk }
compileSdk = Sdk.COMPILE_SDK
defaultConfig { minSdk = Sdk.MIN_SDK }
configureJacocoTestOptions()
kotlin { jvmToolchain(11) }
}
Expand Down
12 changes: 10 additions & 2 deletions contrib/barcode/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ createJacocoTestReportTask()

android {
namespace = "com.google.android.fhir.datacapture.contrib.views.barcode"
compileSdk = Sdk.compileSdk
compileSdk = Sdk.COMPILE_SDK
defaultConfig {
minSdk = Sdk.minSdk
minSdk = Sdk.MIN_SDK
testInstrumentationRunner = Dependencies.androidJunitRunner
// Need to specify this to prevent junit runner from going deep into our dependencies
testInstrumentationRunnerArguments["package"] = "com.google.android.fhir.datacapture"
Expand All @@ -29,6 +29,12 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
}
}
compileOptions {
// Flag to enable support for the new language APIs
// See https://developer.android.com/studio/write/java8-support
isCoreLibraryDesugaringEnabled = true
}

packaging {
resources.excludes.addAll(
listOf(
Expand Down Expand Up @@ -58,6 +64,8 @@ dependencies {
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.truth)

coreLibraryDesugaring(Dependencies.desugarJdkLibs)

implementation(project(":datacapture"))
implementation(Dependencies.Mlkit.barcodeScanning)
implementation(Dependencies.Mlkit.objectDetection)
Expand Down
11 changes: 9 additions & 2 deletions contrib/locationwidget/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ createJacocoTestReportTask()

android {
namespace = "com.google.android.fhir.datacapture.contrib.views.locationwidget"
compileSdk = Sdk.compileSdk
compileSdk = Sdk.COMPILE_SDK
defaultConfig {
minSdk = Sdk.minSdk
minSdk = Sdk.MIN_SDK
testInstrumentationRunner = Dependencies.androidJunitRunner
// Need to specify this to prevent junit runner from going deep into our dependencies
testInstrumentationRunnerArguments["package"] = "com.google.android.fhir.datacapture"
Expand All @@ -29,6 +29,11 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
}
}
compileOptions {
// Flag to enable support for the new language APIs
// See https://developer.android.com/studio/write/java8-support
isCoreLibraryDesugaringEnabled = true
}

packaging {
resources.excludes.addAll(
Expand Down Expand Up @@ -67,6 +72,8 @@ dependencies {
implementation(libs.androidx.fragment)
implementation(libs.kotlinx.coroutines.playservices)

coreLibraryDesugaring(Dependencies.desugarJdkLibs)

testImplementation(Dependencies.robolectric)
testImplementation(libs.androidx.fragment.testing)
testImplementation(libs.junit)
Expand Down
4 changes: 2 additions & 2 deletions datacapture/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ createJacocoTestReportTask()

android {
namespace = "com.google.android.fhir.datacapture"
compileSdk = Sdk.compileSdk
compileSdk = Sdk.COMPILE_SDK
defaultConfig {
minSdk = Sdk.minSdk
minSdk = Sdk.MIN_SDK
testInstrumentationRunner = Dependencies.androidJunitRunner
// Need to specify this to prevent junit runner from going deep into our dependencies
testInstrumentationRunnerArguments["package"] = "com.google.android.fhir.datacapture"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.android.fhir.datacapture

import android.annotation.SuppressLint
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
Expand Down Expand Up @@ -328,8 +329,28 @@ internal object DiffCallbacks {
QUESTIONS.areContentsTheSame(oldItem, newItem)
}
is QuestionnaireAdapterItem.RepeatedGroupHeader -> {
newItem is QuestionnaireAdapterItem.RepeatedGroupHeader &&
oldItem.responses == newItem.responses
if (newItem is QuestionnaireAdapterItem.RepeatedGroupHeader) {
// The `onDeleteClicked` function is a function closure generated in the questionnaire
// viewmodel with a reference to the parent questionnaire view item. When it is
// invoked, it deletes the current repeated group instance from the parent
// questionnaire view item by removing it from the list of children in the parent
// questionnaire view.
// In other words, although the `onDeleteClicked` function is not a data field, it is
// a function closure with references to data structures. Because
// `RepeatedGroupHeader` does not include any other data fields besides the index, it
// is particularly important to distinguish between different `RepeatedGroupHeader`s
// by the `onDeleteClicked` function.
// If this check is not here, an old RepeatedGroupHeader might be mistakenly
// considered up-to-date and retained in the recycler view even though a newer
// version includes a different `onDeleteClicked` function referencing a parent item
// with a different list of children. As a result clicking the delete function might
// result in deleting from an old list.
@SuppressLint("DiffUtilEquals")
val onDeleteClickedCallbacksEqual = oldItem.onDeleteClicked == newItem.onDeleteClicked
onDeleteClickedCallbacksEqual
} else {
false
}
}
is QuestionnaireAdapterItem.Navigation -> {
newItem is QuestionnaireAdapterItem.Navigation &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.datacapture.enablement.EnablementEvaluator
import com.google.android.fhir.datacapture.expressions.EnabledAnswerOptionsEvaluator
import com.google.android.fhir.datacapture.extensions.EntryMode
import com.google.android.fhir.datacapture.extensions.addNestedItemsToAnswer
import com.google.android.fhir.datacapture.extensions.allItems
import com.google.android.fhir.datacapture.extensions.copyNestedItemsToChildlessAnswers
import com.google.android.fhir.datacapture.extensions.cqfExpression
import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem
import com.google.android.fhir.datacapture.extensions.entryMode
Expand Down Expand Up @@ -361,7 +361,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}
}
if (questionnaireItem.shouldHaveNestedItemsUnderAnswers) {
questionnaireResponseItem.addNestedItemsToAnswer(questionnaireItem)
questionnaireResponseItem.copyNestedItemsToChildlessAnswers(questionnaireItem)

// If nested items are added to the answer, the enablement evaluator needs to be
// reinitialized in order for it to rebuild the pre-order map and parent map of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -887,29 +887,52 @@ internal val QuestionnaireItemComponent.shouldHaveNestedItemsUnderAnswers: Boole
get() = item.isNotEmpty() && (type != Questionnaire.QuestionnaireItemType.GROUP || repeats)

/**
* Creates a list of [QuestionnaireResponse.QuestionnaireResponseItemComponent]s from the nested
* items in the [Questionnaire.QuestionnaireItemComponent].
* Creates a list of [QuestionnaireResponse.QuestionnaireResponseItemComponent]s corresponding to
* the nested items under the questionnaire item.
*
* The list can be added as nested items under answers in a corresponding questionnaire response
* item. This may be because
* 1. the questionnaire item is a question with nested questions, in which case each answer in the
* questionnaire response item needs to have the same nested questions, or
* 2. the questionnaire item is a repeated group, in which case each answer in the questionnaire
* response item represents an instance of the repeated group, and needs to have the same nested
* questions.
*
* The hierarchy and order of child items will be retained as specified in the standard. See
* https://www.hl7.org/fhir/questionnaireresponse.html#notes for more details.
*/
fun QuestionnaireItemComponent.getNestedQuestionnaireResponseItems() =
internal fun QuestionnaireItemComponent.createNestedQuestionnaireResponseItems() =
item.map { it.createQuestionnaireResponseItem() }

/**
* Creates a [QuestionnaireResponse.QuestionnaireResponseItemComponent] from the provided
* [Questionnaire.QuestionnaireItemComponent].
* Creates a corresponding [QuestionnaireResponse.QuestionnaireResponseItemComponent] for the
* questionnaire item with the following properties:
* - same `linkId` as the questionnaire item,
* - any initial answer(s) specified either in the `initial` element or as `initialSelected`
* `answerOption`(s),
* - any nested questions under the initial answers (there will be no user input yet since this is
* just being created) if this is a question with nested questions, and
* - any nested questions if this is a non-repeated group.
*
* Note that although initial answers to a repeated group may be interpreted as initial instances of
* the repeated group in the in-memory representation of questionnaire response, they are not
* defined as such in the standard. As a result, we are not treating them as such in this function
* to be conformant.
*
* The hierarchy and order of child items will be retained as specified in the standard. See
* https://www.hl7.org/fhir/questionnaireresponse.html#notes for more details.
*/
fun QuestionnaireItemComponent.createQuestionnaireResponseItem():
internal fun QuestionnaireItemComponent.createQuestionnaireResponseItem():
QuestionnaireResponse.QuestionnaireResponseItemComponent {
return QuestionnaireResponse.QuestionnaireResponseItemComponent().apply {
linkId = this@createQuestionnaireResponseItem.linkId
answer = createQuestionnaireResponseItemAnswers()
if (shouldHaveNestedItemsUnderAnswers && answer.isNotEmpty()) {
this.addNestedItemsToAnswer(this@createQuestionnaireResponseItem)
if (
type != Questionnaire.QuestionnaireItemType.GROUP &&
this@createQuestionnaireResponseItem.item.isNotEmpty() &&
answer.isNotEmpty()
) {
this.copyNestedItemsToChildlessAnswers(this@createQuestionnaireResponseItem)
} else if (
this@createQuestionnaireResponseItem.type == Questionnaire.QuestionnaireItemType.GROUP &&
!repeats
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Google LLC
* Copyright 2023-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -38,14 +38,24 @@ private fun QuestionnaireResponse.QuestionnaireResponseItemComponent.appendDesce
}

/**
* Add nested items under the provided `questionnaireItem` to each answer in the questionnaire
* response item. The hierarchy and order of nested items will be retained as specified in the
* standard.
* Copies nested items under `questionnaireItem` to each answer without children. The hierarchy and
* order of nested items will be retained as specified in the standard.
*
* Existing answers with nested items will not be modified because the nested items may contain
* answers already.
*
* This should be used when
* - a new answer is added to a question with nested questions, or
* - a new answer is added to a repeated group (in which case this indicates a new instance of the
* repeated group will be added to the final questionnaire response).
*
* See https://www.hl7.org/fhir/questionnaireresponse.html#notes for more details.
*/
fun QuestionnaireResponse.QuestionnaireResponseItemComponent.addNestedItemsToAnswer(
internal fun QuestionnaireResponse.QuestionnaireResponseItemComponent
.copyNestedItemsToChildlessAnswers(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
) {
answer.forEach { it.item = questionnaireItem.getNestedQuestionnaireResponseItems() }
answer
.filter { it.item.isEmpty() }
.forEach { it.item = questionnaireItem.createNestedQuestionnaireResponseItems() }
}
Loading

0 comments on commit 9d596aa

Please sign in to comment.