diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt
index 594599d64e..61d8afb074 100644
--- a/buildSrc/src/main/kotlin/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/Dependencies.kt
@@ -101,7 +101,6 @@ object Dependencies {
const val mockWebServer = "com.squareup.okhttp3:mockwebserver:${Versions.http}"
const val jsonToolsPatch = "com.github.java-json-tools:json-patch:${Versions.jsonToolsPatch}"
- const val material = "com.google.android.material:material:${Versions.material}"
const val sqlcipher = "net.zetetic:android-database-sqlcipher:${Versions.sqlcipher}"
const val timber = "com.jakewharton.timber:timber:${Versions.timber}"
const val woodstox = "com.fasterxml.woodstox:woodstox-core:${Versions.woodstox}"
@@ -143,7 +142,6 @@ object Dependencies {
const val jsonToolsPatch = "1.13"
const val jsonAssert = "1.5.1"
- const val material = "1.9.0"
const val retrofit = "2.9.0"
const val gsonConverter = "2.1.0"
const val sqlcipher = "4.5.4"
diff --git a/buildSrc/src/main/kotlin/Releases.kt b/buildSrc/src/main/kotlin/Releases.kt
index dae9dfcc8b..4a5a54d064 100644
--- a/buildSrc/src/main/kotlin/Releases.kt
+++ b/buildSrc/src/main/kotlin/Releases.kt
@@ -48,7 +48,7 @@ object Releases {
object Engine : LibraryArtifact {
override val artifactId = "engine"
- override val version = "1.0.0"
+ override val version = "1.1.0"
override val name = "Android FHIR Engine Library"
}
@@ -98,6 +98,12 @@ object Releases {
const val versionCode = 1
const val versionName = "1.0"
}
+
+ object WorkflowDemo {
+ const val applicationId = "com.google.android.fhir.workflow.demo"
+ const val versionCode = 1
+ const val versionName = "1.0"
+ }
}
fun Project.publishArtifact(artifact: LibraryArtifact) {
diff --git a/catalog/build.gradle.kts b/catalog/build.gradle.kts
index ff0693fd64..a89e6d68e6 100644
--- a/catalog/build.gradle.kts
+++ b/catalog/build.gradle.kts
@@ -46,7 +46,6 @@ dependencies {
coreLibraryDesugaring(Dependencies.desugarJdkLibs)
- implementation(Dependencies.material)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.core)
@@ -54,6 +53,7 @@ dependencies {
implementation(libs.androidx.navigation.fragment)
implementation(libs.androidx.navigation.ui)
implementation(libs.kotlin.stdlib)
+ implementation(libs.material)
implementation(project(path = ":datacapture"))
implementation(project(path = ":engine"))
diff --git a/catalog/src/main/assets/component_modal.json b/catalog/src/main/assets/component_modal.json
index 99c24c2623..54b8dc9222 100644
--- a/catalog/src/main/assets/component_modal.json
+++ b/catalog/src/main/assets/component_modal.json
@@ -2,6 +2,24 @@
"resourceType": "Questionnaire",
"item": [
{
+ "extension": [
+ {
+ "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl",
+ "valueCodeableConcept": {
+ "coding": [
+ {
+ "system": "http://hl7.org/fhir/questionnaire-item-control",
+ "code": "check-box",
+ "display": "Check Box"
+ }
+ ],
+ "text": "Check box"
+ }
+ },
+ {
+ "url": "https://github.com/google/android-fhir/StructureDefinition/dialog"
+ }
+ ],
"linkId": "1.1",
"type": "choice",
"repeats": true,
@@ -44,48 +62,6 @@
"code": "diarrhoea",
"display": "Diarrhoea"
}
- },
- {
- "valueCoding": {
- "code": "fever",
- "display": "Fever"
- }
- },
- {
- "valueCoding": {
- "code": "injury",
- "display": "Injury"
- }
- },
- {
- "valueCoding": {
- "code": "jaundice",
- "display": "Jaundice"
- }
- },
- {
- "valueCoding": {
- "code": "mental-health",
- "display": "Mental health"
- }
- },
- {
- "valueCoding": {
- "code": "nausea",
- "display": "Nausea"
- }
- },
- {
- "valueCoding": {
- "code": "pain",
- "display": "Pain"
- }
- },
- {
- "valueCoding": {
- "code": "bleeding",
- "display": "Bleeding"
- }
}
]
}
diff --git a/catalog/src/main/assets/component_time_picker.json b/catalog/src/main/assets/component_time_picker.json
new file mode 100644
index 0000000000..ac904d99c8
--- /dev/null
+++ b/catalog/src/main/assets/component_time_picker.json
@@ -0,0 +1,36 @@
+{
+ "resourceType": "Questionnaire",
+ "item": [
+ {
+ "linkId": "1",
+ "text": "Enter a time",
+ "type": "time",
+ "extension": [
+ {
+ "url": "http://hl7.org/fhir/StructureDefinition/entryFormat",
+ "valueString": "hh-mm"
+ }
+ ],
+ "item": [
+ {
+ "extension": [
+ {
+ "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
+ "valueCodeableConcept": {
+ "coding": [
+ {
+ "system": "http://hl7.org/fhir/questionnaire-display-category",
+ "code": "instructions"
+ }
+ ]
+ }
+ }
+ ],
+ "linkId": "1-most-recent",
+ "text": "Use keyboard entry or time picker",
+ "type": "display"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/catalog/src/main/assets/component_time_picker_with_validation.json b/catalog/src/main/assets/component_time_picker_with_validation.json
new file mode 100644
index 0000000000..198a8f5bea
--- /dev/null
+++ b/catalog/src/main/assets/component_time_picker_with_validation.json
@@ -0,0 +1,37 @@
+{
+ "resourceType": "Questionnaire",
+ "item": [
+ {
+ "linkId": "1",
+ "text": "Enter a time",
+ "type": "time",
+ "required": true,
+ "extension": [
+ {
+ "url": "http://hl7.org/fhir/StructureDefinition/entryFormat",
+ "valueString": "hh-mm"
+ }
+ ],
+ "item": [
+ {
+ "extension": [
+ {
+ "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
+ "valueCodeableConcept": {
+ "coding": [
+ {
+ "system": "http://hl7.org/fhir/questionnaire-display-category",
+ "code": "instructions"
+ }
+ ]
+ }
+ }
+ ],
+ "linkId": "1-most-recent",
+ "text": "Use keyboard entry or time picker",
+ "type": "display"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt
index d9e4637ada..00f541e5c6 100644
--- a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt
+++ b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt
@@ -102,6 +102,12 @@ class ComponentListViewModel(application: Application, private val state: SavedS
"component_date_picker.json",
"component_date_picker_with_validation.json",
),
+ TIME_PICKER(
+ R.drawable.ic_timepicker,
+ R.string.component_name_time_picker,
+ "component_time_picker.json",
+ "component_time_picker_with_validation.json",
+ ),
DATE_TIME_PICKER(
R.drawable.ic_timepicker,
R.string.component_name_date_time_picker,
@@ -171,6 +177,7 @@ class ComponentListViewModel(application: Application, private val state: SavedS
ViewItem.ComponentItem(Component.TEXT_FIELD),
ViewItem.ComponentItem(Component.AUTO_COMPLETE),
ViewItem.ComponentItem(Component.DATE_PICKER),
+ ViewItem.ComponentItem(Component.TIME_PICKER),
ViewItem.ComponentItem(Component.DATE_TIME_PICKER),
ViewItem.ComponentItem(Component.SLIDER),
ViewItem.ComponentItem(Component.QUANTITY),
diff --git a/catalog/src/main/res/values/strings.xml b/catalog/src/main/res/values/strings.xml
index ae47067784..69b5f26fa8 100644
--- a/catalog/src/main/res/values/strings.xml
+++ b/catalog/src/main/res/values/strings.xml
@@ -28,6 +28,7 @@
Text field
Auto Complete
Date picker
+ Time picker
DateTime picker
Slider
Quantity
diff --git a/codelabs/datacapture/README.md b/codelabs/datacapture/README.md
index e9a7f9e196..af44e32e18 100644
--- a/codelabs/datacapture/README.md
+++ b/codelabs/datacapture/README.md
@@ -76,8 +76,8 @@ of the `app/build.gradle.kts` file of your project:
dependencies {
// ...
- implementation("com.google.android.fhir:data-capture:1.0.0")
- implementation("androidx.fragment:fragment-ktx:1.5.5")
+ implementation("com.google.android.fhir:data-capture:1.2.0")
+ implementation("androidx.fragment:fragment-ktx:1.6.0")
}
```
@@ -150,11 +150,11 @@ Open `MainActivity.kt` and add the following code to the `MainActivity` class:
```kotlin
// Step 2: Configure a QuestionnaireFragment
-val questionnaireJsonString = getStringFromAssets("questionnaire.json")
+questionnaireJsonString = getStringFromAssets("questionnaire.json")
+
+val questionnaireFragment =
+ QuestionnaireFragment.builder().setQuestionnaire(questionnaireJsonString!!).build()
-val questionnaireParams = bundleOf(
-QuestionnaireFragment.EXTRA_QUESTIONNAIRE_JSON_STRING to questionnaireJsonString
-)
```
### Step 3: Add the QuestionnaireFragment to the FragmentContainerView
@@ -168,10 +168,10 @@ Add the following code to the `MainActivity` class:
```kotlin
// Step 3: Add the QuestionnaireFragment to the FragmentContainerView
if (savedInstanceState == null) {
- supportFragmentManager.commit {
- setReorderingAllowed(true)
- add(R.id.fragment_container_view, args = questionnaireParams)
- }
+ supportFragmentManager.commit {
+ setReorderingAllowed(true)
+ add(R.id.fragment_container_view, questionnaireFragment)
+ }
}
// Submit button callback
supportFragmentManager.setFragmentResultListener(
diff --git a/codelabs/engine/README.md b/codelabs/engine/README.md
index 67bfd6fabc..f0fc5b8ddf 100644
--- a/codelabs/engine/README.md
+++ b/codelabs/engine/README.md
@@ -125,7 +125,7 @@ file of your project:
dependencies {
// ...
- implementation("com.google.android.fhir:engine:1.0.0")
+ implementation("com.google.android.fhir:engine:1.1.0")
}
```
@@ -257,7 +257,13 @@ outlined below will guide you through the process.
override fun getFhirEngine() = FhirApplication.fhirEngine(applicationContext)
- override fun getUploadStrategy() = UploadStrategy.AllChangesSquashedBundlePut
+ override fun getUploadStrategy() =
+ UploadStrategy.forBundleRequest(
+ methodForCreate = HttpCreateMethod.PUT,
+ methodForUpdate = HttpUpdateMethod.PATCH,
+ squash = true,
+ bundleSize = 500,
+ )
}
```
diff --git a/codelabs/engine/app/build.gradle.kts b/codelabs/engine/app/build.gradle.kts
index 23c48e875f..b682babd86 100644
--- a/codelabs/engine/app/build.gradle.kts
+++ b/codelabs/engine/app/build.gradle.kts
@@ -49,6 +49,6 @@ dependencies {
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- implementation("com.google.android.fhir:engine:1.0.0")
+ implementation("com.google.android.fhir:engine:1.1.0")
implementation("androidx.fragment:fragment-ktx:1.8.3")
}
diff --git a/codelabs/engine/app/src/main/java/com/google/android/fhir/codelabs/engine/PatientItemViewHolder.kt b/codelabs/engine/app/src/main/java/com/google/android/fhir/codelabs/engine/PatientItemViewHolder.kt
index 409441019f..6b115142a0 100644
--- a/codelabs/engine/app/src/main/java/com/google/android/fhir/codelabs/engine/PatientItemViewHolder.kt
+++ b/codelabs/engine/app/src/main/java/com/google/android/fhir/codelabs/engine/PatientItemViewHolder.kt
@@ -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.
@@ -30,8 +30,10 @@ class PatientItemViewHolder(binding: PatientListItemViewBinding) :
fun bind(patientItem: Patient) {
nameTextView.text =
- patientItem.name.first().let { it.given.joinToString(separator = " ") + " " + it.family }
- genderTextView.text = patientItem.gender.display
+ patientItem.name.firstOrNull()?.let {
+ it.given.joinToString(separator = " ") + " " + it.family
+ }
+ genderTextView.text = patientItem.gender?.display
cityTextView.text = patientItem.address.singleOrNull()?.city
}
}
diff --git a/contrib/barcode/build.gradle.kts b/contrib/barcode/build.gradle.kts
index 426cd37624..d731f7819b 100644
--- a/contrib/barcode/build.gradle.kts
+++ b/contrib/barcode/build.gradle.kts
@@ -70,11 +70,11 @@ dependencies {
implementation(Dependencies.Mlkit.barcodeScanning)
implementation(Dependencies.Mlkit.objectDetection)
implementation(Dependencies.Mlkit.objectDetectionCustom)
- implementation(Dependencies.material)
implementation(Dependencies.timber)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core)
implementation(libs.androidx.fragment)
+ implementation(libs.material)
testImplementation(Dependencies.mockitoInline)
testImplementation(Dependencies.mockitoKotlin)
diff --git a/contrib/locationwidget/build.gradle.kts b/contrib/locationwidget/build.gradle.kts
index dc52ab95a7..0b2148f79c 100644
--- a/contrib/locationwidget/build.gradle.kts
+++ b/contrib/locationwidget/build.gradle.kts
@@ -65,12 +65,12 @@ dependencies {
implementation(project(":datacapture"))
implementation(Dependencies.playServicesLocation)
- implementation(Dependencies.material)
implementation(Dependencies.timber)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core)
implementation(libs.androidx.fragment)
implementation(libs.kotlinx.coroutines.playservices)
+ implementation(libs.material)
coreLibraryDesugaring(Dependencies.desugarJdkLibs)
diff --git a/contrib/locationwidget/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/locationwidget/LocationGpsCoordinateViewHolderFactoryInstrumentedTest.kt b/contrib/locationwidget/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/locationwidget/LocationGpsCoordinateViewHolderFactoryInstrumentedTest.kt
index b91eec759f..bdf795eac5 100644
--- a/contrib/locationwidget/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/locationwidget/LocationGpsCoordinateViewHolderFactoryInstrumentedTest.kt
+++ b/contrib/locationwidget/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/locationwidget/LocationGpsCoordinateViewHolderFactoryInstrumentedTest.kt
@@ -27,6 +27,7 @@ import androidx.core.view.isVisible
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
+import org.hl7.fhir.r4.model.Questionnaire
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -55,4 +56,30 @@ class LocationGpsCoordinateViewHolderFactoryInstrumentedTest {
)
.isEqualTo(InputType.TYPE_NULL)
}
+
+ @Test
+ fun matcherShouldReturnTrueForOriginalGpsCoordinateUrl() {
+ val questionnaireItem = Questionnaire.QuestionnaireItemComponent()
+ questionnaireItem.addExtension(
+ LocationGpsCoordinateViewHolderFactory.PRIMARY_GPS_COORDINATE_EXTENSION_URL,
+ null,
+ )
+ assertThat(LocationGpsCoordinateViewHolderFactory.matcher(questionnaireItem)).isTrue()
+ }
+
+ @Test
+ fun matcherShouldReturnTrueForOldGpsCoordinateUrl() {
+ val questionnaireItem = Questionnaire.QuestionnaireItemComponent()
+ questionnaireItem.addExtension(
+ LocationGpsCoordinateViewHolderFactory.GPS_COORDINATE_EXTENSION_URL,
+ null,
+ )
+ assertThat(LocationGpsCoordinateViewHolderFactory.matcher(questionnaireItem)).isTrue()
+ }
+
+ @Test
+ fun matcherShouldReturnFalseForNoGpsCoordinateUrl() {
+ val questionnaireItem = Questionnaire.QuestionnaireItemComponent()
+ assertThat(LocationGpsCoordinateViewHolderFactory.matcher(questionnaireItem)).isFalse()
+ }
}
diff --git a/contrib/locationwidget/src/main/java/com/google/android/fhir/datacapture/contrib/views/locationwidget/LocationGpsCoordinateViewHolderFactory.kt b/contrib/locationwidget/src/main/java/com/google/android/fhir/datacapture/contrib/views/locationwidget/LocationGpsCoordinateViewHolderFactory.kt
index 18769f6166..a12b87dc69 100644
--- a/contrib/locationwidget/src/main/java/com/google/android/fhir/datacapture/contrib/views/locationwidget/LocationGpsCoordinateViewHolderFactory.kt
+++ b/contrib/locationwidget/src/main/java/com/google/android/fhir/datacapture/contrib/views/locationwidget/LocationGpsCoordinateViewHolderFactory.kt
@@ -59,8 +59,11 @@ object LocationGpsCoordinateViewHolderFactory :
header.context.tryUnwrapContext()?.apply {
val gpsCoordinateExtensionValue =
questionnaireViewItem.questionnaireItem
- .getExtensionByUrl(GPS_COORDINATE_EXTENSION_URL)
- .value as StringType
+ .getExtensionByUrl(PRIMARY_GPS_COORDINATE_EXTENSION_URL)
+ ?.value as? StringType
+ ?: questionnaireViewItem.questionnaireItem
+ .getExtensionByUrl(GPS_COORDINATE_EXTENSION_URL)
+ .value as StringType
when (gpsCoordinateExtensionValue.valueAsString) {
GPS_COORDINATE_EXTENSION_VALUE_LATITUDE -> {
supportFragmentManager.setFragmentResultListener(
@@ -148,9 +151,12 @@ object LocationGpsCoordinateViewHolderFactory :
}
fun matcher(questionnaireItem: Questionnaire.QuestionnaireItemComponent): Boolean {
- return questionnaireItem.hasExtension(GPS_COORDINATE_EXTENSION_URL)
+ return questionnaireItem.hasExtension(PRIMARY_GPS_COORDINATE_EXTENSION_URL) ||
+ questionnaireItem.hasExtension(GPS_COORDINATE_EXTENSION_URL)
}
+ const val PRIMARY_GPS_COORDINATE_EXTENSION_URL =
+ "https://github.com/google/android-fhir/StructureDefinition/gps-coordinate"
const val GPS_COORDINATE_EXTENSION_URL = "gps-coordinate"
const val GPS_COORDINATE_EXTENSION_VALUE_LATITUDE = "latitude"
const val GPS_COORDINATE_EXTENSION_VALUE_LONGITUDE = "longitude"
diff --git a/datacapture/build.gradle.kts b/datacapture/build.gradle.kts
index 34d81ab04c..b3c1e12d92 100644
--- a/datacapture/build.gradle.kts
+++ b/datacapture/build.gradle.kts
@@ -90,7 +90,6 @@ dependencies {
exclude(module = "commons-logging")
exclude(module = "httpclient")
}
- implementation(Dependencies.material)
implementation(Dependencies.timber)
implementation(libs.android.fhir.common)
implementation(libs.androidx.appcompat)
@@ -101,6 +100,7 @@ dependencies {
implementation(libs.glide)
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.core)
+ implementation(libs.material)
testImplementation(Dependencies.mockitoInline)
testImplementation(Dependencies.mockitoKotlin)
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt
index 2b2ea328cf..6fc427eae2 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt
@@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.datacapture.contrib.views.PhoneNumberViewHolderFactory
import com.google.android.fhir.datacapture.extensions.inflate
import com.google.android.fhir.datacapture.extensions.itemControl
+import com.google.android.fhir.datacapture.extensions.shouldUseDialog
import com.google.android.fhir.datacapture.views.NavigationViewHolder
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
import com.google.android.fhir.datacapture.views.factories.AttachmentViewHolderFactory
@@ -46,6 +47,7 @@ import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemView
import com.google.android.fhir.datacapture.views.factories.RadioGroupViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.RepeatedGroupHeaderItemViewHolder
import com.google.android.fhir.datacapture.views.factories.SliderViewHolderFactory
+import com.google.android.fhir.datacapture.views.factories.TimePickerViewHolderFactory
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType
internal class QuestionnaireEditAdapter(
@@ -102,6 +104,7 @@ internal class QuestionnaireEditAdapter(
QuestionnaireViewHolderType.GROUP -> GroupViewHolderFactory
QuestionnaireViewHolderType.BOOLEAN_TYPE_PICKER -> BooleanChoiceViewHolderFactory
QuestionnaireViewHolderType.DATE_PICKER -> DatePickerViewHolderFactory
+ QuestionnaireViewHolderType.TIME_PICKER -> TimePickerViewHolderFactory
QuestionnaireViewHolderType.DATE_TIME_PICKER -> DateTimePickerViewHolderFactory
QuestionnaireViewHolderType.EDIT_TEXT_SINGLE_LINE -> EditTextSingleLineViewHolderFactory
QuestionnaireViewHolderType.EDIT_TEXT_MULTI_LINE -> EditTextMultiLineViewHolderFactory
@@ -222,6 +225,7 @@ internal class QuestionnaireEditAdapter(
QuestionnaireItemType.GROUP -> QuestionnaireViewHolderType.GROUP
QuestionnaireItemType.BOOLEAN -> QuestionnaireViewHolderType.BOOLEAN_TYPE_PICKER
QuestionnaireItemType.DATE -> QuestionnaireViewHolderType.DATE_PICKER
+ QuestionnaireItemType.TIME -> QuestionnaireViewHolderType.TIME_PICKER
QuestionnaireItemType.DATETIME -> QuestionnaireViewHolderType.DATE_TIME_PICKER
QuestionnaireItemType.STRING -> getStringViewHolderType(questionnaireViewItem)
QuestionnaireItemType.TEXT -> QuestionnaireViewHolderType.EDIT_TEXT_MULTI_LINE
@@ -241,8 +245,11 @@ internal class QuestionnaireEditAdapter(
): QuestionnaireViewHolderType {
val questionnaireItem = questionnaireViewItem.questionnaireItem
- // Use the view type that the client wants if they specified an itemControl
- return questionnaireItem.itemControl?.viewHolderType
+ // Use the view type that the client wants if they specified an itemControl or dialog extension
+ return when {
+ questionnaireItem.shouldUseDialog -> QuestionnaireViewHolderType.DIALOG_SELECT
+ else -> questionnaireItem.itemControl?.viewHolderType
+ }
// Otherwise, choose a sensible UI element automatically
?: run {
val numOptions = questionnaireViewItem.enabledAnswerOptions.size
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderType.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderType.kt
index 5a64806841..d9442a652a 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderType.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderType.kt
@@ -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.
@@ -45,6 +45,7 @@ enum class QuestionnaireViewHolderType(val value: Int) {
SLIDER(15),
PHONE_NUMBER(16),
ATTACHMENT(17),
+ TIME_PICKER(18),
;
companion object {
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt
index 40267c3da0..40572b8c63 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt
@@ -46,6 +46,7 @@ import com.google.android.fhir.datacapture.extensions.isHelpCode
import com.google.android.fhir.datacapture.extensions.isHidden
import com.google.android.fhir.datacapture.extensions.isPaginated
import com.google.android.fhir.datacapture.extensions.isRepeatedGroup
+import com.google.android.fhir.datacapture.extensions.launchTimestamp
import com.google.android.fhir.datacapture.extensions.localizedTextSpanned
import com.google.android.fhir.datacapture.extensions.maxValue
import com.google.android.fhir.datacapture.extensions.maxValueCqfCalculatedValueExpression
@@ -68,6 +69,7 @@ import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.datacapture.validation.ValidationResult
import com.google.android.fhir.datacapture.views.QuestionTextConfiguration
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
+import java.util.Date
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -79,6 +81,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.withIndex
import kotlinx.coroutines.launch
+import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse
@@ -165,6 +168,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
.forEach { questionnaireResponse.addItem(it.createQuestionnaireResponseItem()) }
}
}
+ // Add extension for questionnaire launch time stamp
+ questionnaireResponse.launchTimestamp = DateTimeType(Date())
questionnaireResponse.packRepeatedGroups(questionnaire)
}
@@ -500,6 +505,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
)
.map { it.copy() }
unpackRepeatedGroups(this@QuestionnaireViewModel.questionnaire)
+ // Use authored as a submission time stamp
+ authored = Date()
}
}
@@ -1233,7 +1240,7 @@ internal data class QuestionnairePage(
)
internal val QuestionnairePagination.hasPreviousPage: Boolean
- get() = pages.any { it.index < currentPageIndex && it.enabled }
+ get() = pages.any { it.index < currentPageIndex && it.enabled && !it.hidden }
internal val QuestionnairePagination.hasNextPage: Boolean
- get() = pages.any { it.index > currentPageIndex && it.enabled }
+ get() = pages.any { it.index > currentPageIndex && it.enabled && !it.hidden }
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreHeaderViews.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreHeaderViews.kt
index fd4690ea26..84ffef9247 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreHeaderViews.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreHeaderViews.kt
@@ -19,6 +19,7 @@ package com.google.android.fhir.datacapture.extensions
import android.content.Context
import android.text.SpannableStringBuilder
import android.text.Spanned
+import android.text.method.LinkMovementMethod
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.Button
@@ -82,7 +83,10 @@ fun initHelpViews(
}
}
}
- helpTextView.updateTextAndVisibility(questionnaireItem.localizedHelpSpanned)
+ helpTextView.apply {
+ updateTextAndVisibility(questionnaireItem.localizedHelpSpanned)
+ movementMethod = LinkMovementMethod.getInstance()
+ }
}
/**
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt
index 800660db9f..94c2e56738 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt
@@ -61,6 +61,9 @@ internal const val EXTENSION_ITEM_CONTROL_URL_ANDROID_FHIR =
internal const val EXTENSION_ITEM_CONTROL_SYSTEM_ANDROID_FHIR =
"https://github.com/google/android-fhir/questionnaire-item-control"
+internal const val EXTENSION_DIALOG_URL_ANDROID_FHIR =
+ "https://github.com/google/android-fhir/StructureDefinition/dialog"
+
internal enum class StyleUrl(val url: String) {
BASE("https://github.com/google/android-fhir/tree/master/datacapture/android-style"),
PREFIX_TEXT_VIEW("prefix_text_view"),
@@ -237,6 +240,15 @@ val QuestionnaireItemComponent.itemControlCode: String?
val QuestionnaireItemComponent.itemControl: ItemControlTypes?
get() = ItemControlTypes.values().firstOrNull { it.extensionCode == itemControlCode }
+private val QuestionnaireItemComponent.hasDialogExtension: Boolean
+ get() = this.extension.any { it.url == EXTENSION_DIALOG_URL_ANDROID_FHIR }
+
+val QuestionnaireItemComponent.shouldUseDialog: Boolean
+ get() =
+ this.hasDialogExtension &&
+ (this.itemControl?.viewHolderType == QuestionnaireViewHolderType.CHECK_BOX_GROUP ||
+ this.itemControl?.viewHolderType == QuestionnaireViewHolderType.RADIO_GROUP)
+
/**
* The desired orientation for the list of choices.
*
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireResponses.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireResponses.kt
index fa0f4c545d..e560798035 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireResponses.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireResponses.kt
@@ -16,9 +16,14 @@
package com.google.android.fhir.datacapture.extensions
+import org.hl7.fhir.r4.model.DateTimeType
+import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
+internal const val EXTENSION_LAST_LAUNCHED_TIMESTAMP: String =
+ "http://github.com/google-android/questionnaire-lastLaunched-timestamp"
+
/** Pre-order list of all questionnaire response items in the questionnaire. */
val QuestionnaireResponse.allItems: List
get() = item.flatMap { it.descendant }
@@ -67,16 +72,8 @@ private fun List.packR
val questionnaireItem = questionnaireItems.single()
- questionnaireResponseItems.forEach { it ->
- if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP) {
- if (questionnaireItem.repeats) {
- it.answer.forEach { it.item = it.item.packRepeatedGroups(questionnaireItem.item) }
- } else {
- it.item = it.item.packRepeatedGroups(questionnaireItem.item)
- }
- } else {
- it.answer.forEach { it.item = it.item.packRepeatedGroups(questionnaireItem.item) }
- }
+ questionnaireResponseItems.forEach {
+ it.item = it.item.packRepeatedGroups(questionnaireItem.item)
}
if (
@@ -154,3 +151,20 @@ private fun unpackRepeatedGroups(
listOf(questionnaireResponseItem)
}
}
+
+/**
+ * Adds a launch timestamp extension to the Questionnaire Response. If the extension @see
+ * EXTENSION_LAUNCH_TIMESTAMP already exists, it updates its value; otherwise, it adds a new one.
+ */
+internal var QuestionnaireResponse.launchTimestamp: DateTimeType?
+ get() {
+ val extension = this.extension.firstOrNull { it.url == EXTENSION_LAST_LAUNCHED_TIMESTAMP }
+ return extension?.value as? DateTimeType
+ }
+ set(value) {
+ extension.find { it.url == EXTENSION_LAST_LAUNCHED_TIMESTAMP }?.setValue(value)
+ ?: run {
+ // Add a new extension if none exists
+ extension.add(Extension(EXTENSION_LAST_LAUNCHED_TIMESTAMP, value))
+ }
+ }
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreTypes.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreTypes.kt
index bf6a2b00bd..c51bfb8413 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreTypes.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreTypes.kt
@@ -17,6 +17,7 @@
package com.google.android.fhir.datacapture.extensions
import android.content.Context
+import android.text.Spanned
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.views.factories.localDate
import com.google.android.fhir.datacapture.views.factories.localTime
@@ -57,6 +58,8 @@ fun Type.asStringValue(): String {
fun Type.displayString(context: Context): String =
getDisplayString(this, context) ?: context.getString(R.string.not_answered)
+fun Type.displayStringSpanned(context: Context): Spanned = displayString(context).toSpanned()
+
/** Returns value as string depending on the [Type] of element. */
fun Type.getValueAsString(context: Context): String =
getValueString(this) ?: context.getString(R.string.not_answered)
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/GroupHeaderView.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/GroupHeaderView.kt
index da85cbac4e..e2d1872bc4 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/GroupHeaderView.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/GroupHeaderView.kt
@@ -17,6 +17,7 @@
package com.google.android.fhir.datacapture.views
import android.content.Context
+import android.text.method.LinkMovementMethod
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
@@ -55,11 +56,17 @@ class GroupHeaderView(context: Context, attrs: AttributeSet?) : LinearLayout(con
helpCardStateChangedCallback = questionnaireViewItem.helpCardStateChangedCallback,
)
prefix.updateTextAndVisibility(questionnaireViewItem.questionnaireItem.localizedPrefixSpanned)
- // CQF expression takes precedence over static question text
- question.updateTextAndVisibility(questionnaireViewItem.questionText)
- hint.updateTextAndVisibility(
- questionnaireViewItem.enabledDisplayItems.getLocalizedInstructionsSpanned(),
- )
+ question.apply {
+ // CQF expression takes precedence over static question text
+ updateTextAndVisibility(questionnaireViewItem.questionText)
+ movementMethod = LinkMovementMethod.getInstance()
+ }
+ hint.apply {
+ updateTextAndVisibility(
+ questionnaireViewItem.enabledDisplayItems.getLocalizedInstructionsSpanned(),
+ )
+ movementMethod = LinkMovementMethod.getInstance()
+ }
visibility = getHeaderViewVisibility(prefix, question, hint)
applyCustomOrDefaultStyle(
questionnaireViewItem.questionnaireItem,
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/HeaderView.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/HeaderView.kt
index 7e5e77231d..b723190448 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/HeaderView.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/HeaderView.kt
@@ -17,6 +17,7 @@
package com.google.android.fhir.datacapture.views
import android.content.Context
+import android.text.method.LinkMovementMethod
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
@@ -55,13 +56,17 @@ class HeaderView(context: Context, attrs: AttributeSet?) : LinearLayout(context,
helpCardStateChangedCallback = questionnaireViewItem.helpCardStateChangedCallback,
)
prefix.updateTextAndVisibility(questionnaireViewItem.questionnaireItem.localizedPrefixSpanned)
- // CQF expression takes precedence over static question text
- question.updateTextAndVisibility(
- appendAsteriskToQuestionText(question.context, questionnaireViewItem),
- )
- hint.updateTextAndVisibility(
- questionnaireViewItem.enabledDisplayItems.getLocalizedInstructionsSpanned(),
- )
+ question.apply {
+ // CQF expression takes precedence over static question text
+ updateTextAndVisibility(appendAsteriskToQuestionText(question.context, questionnaireViewItem))
+ movementMethod = LinkMovementMethod.getInstance()
+ }
+ hint.apply {
+ updateTextAndVisibility(
+ questionnaireViewItem.enabledDisplayItems.getLocalizedInstructionsSpanned(),
+ )
+ movementMethod = LinkMovementMethod.getInstance()
+ }
// Make the entire view GONE if there is nothing to show. This is to avoid an empty row in the
// questionnaire.
visibility = getHeaderViewVisibility(prefix, question, hint)
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/OptionSelectDialogFragment.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/OptionSelectDialogFragment.kt
index 4fb0238ca0..ab40dc3eb1 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/OptionSelectDialogFragment.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/OptionSelectDialogFragment.kt
@@ -41,6 +41,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.itemAnswerOptionImage
import com.google.android.fhir.datacapture.extensions.optionExclusive
+import com.google.android.fhir.datacapture.extensions.toSpanned
import com.google.android.fhir.datacapture.views.factories.OptionSelectOption
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemDialogSelectViewModel
import com.google.android.fhir.datacapture.views.factories.SelectedOptions
@@ -208,7 +209,7 @@ private class OptionSelectAdapter(val multiSelectEnabled: Boolean) :
} else {
(holder as OptionSelectViewHolder.OptionSingle).radioButton
}
- compoundButton.text = item.option.displayString
+ compoundButton.text = item.option.displayString.toSpanned()
compoundButton.setCompoundDrawablesRelative(
item.option.item.itemAnswerOptionImage(compoundButton.context),
null,
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/CheckBoxGroupViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/CheckBoxGroupViewHolderFactory.kt
index 6e41ca6b9f..179eb7da79 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/CheckBoxGroupViewHolderFactory.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/CheckBoxGroupViewHolderFactory.kt
@@ -27,7 +27,7 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.ChoiceOrientationTypes
import com.google.android.fhir.datacapture.extensions.choiceOrientation
-import com.google.android.fhir.datacapture.extensions.displayString
+import com.google.android.fhir.datacapture.extensions.displayStringSpanned
import com.google.android.fhir.datacapture.extensions.itemAnswerOptionImage
import com.google.android.fhir.datacapture.extensions.optionExclusive
import com.google.android.fhir.datacapture.extensions.tryUnwrapContext
@@ -104,7 +104,7 @@ internal object CheckBoxGroupViewHolderFactory :
val checkbox =
checkboxLayout.findViewById(R.id.check_box).apply {
id = viewId
- text = answerOption.value.displayString(header.context)
+ text = answerOption.value.displayStringSpanned(header.context)
setCompoundDrawablesRelative(
answerOption.itemAnswerOptionImage(checkboxGroup.context),
null,
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DialogSelectViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DialogSelectViewHolderFactory.kt
index 85769b3679..206a4b77e2 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DialogSelectViewHolderFactory.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DialogSelectViewHolderFactory.kt
@@ -32,6 +32,7 @@ import com.google.android.fhir.datacapture.extensions.getRequiredOrOptionalText
import com.google.android.fhir.datacapture.extensions.getValidationErrorMessage
import com.google.android.fhir.datacapture.extensions.itemControl
import com.google.android.fhir.datacapture.extensions.localizedFlyoverSpanned
+import com.google.android.fhir.datacapture.extensions.toSpanned
import com.google.android.fhir.datacapture.extensions.tryUnwrapContext
import com.google.android.fhir.datacapture.validation.ValidationResult
import com.google.android.fhir.datacapture.views.HeaderView
@@ -76,12 +77,12 @@ internal object QuestionnaireItemDialogSelectViewHolderFactory :
val questionnaireItem = questionnaireViewItem.questionnaireItem
val selectedOptions = questionnaireViewItem.extractInitialOptions(holder.header.context)
- holder.summary.text = selectedOptions.selectedSummary
+ holder.summary.text = selectedOptions.selectedSummary.toSpanned()
selectedOptionsJob =
activity.lifecycleScope.launch {
// Listen for changes to selected options to update summary + FHIR data model
viewModel.getSelectedOptionsFlow(questionnaireItem.linkId).collect { selectedOptions ->
- holder.summary.text = selectedOptions.selectedSummary
+ holder.summary.text = selectedOptions.selectedSummary.toSpanned()
updateAnswers(selectedOptions)
}
}
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactory.kt
index 1a3c581f1c..8bcfc54e31 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactory.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactory.kt
@@ -18,6 +18,7 @@ package com.google.android.fhir.datacapture.views.factories
import android.content.Context
import android.graphics.drawable.Drawable
+import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -33,6 +34,7 @@ import com.google.android.fhir.datacapture.extensions.getValidationErrorMessage
import com.google.android.fhir.datacapture.extensions.identifierString
import com.google.android.fhir.datacapture.extensions.itemAnswerOptionImage
import com.google.android.fhir.datacapture.extensions.localizedFlyoverSpanned
+import com.google.android.fhir.datacapture.extensions.toSpanned
import com.google.android.fhir.datacapture.extensions.tryUnwrapContext
import com.google.android.fhir.datacapture.validation.ValidationResult
import com.google.android.fhir.datacapture.views.HeaderView
@@ -92,8 +94,8 @@ internal object DropDownViewHolderFactory :
answerOptionList
.firstOrNull { it.answerId == selectedAnswerIdentifier }
?.let {
- autoCompleteTextView.setText(it.answerOptionString)
- autoCompleteTextView.setSelection(it.answerOptionString.length)
+ autoCompleteTextView.setText(it.answerOptionStringSpanned())
+ autoCompleteTextView.setSelection(it.answerOptionStringSpanned().length)
autoCompleteTextView.setCompoundDrawablesRelative(
it.answerOptionImage,
null,
@@ -105,7 +107,7 @@ internal object DropDownViewHolderFactory :
autoCompleteTextView.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ ->
val selectedItem = adapter.getItem(position)
- autoCompleteTextView.setText(selectedItem?.answerOptionString, false)
+ autoCompleteTextView.setText(selectedItem?.answerOptionStringSpanned(), false)
autoCompleteTextView.setCompoundDrawablesRelative(
adapter.getItem(position)?.answerOptionImage,
null,
@@ -165,7 +167,7 @@ internal class AnswerOptionDropDownArrayAdapter(
val answerOption: DropDownAnswerOption? = getItem(position)
val answerOptionTextView =
listItemView?.findViewById(R.id.answer_option_textview) as TextView
- answerOptionTextView.text = answerOption?.answerOptionString
+ answerOptionTextView.text = answerOption?.answerOptionStringSpanned()
answerOptionTextView.setCompoundDrawablesRelative(
answerOption?.answerOptionImage,
null,
@@ -187,4 +189,6 @@ internal data class DropDownAnswerOption(
override fun toString(): String {
return this.answerOptionString
}
+
+ fun answerOptionStringSpanned(): Spanned = answerOptionString.toSpanned()
}
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/RadioGroupViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/RadioGroupViewHolderFactory.kt
index 876a4f3ac6..f912560425 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/RadioGroupViewHolderFactory.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/RadioGroupViewHolderFactory.kt
@@ -28,7 +28,7 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.ChoiceOrientationTypes
import com.google.android.fhir.datacapture.extensions.choiceOrientation
-import com.google.android.fhir.datacapture.extensions.displayString
+import com.google.android.fhir.datacapture.extensions.displayStringSpanned
import com.google.android.fhir.datacapture.extensions.itemAnswerOptionImage
import com.google.android.fhir.datacapture.extensions.tryUnwrapContext
import com.google.android.fhir.datacapture.validation.Invalid
@@ -113,7 +113,7 @@ internal object RadioGroupViewHolderFactory :
val radioButton =
radioButtonItem.findViewById(R.id.radio_button).apply {
id = viewId
- text = answerOption.value.displayString(header.context)
+ text = answerOption.value.displayStringSpanned(header.context)
setCompoundDrawablesRelative(
answerOption.itemAnswerOptionImage(radioGroup.context),
null,
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/ReviewViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/ReviewViewHolderFactory.kt
index a6621b2aba..cb69258760 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/ReviewViewHolderFactory.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/ReviewViewHolderFactory.kt
@@ -26,6 +26,7 @@ import com.google.android.fhir.datacapture.extensions.getHeaderViewVisibility
import com.google.android.fhir.datacapture.extensions.getLocalizedInstructionsSpanned
import com.google.android.fhir.datacapture.extensions.localizedFlyoverSpanned
import com.google.android.fhir.datacapture.extensions.localizedPrefixSpanned
+import com.google.android.fhir.datacapture.extensions.toSpanned
import com.google.android.fhir.datacapture.extensions.updateTextAndVisibility
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
@@ -90,7 +91,7 @@ internal object ReviewViewHolderFactory : QuestionnaireItemViewHolderFactory(R.l
answerView.visibility = GONE
}
else -> {
- answerView.text = questionnaireViewItem.answerString(answerView.context)
+ answerView.text = questionnaireViewItem.answerString(answerView.context).toSpanned()
answerView.visibility = VISIBLE
if (questionnaireViewItem.validationResult is Invalid) {
errorView.findViewById(R.id.error_text_view).text =
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/TimePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/TimePickerViewHolderFactory.kt
new file mode 100644
index 0000000000..91062b1de8
--- /dev/null
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/TimePickerViewHolderFactory.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 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.
+ * 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 com.google.android.fhir.datacapture.views.factories
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.text.InputType
+import android.text.format.DateFormat
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import com.google.android.fhir.datacapture.R
+import com.google.android.fhir.datacapture.extensions.getRequiredOrOptionalText
+import com.google.android.fhir.datacapture.extensions.toLocalizedString
+import com.google.android.fhir.datacapture.extensions.tryUnwrapContext
+import com.google.android.fhir.datacapture.views.HeaderView
+import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
+import com.google.android.material.textfield.TextInputEditText
+import com.google.android.material.textfield.TextInputLayout
+import com.google.android.material.timepicker.MaterialTimePicker
+import com.google.android.material.timepicker.MaterialTimePicker.INPUT_MODE_CLOCK
+import com.google.android.material.timepicker.MaterialTimePicker.INPUT_MODE_KEYBOARD
+import com.google.android.material.timepicker.TimeFormat
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+import kotlinx.coroutines.launch
+import org.hl7.fhir.r4.model.QuestionnaireResponse
+import org.hl7.fhir.r4.model.TimeType
+
+object TimePickerViewHolderFactory : QuestionnaireItemViewHolderFactory(R.layout.time_picker_view) {
+
+ override fun getQuestionnaireItemViewHolderDelegate() =
+ object : QuestionnaireItemViewHolderDelegate {
+ private val TAG = "time-picker"
+ private lateinit var context: AppCompatActivity
+ private lateinit var header: HeaderView
+ private lateinit var timeInputLayout: TextInputLayout
+ private lateinit var timeInputEditText: TextInputEditText
+ override lateinit var questionnaireViewItem: QuestionnaireViewItem
+
+ override fun init(itemView: View) {
+ context = itemView.context.tryUnwrapContext()!!
+ header = itemView.findViewById(R.id.header)
+ timeInputLayout = itemView.findViewById(R.id.text_input_layout)
+ timeInputEditText = itemView.findViewById(R.id.text_input_edit_text)
+ timeInputEditText.inputType = InputType.TYPE_NULL
+ timeInputEditText.hint = itemView.context.getString(R.string.time)
+
+ timeInputLayout.setEndIconOnClickListener {
+ // The application is wrapped in a ContextThemeWrapper in QuestionnaireFragment
+ // and again in TextInputEditText during layout inflation. As a result, it is
+ // necessary to access the base context twice to retrieve the application object
+ // from the view's context.
+ val context = itemView.context.tryUnwrapContext()!!
+ buildMaterialTimePicker(context, INPUT_MODE_CLOCK)
+ }
+ timeInputEditText.setOnClickListener {
+ buildMaterialTimePicker(itemView.context, INPUT_MODE_KEYBOARD)
+ }
+ }
+
+ @SuppressLint("NewApi") // java.time APIs can be used due to desugaring
+ override fun bind(questionnaireViewItem: QuestionnaireViewItem) {
+ clearPreviousState()
+ header.bind(questionnaireViewItem)
+ timeInputLayout.helperText = getRequiredOrOptionalText(questionnaireViewItem, context)
+
+ val questionnaireItemViewItemDateTimeAnswer =
+ questionnaireViewItem.answers.singleOrNull()?.valueTimeType?.localTime
+
+ // If there is no set answer in the QuestionnaireItemViewItem, make the time field empty.
+ timeInputEditText.setText(
+ questionnaireItemViewItemDateTimeAnswer?.toLocalizedString(timeInputEditText.context)
+ ?: "",
+ )
+ }
+
+ override fun setReadOnly(isReadOnly: Boolean) {
+ // The system outside this delegate should only be able to mark it read only. Otherwise, it
+ // will change the state set by this delegate in bindView().
+ if (isReadOnly) {
+ timeInputEditText.isEnabled = false
+ timeInputLayout.isEnabled = false
+ }
+ }
+
+ private fun buildMaterialTimePicker(context: Context, inputMode: Int) {
+ val selectedTime =
+ questionnaireViewItem.answers.singleOrNull()?.valueTimeType?.localTime ?: LocalTime.now()
+ val timeFormat =
+ if (DateFormat.is24HourFormat(context)) {
+ TimeFormat.CLOCK_24H
+ } else {
+ TimeFormat.CLOCK_12H
+ }
+ MaterialTimePicker.Builder()
+ .setTitleText(R.string.select_time)
+ .setHour(selectedTime.hour)
+ .setMinute(selectedTime.minute)
+ .setTimeFormat(timeFormat)
+ .setInputMode(inputMode)
+ .build()
+ .apply {
+ addOnPositiveButtonClickListener {
+ with(LocalTime.of(this.hour, this.minute, 0)) {
+ timeInputEditText.setText(this.toLocalizedString(context))
+ setQuestionnaireItemViewItemAnswer(this)
+ timeInputEditText.clearFocus()
+ }
+ }
+ }
+ .show(context.tryUnwrapContext()!!.supportFragmentManager, TAG)
+ }
+
+ /** Set the answer in the [QuestionnaireResponse]. */
+ private fun setQuestionnaireItemViewItemAnswer(localDateTime: LocalTime) =
+ context.lifecycleScope.launch {
+ questionnaireViewItem.setAnswer(
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent()
+ .setValue(TimeType(localDateTime.format(DateTimeFormatter.ISO_TIME))),
+ )
+ }
+
+ private fun clearPreviousState() {
+ timeInputEditText.isEnabled = true
+ timeInputLayout.isEnabled = true
+ }
+ }
+
+ private val TimeType.localTime
+ get() =
+ LocalTime.of(
+ hour,
+ minute,
+ second.toInt(),
+ )
+}
diff --git a/datacapture/src/main/res/layout/edit_text_single_line_view.xml b/datacapture/src/main/res/layout/edit_text_single_line_view.xml
index 7520fe616a..56bf4db06b 100644
--- a/datacapture/src/main/res/layout/edit_text_single_line_view.xml
+++ b/datacapture/src/main/res/layout/edit_text_single_line_view.xml
@@ -52,6 +52,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapterTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapterTest.kt
index 6dd1a3e33d..7f90968396 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapterTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapterTest.kt
@@ -19,6 +19,7 @@ package com.google.android.fhir.datacapture
import android.os.Build
import android.widget.FrameLayout
import androidx.test.core.app.ApplicationProvider
+import com.google.android.fhir.datacapture.extensions.EXTENSION_DIALOG_URL_ANDROID_FHIR
import com.google.android.fhir.datacapture.extensions.EXTENSION_ITEM_CONTROL_SYSTEM
import com.google.android.fhir.datacapture.extensions.EXTENSION_ITEM_CONTROL_SYSTEM_ANDROID_FHIR
import com.google.android.fhir.datacapture.extensions.EXTENSION_ITEM_CONTROL_URL
@@ -496,6 +497,82 @@ class QuestionnaireEditAdapterTest {
.isEqualTo(QuestionnaireViewHolderType.DROP_DOWN.value)
}
+ @Test
+ fun `getItemViewType() with radio button and dialog extension should return dialog select view holder type`() {
+ val questionnaireEditAdapter = QuestionnaireEditAdapter()
+ val questionnaireItem =
+ Questionnaire.QuestionnaireItemComponent().setType(Questionnaire.QuestionnaireItemType.CHOICE)
+ questionnaireItem.apply {
+ addExtension(
+ Extension()
+ .setUrl(EXTENSION_ITEM_CONTROL_URL)
+ .setValue(
+ CodeableConcept()
+ .addCoding(
+ Coding()
+ .setCode(ItemControlTypes.RADIO_BUTTON.extensionCode)
+ .setDisplay("Radio Button")
+ .setSystem(EXTENSION_ITEM_CONTROL_SYSTEM),
+ ),
+ ),
+ )
+ addExtension(Extension().setUrl(EXTENSION_DIALOG_URL_ANDROID_FHIR))
+ }
+ questionnaireEditAdapter.submitList(
+ listOf(
+ QuestionnaireAdapterItem.Question(
+ QuestionnaireViewItem(
+ questionnaireItem,
+ QuestionnaireResponse.QuestionnaireResponseItemComponent(),
+ validationResult = NotValidated,
+ answersChangedCallback = { _, _, _, _ -> },
+ ),
+ ),
+ ),
+ )
+
+ assertThat(questionnaireEditAdapter.getItemViewType(0))
+ .isEqualTo(QuestionnaireViewHolderType.DIALOG_SELECT.value)
+ }
+
+ @Test
+ fun `getItemViewType() with check box and dialog extension should return dialog select view holder type`() {
+ val questionnaireEditAdapter = QuestionnaireEditAdapter()
+ val questionnaireItem =
+ Questionnaire.QuestionnaireItemComponent().setType(Questionnaire.QuestionnaireItemType.CHOICE)
+ questionnaireItem.apply {
+ addExtension(
+ Extension()
+ .setUrl(EXTENSION_ITEM_CONTROL_URL)
+ .setValue(
+ CodeableConcept()
+ .addCoding(
+ Coding()
+ .setCode(ItemControlTypes.CHECK_BOX.extensionCode)
+ .setDisplay("Check Box")
+ .setSystem(EXTENSION_ITEM_CONTROL_SYSTEM),
+ ),
+ ),
+ )
+ addExtension(Extension().setUrl(EXTENSION_DIALOG_URL_ANDROID_FHIR))
+ }
+ questionnaireEditAdapter.submitList(
+ listOf(
+ QuestionnaireAdapterItem.Question(
+ QuestionnaireViewItem(
+ questionnaireItem,
+ QuestionnaireResponse.QuestionnaireResponseItemComponent(),
+ validationResult = NotValidated,
+ answersChangedCallback = { _, _, _, _ -> },
+ ),
+ ),
+ ),
+ )
+
+ assertThat(questionnaireEditAdapter.getItemViewType(0))
+ .isEqualTo(QuestionnaireViewHolderType.DIALOG_SELECT.value)
+ }
+
// TODO: test errors thrown for unsupported types
@Test
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderTypeTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderTypeTest.kt
index 7b971501fb..04f45d42a6 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderTypeTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewHolderTypeTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Google LLC
+ * Copyright 2022-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.
@@ -28,7 +28,7 @@ import org.robolectric.annotation.Config
class QuestionnaireViewHolderTypeTest {
@Test
fun size_shouldReturnNumberOfQuestionnaireViewHolderTypes() {
- assertThat(QuestionnaireViewHolderType.values().size).isEqualTo(18)
+ assertThat(QuestionnaireViewHolderType.values().size).isEqualTo(19)
}
@Test
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelParameterizedTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelParameterizedTest.kt
index f6c62956a3..312ff4fd2a 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelParameterizedTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelParameterizedTest.kt
@@ -29,6 +29,7 @@ import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_URI
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_SHOW_REVIEW_PAGE_FIRST
+import com.google.android.fhir.datacapture.extensions.EXTENSION_LAST_LAUNCHED_TIMESTAMP
import com.google.android.fhir.datacapture.testing.DataCaptureTestApplication
import com.google.common.truth.Truth.assertThat
import java.io.File
@@ -89,7 +90,7 @@ class QuestionnaireViewModelParameterizedTest(
val viewModel = createQuestionnaireViewModel(questionnaire)
runTest {
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
viewModel.getQuestionnaireResponse(),
QuestionnaireResponse().apply {
this.questionnaire = "http://www.sample-org/FHIR/Resources/Questionnaire/a-questionnaire"
@@ -135,7 +136,12 @@ class QuestionnaireViewModelParameterizedTest(
val viewModel = createQuestionnaireViewModel(questionnaire, questionnaireResponse)
- runTest { assertResourceEquals(viewModel.getQuestionnaireResponse(), questionnaireResponse) }
+ runTest {
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
+ viewModel.getQuestionnaireResponse(),
+ questionnaireResponse,
+ )
+ }
}
private fun createQuestionnaireViewModel(
@@ -187,6 +193,28 @@ class QuestionnaireViewModelParameterizedTest(
.isEqualTo(printer.encodeResourceToString(expected))
}
+ /**
+ * Asserts that the `expected` and the `actual` Questionnaire Responses are equal ignoring the
+ * stamp values
+ */
+ fun assertQuestionnaireResponseEqualsIgnoringTimestamps(
+ actual: QuestionnaireResponse,
+ expected: QuestionnaireResponse,
+ ) {
+ val actualResponseWithoutTimestamp =
+ actual.copy().apply {
+ extension.removeIf { ext -> ext.url == EXTENSION_LAST_LAUNCHED_TIMESTAMP }
+ authored = null
+ }
+ val expectedResponseWithoutTimestamp =
+ expected.copy().apply {
+ extension.removeIf { ext -> ext.url == EXTENSION_LAST_LAUNCHED_TIMESTAMP }
+ authored = null
+ }
+ assertThat(printer.encodeResourceToString(actualResponseWithoutTimestamp))
+ .isEqualTo(printer.encodeResourceToString(expectedResponseWithoutTimestamp))
+ }
+
@JvmStatic
@Parameters
fun parameters() =
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt
index e644e4c6d6..659429a95a 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt
@@ -48,6 +48,7 @@ import com.google.android.fhir.datacapture.extensions.EXTENSION_ENTRY_MODE_URL
import com.google.android.fhir.datacapture.extensions.EXTENSION_HIDDEN_URL
import com.google.android.fhir.datacapture.extensions.EXTENSION_ITEM_CONTROL_SYSTEM
import com.google.android.fhir.datacapture.extensions.EXTENSION_ITEM_CONTROL_URL
+import com.google.android.fhir.datacapture.extensions.EXTENSION_LAST_LAUNCHED_TIMESTAMP
import com.google.android.fhir.datacapture.extensions.EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
import com.google.android.fhir.datacapture.extensions.EXTENSION_VARIABLE_URL
import com.google.android.fhir.datacapture.extensions.EntryMode
@@ -187,7 +188,7 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire)
runTest {
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
viewModel.getQuestionnaireResponse(),
QuestionnaireResponse().apply {
this.questionnaire = "http://www.sample-org/FHIR/Resources/Questionnaire/a-questionnaire"
@@ -213,7 +214,7 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire)
runTest {
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
viewModel.getQuestionnaireResponse(),
QuestionnaireResponse().apply {
addItem(
@@ -251,7 +252,7 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire)
runTest {
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
viewModel.getQuestionnaireResponse(),
QuestionnaireResponse().apply {
addItem(
@@ -324,7 +325,7 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire)
runTest {
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
viewModel.getQuestionnaireResponse(),
QuestionnaireResponse().apply {
addItem(
@@ -422,7 +423,7 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire)
runTest {
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
viewModel.getQuestionnaireResponse(),
QuestionnaireResponse().apply {
addItem(
@@ -497,7 +498,12 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire, questionnaireResponse)
- runTest { assertResourceEquals(viewModel.getQuestionnaireResponse(), questionnaireResponse) }
+ runTest {
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
+ viewModel.getQuestionnaireResponse(),
+ questionnaireResponse,
+ )
+ }
}
@Test
@@ -565,7 +571,7 @@ class QuestionnaireViewModelTest {
}
runTest {
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
createQuestionnaireViewModel(
questionnaire,
questionnaireResponse,
@@ -651,7 +657,12 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire, questionnaireResponse)
- runTest { assertResourceEquals(questionnaireResponse, viewModel.getQuestionnaireResponse()) }
+ runTest {
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
+ viewModel.getQuestionnaireResponse(),
+ questionnaireResponse,
+ )
+ }
}
@Test
@@ -742,7 +753,12 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire, questionnaireResponse)
- runTest { assertResourceEquals(viewModel.getQuestionnaireResponse(), questionnaireResponse) }
+ runTest {
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
+ viewModel.getQuestionnaireResponse(),
+ questionnaireResponse,
+ )
+ }
}
@Test
@@ -794,7 +810,12 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire, questionnaireResponse)
- runTest { assertResourceEquals(viewModel.getQuestionnaireResponse(), questionnaireResponse) }
+ runTest {
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
+ viewModel.getQuestionnaireResponse(),
+ questionnaireResponse,
+ )
+ }
}
@Test
@@ -853,7 +874,12 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire, questionnaireResponse)
- runTest { assertResourceEquals(viewModel.getQuestionnaireResponse(), questionnaireResponse) }
+ runTest {
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
+ viewModel.getQuestionnaireResponse(),
+ questionnaireResponse,
+ )
+ }
}
@Test
@@ -895,7 +921,12 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire, questionnaireResponse)
- runTest { assertResourceEquals(viewModel.getQuestionnaireResponse(), questionnaireResponse) }
+ runTest {
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
+ viewModel.getQuestionnaireResponse(),
+ questionnaireResponse,
+ )
+ }
}
@Test
@@ -1006,7 +1037,7 @@ class QuestionnaireViewModelTest {
printer.parseResource(QuestionnaireResponse::class.java, expectedResponseString)
as QuestionnaireResponse
- assertResourceEquals(value, expectedResponse)
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(value, expectedResponse)
}
}
@@ -1134,7 +1165,7 @@ class QuestionnaireViewModelTest {
printer.parseResource(QuestionnaireResponse::class.java, expectedResponseString)
as QuestionnaireResponse
- assertResourceEquals(value, expectedResponse)
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(value, expectedResponse)
}
}
@@ -1502,6 +1533,102 @@ class QuestionnaireViewModelTest {
// Pagination //
// //
// ==================================================================== //
+ @Test
+ fun `should include all top level items as pages when any item has page extension`() = runTest {
+ val questionnaire =
+ Questionnaire().apply {
+ id = "a-questionnaire"
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page1-noExtension"
+ type = Questionnaire.QuestionnaireItemType.GROUP
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page1-1"
+ type = Questionnaire.QuestionnaireItemType.BOOLEAN
+ text = "Question on page 1"
+ },
+ )
+ },
+ )
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page2"
+ type = Questionnaire.QuestionnaireItemType.GROUP
+ addExtension(paginationExtension)
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page2-1"
+ type = Questionnaire.QuestionnaireItemType.BOOLEAN
+ text = "Question on page 2"
+ },
+ )
+ },
+ )
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page3-noExtension"
+ type = Questionnaire.QuestionnaireItemType.GROUP
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page3-1"
+ type = Questionnaire.QuestionnaireItemType.BOOLEAN
+ text = "Question on page 3"
+ },
+ )
+ },
+ )
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page4-noExtension-hidden"
+ addExtension(hiddenExtension)
+ type = Questionnaire.QuestionnaireItemType.GROUP
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page4-1"
+ type = Questionnaire.QuestionnaireItemType.BOOLEAN
+ text = "Question on page 4"
+ },
+ )
+ },
+ )
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page5"
+ type = Questionnaire.QuestionnaireItemType.GROUP
+ addExtension(paginationExtension)
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page5-1"
+ type = Questionnaire.QuestionnaireItemType.BOOLEAN
+ text = "Question on page 5"
+ },
+ )
+ },
+ )
+ }
+
+ val viewModel = createQuestionnaireViewModel(questionnaire)
+ viewModel.runViewModelBlocking {
+ assertThat(
+ (viewModel.questionnaireStateFlow.value.displayMode as DisplayMode.EditMode).pagination,
+ )
+ .isEqualTo(
+ QuestionnairePagination(
+ isPaginated = true,
+ pages =
+ listOf(
+ QuestionnairePage(0, enabled = true, hidden = false),
+ QuestionnairePage(1, enabled = true, hidden = false),
+ QuestionnairePage(2, enabled = true, hidden = false),
+ QuestionnairePage(3, enabled = true, hidden = true),
+ QuestionnairePage(4, enabled = true, hidden = false),
+ ),
+ currentPageIndex = 0,
+ ),
+ )
+ }
+ }
@Test
fun `should show current page`() = runTest {
@@ -1799,8 +1926,11 @@ class QuestionnaireViewModelTest {
}
val viewModel = createQuestionnaireViewModel(questionnaire)
viewModel.runViewModelBlocking {
+ val questionnaireStatePagination =
+ (viewModel.questionnaireStateFlow.value.displayMode as DisplayMode.EditMode).pagination
+
assertThat(
- (viewModel.questionnaireStateFlow.value.displayMode as DisplayMode.EditMode).pagination,
+ questionnaireStatePagination,
)
.isEqualTo(
QuestionnairePagination(
@@ -1814,6 +1944,83 @@ class QuestionnaireViewModelTest {
currentPageIndex = 1,
),
)
+
+ assertThat(questionnaireStatePagination.hasPreviousPage).isFalse()
+ }
+ }
+
+ @Test
+ fun `should skip last page if it is hidden`() = runTest {
+ val questionnaire =
+ Questionnaire().apply {
+ id = "a-questionnaire"
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page1"
+ type = Questionnaire.QuestionnaireItemType.GROUP
+ addExtension(paginationExtension)
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page1-1"
+ type = Questionnaire.QuestionnaireItemType.BOOLEAN
+ text = "Question on page 1"
+ },
+ )
+ },
+ )
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page2"
+ type = Questionnaire.QuestionnaireItemType.GROUP
+ addExtension(paginationExtension)
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page2-1"
+ type = Questionnaire.QuestionnaireItemType.BOOLEAN
+ text = "Question on page 2"
+ },
+ )
+ },
+ )
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page3"
+ type = Questionnaire.QuestionnaireItemType.GROUP
+ addExtension(paginationExtension)
+ addExtension(hiddenExtension)
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "page3-1"
+ type = Questionnaire.QuestionnaireItemType.BOOLEAN
+ text = "Question on page 3"
+ },
+ )
+ },
+ )
+ }
+ val viewModel = createQuestionnaireViewModel(questionnaire)
+ viewModel.runViewModelBlocking {
+ viewModel.goToNextPage()
+ val questionnaireStatePagination =
+ (viewModel.questionnaireStateFlow.value.displayMode as DisplayMode.EditMode).pagination
+
+ assertThat(
+ questionnaireStatePagination,
+ )
+ .isEqualTo(
+ QuestionnairePagination(
+ isPaginated = true,
+ pages =
+ listOf(
+ QuestionnairePage(0, enabled = true, hidden = false),
+ QuestionnairePage(1, enabled = true, hidden = false),
+ QuestionnairePage(2, enabled = true, hidden = true),
+ ),
+ currentPageIndex = 1,
+ ),
+ )
+
+ assertThat(questionnaireStatePagination.hasNextPage).isFalse()
}
}
@@ -3340,7 +3547,7 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire)
runTest {
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
viewModel.getQuestionnaireResponse(),
QuestionnaireResponse().apply {
addItem(
@@ -3387,7 +3594,7 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire)
runTest {
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
viewModel.getQuestionnaireResponse(),
QuestionnaireResponse().apply {
addItem(
@@ -3435,7 +3642,7 @@ class QuestionnaireViewModelTest {
val viewModel = createQuestionnaireViewModel(questionnaire)
runTest {
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
viewModel.getQuestionnaireResponse(),
QuestionnaireResponse().apply {
addItem(
@@ -3489,7 +3696,7 @@ class QuestionnaireViewModelTest {
val viewModel = QuestionnaireViewModel(context, state)
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
viewModel.getQuestionnaireResponse(),
QuestionnaireResponse().apply {
addItem(
@@ -3534,7 +3741,7 @@ class QuestionnaireViewModelTest {
val viewModel = QuestionnaireViewModel(context, state)
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
viewModel.getQuestionnaireResponse(),
QuestionnaireResponse().apply {
addItem(
@@ -3621,7 +3828,10 @@ class QuestionnaireViewModelTest {
val viewModel = QuestionnaireViewModel(context, state)
- assertResourceEquals(viewModel.getQuestionnaireResponse(), questionnaireResponse)
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
+ viewModel.getQuestionnaireResponse(),
+ questionnaireResponse,
+ )
}
@Test
@@ -3787,7 +3997,7 @@ class QuestionnaireViewModelTest {
// Clearing the answer disables question-2 that in turn disables question-3.
items.first { it.questionnaireItem.linkId == "question-1" }.clearAnswer()
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
viewModel.getQuestionnaireResponse(),
QuestionnaireResponse().apply {
id = "a-questionnaire-response"
@@ -3809,7 +4019,10 @@ class QuestionnaireViewModelTest {
},
)
- assertResourceEquals(viewModel.getQuestionnaireResponse(), questionnaireResponse)
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
+ viewModel.getQuestionnaireResponse(),
+ questionnaireResponse,
+ )
}
}
@@ -4180,7 +4393,7 @@ class QuestionnaireViewModelTest {
printer.parseResource(QuestionnaireResponse::class.java, expectedResponseString)
as QuestionnaireResponse
- assertResourceEquals(value, expectedResponse)
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(value, expectedResponse)
}
}
@@ -4278,7 +4491,7 @@ class QuestionnaireViewModelTest {
val expectedResponse =
printer.parseResource(QuestionnaireResponse::class.java, expectedResponseString)
as QuestionnaireResponse
- assertResourceEquals(value, expectedResponse)
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(value, expectedResponse)
}
}
@@ -4339,7 +4552,10 @@ class QuestionnaireViewModelTest {
},
)
- assertResourceEquals(viewModel.getQuestionnaireResponse(), questionnaireResponse)
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
+ viewModel.getQuestionnaireResponse(),
+ questionnaireResponse,
+ )
}
}
@@ -4459,7 +4675,7 @@ class QuestionnaireViewModelTest {
)
.inOrder()
- assertResourceEquals(
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
actual = viewModel.getQuestionnaireResponse(),
expected =
QuestionnaireResponse().apply {
@@ -4641,7 +4857,10 @@ class QuestionnaireViewModelTest {
},
)
- assertResourceEquals(viewModel.getQuestionnaireResponse(), questionnaireResponse)
+ assertQuestionnaireResponseEqualsIgnoringTimestamps(
+ viewModel.getQuestionnaireResponse(),
+ questionnaireResponse,
+ )
}
}
@@ -7517,6 +7736,28 @@ class QuestionnaireViewModelTest {
assertThat(printer.encodeResourceToString(actual))
.isEqualTo(printer.encodeResourceToString(expected))
}
+
+ /**
+ * Asserts that the `expected` and the `actual` Questionnaire Responses are equal ignoring the
+ * stamp values
+ */
+ private fun assertQuestionnaireResponseEqualsIgnoringTimestamps(
+ actual: QuestionnaireResponse,
+ expected: QuestionnaireResponse,
+ ) {
+ val actualResponseWithoutTimestamp =
+ actual.copy().apply {
+ extension.removeIf { ext -> ext.url == EXTENSION_LAST_LAUNCHED_TIMESTAMP }
+ authored = null
+ }
+ val expectedResponseWithoutTimestamp =
+ expected.copy().apply {
+ extension.removeIf { ext -> ext.url == EXTENSION_LAST_LAUNCHED_TIMESTAMP }
+ authored = null
+ }
+ assertThat(printer.encodeResourceToString(actualResponseWithoutTimestamp))
+ .isEqualTo(printer.encodeResourceToString(expectedResponseWithoutTimestamp))
+ }
}
}
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireResponsesTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireResponsesTest.kt
index 165d95fe45..275d031f01 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireResponsesTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireResponsesTest.kt
@@ -20,6 +20,7 @@ import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import com.google.common.truth.Truth.assertThat
import org.hl7.fhir.r4.model.BooleanType
+import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse
@@ -260,6 +261,121 @@ class MoreQuestionnaireResponsesTest {
assertResourceEquals(questionnaireResponse, packedQuestionnaireResponse)
}
+ @Test
+ fun `should pack repeated groups recursively`() {
+ val questionnaire =
+ Questionnaire().apply {
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "repeated-group"
+ type = Questionnaire.QuestionnaireItemType.GROUP
+ repeats = true
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "nested-repeated-group"
+ type = Questionnaire.QuestionnaireItemType.GROUP
+ repeats = true
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "nested-nested-question"
+ type = Questionnaire.QuestionnaireItemType.BOOLEAN
+ },
+ )
+ },
+ )
+ },
+ )
+ }
+
+ val questionnaireResponse =
+ QuestionnaireResponse().apply {
+ addItem(
+ QuestionnaireResponseItemComponent().apply {
+ linkId = "repeated-group"
+ addItem(
+ QuestionnaireResponseItemComponent().apply {
+ linkId = "nested-repeated-group"
+ addItem(
+ QuestionnaireResponseItemComponent().apply {
+ linkId = "nested-nested-question"
+ addAnswer(
+ QuestionnaireResponseItemAnswerComponent().apply {
+ value = BooleanType(true)
+ },
+ )
+ },
+ )
+ },
+ )
+ addItem(
+ QuestionnaireResponseItemComponent().apply {
+ linkId = "nested-repeated-group"
+ addItem(
+ QuestionnaireResponseItemComponent().apply {
+ linkId = "nested-nested-question"
+ addAnswer(
+ QuestionnaireResponseItemAnswerComponent().apply {
+ value = BooleanType(false)
+ },
+ )
+ },
+ )
+ },
+ )
+ },
+ )
+ }
+
+ val packedQuestionnaireResponse =
+ QuestionnaireResponse().apply {
+ addItem(
+ QuestionnaireResponseItemComponent().apply {
+ linkId = "repeated-group"
+ addAnswer(
+ QuestionnaireResponseItemAnswerComponent().apply {
+ addItem(
+ QuestionnaireResponseItemComponent().apply {
+ linkId = "nested-repeated-group"
+ addAnswer(
+ QuestionnaireResponseItemAnswerComponent().apply {
+ addItem(
+ QuestionnaireResponseItemComponent().apply {
+ linkId = "nested-nested-question"
+ addAnswer(
+ QuestionnaireResponseItemAnswerComponent().apply {
+ value = BooleanType(true)
+ },
+ )
+ },
+ )
+ },
+ )
+ addAnswer(
+ QuestionnaireResponseItemAnswerComponent().apply {
+ addItem(
+ QuestionnaireResponseItemComponent().apply {
+ linkId = "nested-nested-question"
+ addAnswer(
+ QuestionnaireResponseItemAnswerComponent().apply {
+ value = BooleanType(false)
+ },
+ )
+ },
+ )
+ },
+ )
+ },
+ )
+ },
+ )
+ },
+ )
+ }
+
+ questionnaireResponse.packRepeatedGroups(questionnaire)
+ assertResourceEquals(questionnaireResponse, packedQuestionnaireResponse)
+ }
+
@Test
fun `should not modify non-repeated groups while packing repeated groups`() {
val questionnaire =
@@ -695,4 +811,29 @@ class MoreQuestionnaireResponsesTest {
assertThat(iParser.encodeResourceToString(actual))
.isEqualTo(iParser.encodeResourceToString(expected))
}
+
+ @Test
+ fun `should add launchTimestamp`() {
+ val questionnaireResponse = QuestionnaireResponse()
+ val dateTimeType = DateTimeType("2024-07-05T00:00:00Z")
+ questionnaireResponse.launchTimestamp = dateTimeType
+
+ assertThat(dateTimeType).isEqualTo(questionnaireResponse.launchTimestamp)
+ }
+
+ @Test
+ fun `launchTimestamp should be null when not added`() {
+ assertThat(QuestionnaireResponse().launchTimestamp).isNull()
+ }
+
+ @Test
+ fun `launchTimestamp should update if already exists`() {
+ val questionnaireResponse = QuestionnaireResponse()
+ val oldDateTimeType = DateTimeType("2024-07-01T00:00:00Z")
+ val newDateTimeType = DateTimeType("2024-07-05T00:00:00Z")
+ questionnaireResponse.launchTimestamp = oldDateTimeType
+ questionnaireResponse.launchTimestamp = newDateTimeType
+
+ assertThat(newDateTimeType).isEqualTo(questionnaireResponse.launchTimestamp)
+ }
}
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/TimePickerViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/TimePickerViewHolderFactoryTest.kt
new file mode 100644
index 0000000000..90e4d5bc3f
--- /dev/null
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/TimePickerViewHolderFactoryTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 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.
+ * 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 com.google.android.fhir.datacapture.views
+
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import com.google.android.fhir.datacapture.R
+import com.google.android.fhir.datacapture.validation.NotValidated
+import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder
+import com.google.android.fhir.datacapture.views.factories.TimePickerViewHolderFactory
+import com.google.common.truth.Truth.assertThat
+import org.hl7.fhir.r4.model.Questionnaire
+import org.hl7.fhir.r4.model.QuestionnaireResponse
+import org.hl7.fhir.r4.model.TimeType
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.shadows.ShadowSettings
+
+@RunWith(RobolectricTestRunner::class)
+class TimePickerViewHolderFactoryTest {
+ private val context =
+ Robolectric.buildActivity(AppCompatActivity::class.java).create().get().apply {
+ setTheme(com.google.android.material.R.style.Theme_Material3_DayNight)
+ }
+ private val parent = FrameLayout(context)
+ private val viewHolder = TimePickerViewHolderFactory.create(parent)
+
+ private val QuestionnaireItemViewHolder.timeInputView: TextView
+ get() {
+ return itemView.findViewById(R.id.text_input_edit_text)
+ }
+
+ @Test
+ fun shouldSetQuestionHeader() {
+ viewHolder.bind(
+ QuestionnaireViewItem(
+ Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" },
+ QuestionnaireResponse.QuestionnaireResponseItemComponent(),
+ validationResult = NotValidated,
+ answersChangedCallback = { _, _, _, _ -> },
+ ),
+ )
+
+ assertThat(viewHolder.itemView.findViewById(R.id.question).text.toString())
+ .isEqualTo("Question?")
+ }
+
+ @Test
+ fun shouldSetEmptyTimeInput() {
+ viewHolder.bind(
+ QuestionnaireViewItem(
+ Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" },
+ QuestionnaireResponse.QuestionnaireResponseItemComponent(),
+ validationResult = NotValidated,
+ answersChangedCallback = { _, _, _, _ -> },
+ ),
+ )
+
+ assertThat(viewHolder.timeInputView.text.toString()).isEqualTo("")
+ }
+
+ @Test
+ fun `should show AM time when set time format is 12 hrs`() {
+ ShadowSettings.set24HourTimeFormat(false)
+ viewHolder.bind(
+ QuestionnaireViewItem(
+ Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" },
+ QuestionnaireResponse.QuestionnaireResponseItemComponent()
+ .addAnswer(
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent()
+ .setValue(TimeType("10:10")),
+ ),
+ validationResult = NotValidated,
+ answersChangedCallback = { _, _, _, _ -> },
+ ),
+ )
+ assertThat(viewHolder.timeInputView.text.toString()).isEqualTo("10:10 AM")
+ }
+
+ @Test
+ fun `should show PM time when set time format is 12 hrs`() {
+ ShadowSettings.set24HourTimeFormat(false)
+ viewHolder.bind(
+ QuestionnaireViewItem(
+ Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" },
+ QuestionnaireResponse.QuestionnaireResponseItemComponent()
+ .addAnswer(
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent()
+ .setValue(TimeType("22:10:10")),
+ ),
+ validationResult = NotValidated,
+ answersChangedCallback = { _, _, _, _ -> },
+ ),
+ )
+ assertThat(viewHolder.timeInputView.text.toString()).isEqualTo("10:10 PM")
+ }
+
+ @Test
+ fun `should show time when set time format is 24 hrs`() {
+ ShadowSettings.set24HourTimeFormat(true)
+ viewHolder.bind(
+ QuestionnaireViewItem(
+ Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" },
+ QuestionnaireResponse.QuestionnaireResponseItemComponent()
+ .addAnswer(
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent()
+ .setValue(TimeType("22:10")),
+ ),
+ validationResult = NotValidated,
+ answersChangedCallback = { _, _, _, _ -> },
+ ),
+ )
+ assertThat(viewHolder.timeInputView.text.toString()).isEqualTo("22:10")
+ }
+}
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/CheckBoxGroupViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/CheckBoxGroupViewHolderFactoryTest.kt
index bc49267601..f5f361cc87 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/CheckBoxGroupViewHolderFactoryTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/CheckBoxGroupViewHolderFactoryTest.kt
@@ -124,7 +124,8 @@ class CheckBoxGroupViewHolderFactoryTest {
val checkBoxGroup = viewHolder.itemView.findViewById(R.id.checkbox_group)
val children = checkBoxGroup.children.asIterable().filterIsInstance()
children.forEachIndexed { index, view ->
- assertThat(view.text).isEqualTo(questionnaire.answerOption[index].valueCoding.display)
+ assertThat(view.text.toString())
+ .isEqualTo(questionnaire.answerOption[index].valueCoding.display)
assertThat(view.layoutParams.width).isEqualTo(ViewGroup.LayoutParams.MATCH_PARENT)
}
}
@@ -161,7 +162,8 @@ class CheckBoxGroupViewHolderFactoryTest {
val checkBoxGroup = viewHolder.itemView.findViewById(R.id.checkbox_group)
val children = checkBoxGroup.children.asIterable().filterIsInstance()
children.forEachIndexed { index, view ->
- assertThat(view.text).isEqualTo(questionnaire.answerOption[index].valueCoding.display)
+ assertThat(view.text.toString())
+ .isEqualTo(questionnaire.answerOption[index].valueCoding.display)
assertThat(view.layoutParams.width).isEqualTo(ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/RadioGroupViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/RadioGroupViewHolderFactoryTest.kt
index cb2d536d58..6f4bc8a3e4 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/RadioGroupViewHolderFactoryTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/RadioGroupViewHolderFactoryTest.kt
@@ -118,7 +118,8 @@ class RadioGroupViewHolderFactoryTest {
val radioGroup = viewHolder.itemView.findViewById(R.id.radio_group)
val children = radioGroup.children.asIterable().filterIsInstance()
children.forEachIndexed { index, view ->
- assertThat(view.text).isEqualTo(questionnaire.answerOption[index].valueCoding.display)
+ assertThat(view.text.toString())
+ .isEqualTo(questionnaire.answerOption[index].valueCoding.display)
assertThat(view.layoutParams.width).isEqualTo(ViewGroup.LayoutParams.MATCH_PARENT)
}
}
@@ -154,7 +155,8 @@ class RadioGroupViewHolderFactoryTest {
val radioGroup = viewHolder.itemView.findViewById(R.id.radio_group)
val children = radioGroup.children.asIterable().filterIsInstance()
children.forEachIndexed { index, view ->
- assertThat(view.text).isEqualTo(questionnaire.answerOption[index].valueCoding.display)
+ assertThat(view.text.toString())
+ .isEqualTo(questionnaire.answerOption[index].valueCoding.display)
assertThat(view.layoutParams.width).isEqualTo(ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/ReviewViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/ReviewViewHolderFactoryTest.kt
index cfa8ea2408..3e43a24d3d 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/ReviewViewHolderFactoryTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/ReviewViewHolderFactoryTest.kt
@@ -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.
@@ -180,7 +180,7 @@ class ReviewViewHolderFactoryTest {
),
)
- assertThat(viewHolder.itemView.findViewById(R.id.answer_text_view).text)
+ assertThat(viewHolder.itemView.findViewById(R.id.answer_text_view).text.toString())
.isEqualTo(
ApplicationProvider.getApplicationContext().getString(R.string.not_answered),
)
@@ -206,7 +206,7 @@ class ReviewViewHolderFactoryTest {
),
)
- assertThat(viewHolder.itemView.findViewById(R.id.answer_text_view).text)
+ assertThat(viewHolder.itemView.findViewById(R.id.answer_text_view).text.toString())
.isEqualTo(ApplicationProvider.getApplicationContext().getString(R.string.yes))
}
@@ -321,7 +321,7 @@ class ReviewViewHolderFactoryTest {
),
)
- assertThat(viewHolder.itemView.findViewById(R.id.answer_text_view).text)
+ assertThat(viewHolder.itemView.findViewById(R.id.answer_text_view).text.toString())
.isEqualTo("Yes")
}
@@ -340,7 +340,7 @@ class ReviewViewHolderFactoryTest {
),
)
- assertThat(viewHolder.itemView.findViewById(R.id.answer_text_view).text)
+ assertThat(viewHolder.itemView.findViewById(R.id.answer_text_view).text.toString())
.isEqualTo("Not Answered")
}
diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts
index 7d0fb5c13c..18c4cd7f1f 100644
--- a/demo/build.gradle.kts
+++ b/demo/build.gradle.kts
@@ -43,7 +43,6 @@ dependencies {
coreLibraryDesugaring(Dependencies.desugarJdkLibs)
- implementation(Dependencies.material)
implementation(Dependencies.timber)
implementation(libs.androidx.activity)
implementation(libs.androidx.appcompat)
@@ -60,6 +59,7 @@ dependencies {
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.core)
+ implementation(libs.material)
implementation(project(":datacapture")) {
exclude(group = "com.google.android.fhir", module = "engine")
}
diff --git a/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt b/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt
new file mode 100644
index 0000000000..2694044bc8
--- /dev/null
+++ b/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2024-2025 Google LLC
+ *
+ * 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 com.google.android.fhir.demo
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.CheckBox
+import android.widget.EditText
+import android.widget.RadioGroup
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.fragment.NavHostFragment
+import com.google.android.fhir.demo.helpers.PatientCreationHelper
+import com.google.android.material.tabs.TabLayout
+import kotlinx.coroutines.launch
+import org.hl7.fhir.r4.model.Enumerations
+
+class CrudOperationFragment : Fragment() {
+ private val crudOperationViewModel: CrudOperationViewModel by viewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ return inflater.inflate(R.layout.fragment_crud_layout, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setUpActionBar()
+ setHasOptionsMenu(true)
+ setupUiOnScreenLaunch()
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ crudOperationViewModel.patientUiState.collect { patientUiState ->
+ patientUiState?.let {
+ when (it.operationType) {
+ OperationType.CREATE -> {
+ Toast.makeText(requireContext(), "Patient is saved", Toast.LENGTH_SHORT).show()
+ }
+ OperationType.READ -> displayPatientDetails(it)
+ OperationType.UPDATE -> {
+ Toast.makeText(requireContext(), "Patient is updated", Toast.LENGTH_SHORT).show()
+ }
+ OperationType.DELETE -> {
+ // Reset the page as the patient has been deleted.
+ clearUiFieldValues()
+ configureFieldsForOperation(OperationType.DELETE)
+ Toast.makeText(requireContext(), "Patient is deleted", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ android.R.id.home -> {
+ NavHostFragment.findNavController(this).navigateUp()
+ true
+ }
+ else -> false
+ }
+ }
+
+ private fun setUpActionBar() {
+ (requireActivity() as AppCompatActivity).supportActionBar?.apply {
+ title = requireContext().getString(R.string.crud_operations)
+ setDisplayHomeAsUpEnabled(true)
+ }
+ }
+
+ private fun setupUiOnScreenLaunch() {
+ setupTabLayoutChangeListener()
+ selectTab(TAB_CREATE)
+ setupUiForCrudOperation(OperationType.CREATE)
+
+ requireView().findViewById