diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 8427272ecc..5469d71f81 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -117,6 +117,8 @@ object Dependencies { const val kotlinTestJunit = "org.jetbrains.kotlin:kotlin-test-junit:${Versions.Kotlin.stdlib}" const val kotlinCoroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.Kotlin.kotlinCoroutinesCore}" + const val kotlinCoroutinesPlay = + "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:${Versions.Kotlin.kotlinCoroutinesCore}" const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.Kotlin.stdlib}" } @@ -155,6 +157,9 @@ object Dependencies { "com.google.mlkit:object-detection-custom:${Versions.Mlkit.objectDetectionCustom}" } + const val playServicesLocation = + "com.google.android.gms:play-services-location:${Versions.playServicesLocation}" + const val androidFhirGroup = "com.google.android.fhir" const val androidFhirEngineModule = "engine" const val androidFhirKnowledgeModule = "knowledge" @@ -307,6 +312,8 @@ object Dependencies { const val objectDetection = "16.2.3" const val objectDetectionCustom = "16.3.1" } + + const val playServicesLocation = "21.0.1" } fun Configuration.removeIncompatibleDependencies() { diff --git a/buildSrc/src/main/kotlin/LicenseeConfig.kt b/buildSrc/src/main/kotlin/LicenseeConfig.kt index 554c26a87a..0beaf453c8 100644 --- a/buildSrc/src/main/kotlin/LicenseeConfig.kt +++ b/buildSrc/src/main/kotlin/LicenseeConfig.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. @@ -116,6 +116,7 @@ fun Project.configureLicensee() { // https://developers.google.com/android/reference/com/google/android/gms/common/package-summary allowDependency("com.google.android.gms", "play-services-base", "17.4.0") { because("") } allowDependency("com.google.android.gms", "play-services-base", "18.0.1") { because("") } + allowDependency("com.google.android.gms", "play-services-base", "18.1.0") { because("") } allowDependency("com.google.android.odml", "image", "1.0.0-beta1") { because("") } @@ -123,6 +124,7 @@ fun Project.configureLicensee() { // https://developers.google.com/android/reference/com/google/android/gms/common/package-summary allowDependency("com.google.android.gms", "play-services-basement", "17.4.0") { because("") } allowDependency("com.google.android.gms", "play-services-basement", "18.0.0") { because("") } + allowDependency("com.google.android.gms", "play-services-basement", "18.1.0") { because("") } // https://developers.google.com/android/reference/com/google/android/gms/common/package-summary allowDependency("com.google.android.gms", "play-services-clearcut", "17.0.0") { because("") } @@ -138,6 +140,7 @@ fun Project.configureLicensee() { // Tasks API Android https://developers.google.com/android/guides/tasks allowDependency("com.google.android.gms", "play-services-tasks", "17.2.0") { because("") } allowDependency("com.google.android.gms", "play-services-tasks", "18.0.1") { because("") } + allowDependency("com.google.android.gms", "play-services-tasks", "18.0.2") { because("") } // Barcode Scanning https://developers.google.com/ml-kit/vision/barcode-scanning allowDependency("com.google.mlkit", "barcode-scanning", "16.1.1") { because("") } @@ -201,4 +204,5 @@ private val nonStandardLicenseUrls = "http://opensource.org/licenses/BSD-3-Clause", "http://www.opensource.org/licenses/bsd-license.php", "https://asm.ow2.io/license.html", + "https://developer.android.com/studio/terms.html", ) diff --git a/buildSrc/src/main/kotlin/Releases.kt b/buildSrc/src/main/kotlin/Releases.kt index 4a458fa3ea..69ed05d2de 100644 --- a/buildSrc/src/main/kotlin/Releases.kt +++ b/buildSrc/src/main/kotlin/Releases.kt @@ -70,6 +70,13 @@ object Releases { override val version = "0.1.0-beta3" override val name = "Android FHIR Structured Data Capture - Barcode Extensions (contrib)" } + + object LocationWidget : LibraryArtifact { + override val artifactId = "contrib-locationwidget" + override val version = "0.1.0-alpha01" + override val name = + "Android FHIR Structured Data Capture - Location Widget Extensions (contrib)" + } } object Knowledge : LibraryArtifact { diff --git a/catalog/build.gradle.kts b/catalog/build.gradle.kts index e4bb7a4373..3287f3b4d8 100644 --- a/catalog/build.gradle.kts +++ b/catalog/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { implementation(project(path = ":datacapture")) implementation(project(path = ":engine")) implementation(project(path = ":contrib:barcode")) + implementation(project(path = ":contrib:locationwidget")) testImplementation(Dependencies.junit) } diff --git a/catalog/src/main/assets/component_location_widget.json b/catalog/src/main/assets/component_location_widget.json new file mode 100644 index 0000000000..73c26da715 --- /dev/null +++ b/catalog/src/main/assets/component_location_widget.json @@ -0,0 +1,52 @@ +{ + "resourceType": "Questionnaire", + "language": "en", + "status": "active", + "date": "2020-11-18T07:24:47.111Z", + "item": [ + { + "linkId": "location-widget", + "type": "group", + "text": "Location Widget", + "extension": [ + { + "url": "https://github.com/google/android-fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://github.com/google/android-fhir/questionnaire-item-control", + "code": "location-widget" + } + ] + } + } + ], + "item": [ + { + "linkId": "latitude", + "type": "decimal", + "required": true, + "text": "Latitude", + "extension": [ + { + "url": "gps-coordinate", + "valueString": "latitude" + } + ] + }, + { + "linkId": "longitude", + "type": "decimal", + "required": true, + "text": "Longitude", + "extension": [ + { + "url": "gps-coordinate", + "valueString": "longitude" + } + ] + } + ] + } + ] +} diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/CatalogApplication.kt b/catalog/src/main/java/com/google/android/fhir/catalog/CatalogApplication.kt index bf2c639fce..204651fd02 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/CatalogApplication.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/CatalogApplication.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 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. @@ -40,7 +40,11 @@ class CatalogApplication : Application(), DataCaptureConfig.Provider { FhirEngineProvider.init(FhirEngineConfiguration()) dataCaptureConfig = - DataCaptureConfig(xFhirQueryResolver = { fhirEngine.search(it).map { it.resource } }) + DataCaptureConfig( + xFhirQueryResolver = { fhirEngine.search(it).map { it.resource } }, + questionnaireItemViewHolderFactoryMatchersProviderFactory = + ContribQuestionnaireItemViewHolderFactoryMatchersProviderFactory, + ) CoroutineScope(Dispatchers.IO).launch { assets 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 8fd9f2acdb..bdafbcd17a 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 @@ -147,6 +147,11 @@ class ComponentListViewModel(application: Application, private val state: SavedS R.string.component_name_initial_value, "component_initial_value.json", ), + LOCATION_WIDGET( + R.drawable.ic_location_on, + R.string.component_name_location_widget, + "component_location_widget.json", + ), } val viewItemList = @@ -171,6 +176,7 @@ class ComponentListViewModel(application: Application, private val state: SavedS ViewItem.ComponentItem(Component.ITEM_MEDIA), ViewItem.ComponentItem(Component.ITEM_ANSWER_MEDIA), ViewItem.ComponentItem(Component.INITIAL_VALUE), + ViewItem.ComponentItem(Component.LOCATION_WIDGET), ) fun isComponent(context: Context, title: String) = diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/ContribQuestionnaireItemViewHolderFactoryMatchersProviderFactory.kt b/catalog/src/main/java/com/google/android/fhir/catalog/ContribQuestionnaireItemViewHolderFactoryMatchersProviderFactory.kt new file mode 100644 index 0000000000..4541a27f50 --- /dev/null +++ b/catalog/src/main/java/com/google/android/fhir/catalog/ContribQuestionnaireItemViewHolderFactoryMatchersProviderFactory.kt @@ -0,0 +1,51 @@ +/* + * 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.catalog + +import com.google.android.fhir.datacapture.QuestionnaireFragment +import com.google.android.fhir.datacapture.QuestionnaireItemViewHolderFactoryMatchersProviderFactory +import com.google.android.fhir.datacapture.contrib.views.locationwidget.LocationGpsCoordinateViewHolderFactory +import com.google.android.fhir.datacapture.contrib.views.locationwidget.LocationWidgetViewHolderFactory + +object ContribQuestionnaireItemViewHolderFactoryMatchersProviderFactory : + QuestionnaireItemViewHolderFactoryMatchersProviderFactory { + + const val LOCATION_WIDGET_PROVIDER = "location-widget-provider" + + override fun get( + provider: String, + ): QuestionnaireFragment.QuestionnaireItemViewHolderFactoryMatchersProvider = + when (provider) { + LOCATION_WIDGET_PROVIDER -> + object : QuestionnaireFragment.QuestionnaireItemViewHolderFactoryMatchersProvider() { + override fun get(): + List { + return listOf( + QuestionnaireFragment.QuestionnaireItemViewHolderFactoryMatcher( + factory = LocationGpsCoordinateViewHolderFactory, + matches = LocationGpsCoordinateViewHolderFactory::matcher, + ), + QuestionnaireFragment.QuestionnaireItemViewHolderFactoryMatcher( + factory = LocationWidgetViewHolderFactory, + matches = LocationWidgetViewHolderFactory::matcher, + ), + ) + } + } + else -> throw NotImplementedError() + } +} diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/DemoQuestionnaireFragment.kt b/catalog/src/main/java/com/google/android/fhir/catalog/DemoQuestionnaireFragment.kt index 4d06d65961..9a8ac0879a 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/DemoQuestionnaireFragment.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/DemoQuestionnaireFragment.kt @@ -140,7 +140,13 @@ class DemoQuestionnaireFragment : Fragment() { setReorderingAllowed(true) val questionnaireFragment = QuestionnaireFragment.builder() - .apply { setQuestionnaire(args.questionnaireJsonStringKey!!) } + .apply { + setCustomQuestionnaireItemViewHolderFactoryMatchersProvider( + ContribQuestionnaireItemViewHolderFactoryMatchersProviderFactory + .LOCATION_WIDGET_PROVIDER, + ) + setQuestionnaire(args.questionnaireJsonStringKey!!) + } .build() add(R.id.container, questionnaireFragment, QUESTIONNAIRE_FRAGMENT_TAG) } diff --git a/catalog/src/main/res/drawable/ic_location_on.xml b/catalog/src/main/res/drawable/ic_location_on.xml new file mode 100644 index 0000000000..0f96a89039 --- /dev/null +++ b/catalog/src/main/res/drawable/ic_location_on.xml @@ -0,0 +1,13 @@ + + + diff --git a/catalog/src/main/res/values/strings.xml b/catalog/src/main/res/values/strings.xml index 3371f69d71..5509e201a6 100644 --- a/catalog/src/main/res/values/strings.xml +++ b/catalog/src/main/res/values/strings.xml @@ -36,6 +36,7 @@ Item Answer Media Repeated Group Attachment + Location Widget Default Paginated Review diff --git a/contrib/locationwidget/.gitignore b/contrib/locationwidget/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/contrib/locationwidget/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/contrib/locationwidget/build.gradle.kts b/contrib/locationwidget/build.gradle.kts new file mode 100644 index 0000000000..324638d7d0 --- /dev/null +++ b/contrib/locationwidget/build.gradle.kts @@ -0,0 +1,75 @@ +import Dependencies.removeIncompatibleDependencies + +plugins { + id(Plugins.BuildPlugins.androidLib) + id(Plugins.BuildPlugins.kotlinAndroid) + id(Plugins.BuildPlugins.mavenPublish) + jacoco +} + +publishArtifact(Releases.Contrib.LocationWidget) + +createJacocoTestReportTask() + +android { + namespace = "com.google.android.fhir.datacapture.contrib.views.locationwidget" + compileSdk = Sdk.compileSdk + defaultConfig { + minSdk = Sdk.minSdk + testInstrumentationRunner = Dependencies.androidJunitRunner + // Need to specify this to prevent junit runner from going deep into our dependencies + testInstrumentationRunnerArguments["package"] = "com.google.android.fhir.datacapture" + } + + buildFeatures { viewBinding = true } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) + } + } + + packaging { + resources.excludes.addAll( + listOf( + "META-INF/INDEX.LIST", + "META-INF/ASL2.0", + "META-INF/ASL-2.0.txt", + "META-INF/LGPL-3.0.txt", + ), + ) + } + + configureJacocoTestOptions() + + testOptions { animationsDisabled = true } + kotlin { jvmToolchain(11) } +} + +configurations { all { removeIncompatibleDependencies() } } + +dependencies { + implementation(project(":datacapture")) + implementation(Dependencies.Androidx.coreKtx) + implementation(Dependencies.Androidx.fragmentKtx) + implementation(Dependencies.playServicesLocation) + implementation(Dependencies.Kotlin.kotlinCoroutinesPlay) + implementation(Dependencies.material) + implementation(Dependencies.timber) + implementation(Dependencies.Androidx.appCompat) + + testImplementation(Dependencies.AndroidxTest.fragmentTesting) + testImplementation(Dependencies.Kotlin.kotlinTestJunit) + testImplementation(Dependencies.junit) + testImplementation(Dependencies.robolectric) + testImplementation(Dependencies.truth) + + androidTestImplementation(Dependencies.AndroidxTest.core) + androidTestImplementation(Dependencies.AndroidxTest.extJunit) + androidTestImplementation(Dependencies.AndroidxTest.extJunitKtx) + androidTestImplementation(Dependencies.AndroidxTest.fragmentTesting) + androidTestImplementation(Dependencies.AndroidxTest.rules) + androidTestImplementation(Dependencies.AndroidxTest.runner) + androidTestImplementation(Dependencies.truth) +} 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 new file mode 100644 index 0000000000..b91eec759f --- /dev/null +++ b/contrib/locationwidget/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/locationwidget/LocationGpsCoordinateViewHolderFactoryInstrumentedTest.kt @@ -0,0 +1,58 @@ +/* + * 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.contrib.views.locationwidget + +import android.content.Context +import android.text.InputType +import android.view.View +import android.view.ViewGroup +import android.widget.AutoCompleteTextView +import android.widget.FrameLayout +import androidx.appcompat.view.ContextThemeWrapper +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.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LocationGpsCoordinateViewHolderFactoryInstrumentedTest { + private lateinit var context: Context + private lateinit var parent: ViewGroup + + @Before + fun setUp() { + context = + ContextThemeWrapper( + InstrumentationRegistry.getInstrumentation().targetContext, + com.google.android.fhir.datacapture.R.style.Theme_Questionnaire, + ) + parent = FrameLayout(context) + } + + @Test + fun createShouldReturnViewHolderWithUnEditableInputField() { + val viewHolder = LocationGpsCoordinateViewHolderFactory.create(parent) + assertThat(viewHolder.itemView.findViewById(R.id.autoCompleteTextView).isVisible).isTrue() + assertThat( + viewHolder.itemView.findViewById(R.id.autoCompleteTextView).inputType, + ) + .isEqualTo(InputType.TYPE_NULL) + } +} diff --git a/contrib/locationwidget/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/locationwidget/LocationWidgetViewHolderFactoryInstrumentedTest.kt b/contrib/locationwidget/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/locationwidget/LocationWidgetViewHolderFactoryInstrumentedTest.kt new file mode 100644 index 0000000000..fb61437026 --- /dev/null +++ b/contrib/locationwidget/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/locationwidget/LocationWidgetViewHolderFactoryInstrumentedTest.kt @@ -0,0 +1,54 @@ +/* + * 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.contrib.views.locationwidget + +import android.content.Context +import android.view.ViewGroup +import android.widget.Button +import android.widget.FrameLayout +import androidx.appcompat.view.ContextThemeWrapper +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.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LocationWidgetViewHolderFactoryInstrumentedTest { + private lateinit var context: Context + private lateinit var parent: ViewGroup + + @Before + fun setUp() { + context = + ContextThemeWrapper( + InstrumentationRegistry.getInstrumentation().targetContext, + com.google.android.fhir.datacapture.R.style.Theme_Questionnaire, + ) + parent = FrameLayout(context) + } + + @Test + fun createShouldReturnViewHolderWithLocationWidgetButton() { + val viewHolder = LocationWidgetViewHolderFactory.create(parent) + assertThat(viewHolder.itemView.findViewById