Skip to content

Commit

Permalink
Merge pull request #2997 from opensrp/questionnaire-location
Browse files Browse the repository at this point in the history
Add Location co-ordinates on questionnaire submission
  • Loading branch information
dubdabasoduba authored Mar 18, 2024
2 parents fa44073 + ac82739 commit 402511d
Show file tree
Hide file tree
Showing 20 changed files with 713 additions and 25 deletions.
1 change: 0 additions & 1 deletion android/engine/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ dependencies {
api(libs.jjwt)
api(libs.fhir.common.utils) { exclude(group = "org.slf4j", module = "jcl-over-slf4j") }
api(libs.runtime.livedata)
// api(libs.material3)
api(libs.foundation)
api(libs.fhir.common.utils)
api(libs.kotlinx.serialization.json)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,9 @@ data class ApplicationConfiguration(
val showLogo: Boolean = true,
val taskBackgroundWorkerBatchSize: Int = 500,
val eventWorkflows: List<EventWorkflow> = emptyList(),
val logGpsLocation: List<LocationLogOptions> = emptyList(),
) : Configuration()

enum class LocationLogOptions {
QUESTIONNAIRE,
}
8 changes: 6 additions & 2 deletions android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ okhttp = "4.12.0"
okhttp-logging-interceptor = "4.11.0"
orchestrator = "1.4.2"
p2p-lib = "0.6.9-SNAPSHOT"
paging-compose = "3.2.0"
paging-runtime-ktx = "3.2.0"
playServicesLocation = "21.0.1"
paging = "3.2.1"
preference-ktx = "1.2.1"
prettytime = "5.0.2.Final"
Expand Down Expand Up @@ -166,8 +169,9 @@ okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhtt
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp-logging-interceptor" }
orchestrator = { group = "androidx.test", name = "orchestrator", version.ref = "orchestrator" }
p2p-lib = { group = "org.smartregister", name = "p2p-lib", version.ref = "p2p-lib" }
paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" }
paging-runtime-ktx = { group = "androidx.paging", name = "paging-runtime-ktx", version.ref = "paging" }
paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging-compose" }
paging-runtime-ktx = { group = "androidx.paging", name = "paging-runtime-ktx", version.ref = "paging-runtime-ktx" }
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference-ktx" }
prettytime = { group = "org.ocpsoft.prettytime", name = "prettytime", version.ref = "prettytime" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
Expand Down
1 change: 1 addition & 0 deletions android/quest/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ dependencies {
implementation(libs.material)
implementation(libs.dagger.hilt.android)
implementation(libs.hilt.work)
implementation(libs.play.services.location)

// Annotation processors
kapt(libs.hilt.compiler)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class RegisterCardListTest {
)
}

composeTestRule.onNodeWithTag(REGISTER_CARD_LIST_TEST_TAG).onChildren().assertCountEquals(2)
composeTestRule.onNodeWithTag(REGISTER_CARD_LIST_TEST_TAG).onChildren().assertCountEquals(3)

composeTestRule
.onNodeWithTag(REGISTER_CARD_LIST_TEST_TAG)
Expand Down Expand Up @@ -119,7 +119,7 @@ class RegisterCardListTest {
)
}

composeTestRule.onNodeWithTag(REGISTER_CARD_LIST_TEST_TAG).onChildren().assertCountEquals(3)
composeTestRule.onNodeWithTag(REGISTER_CARD_LIST_TEST_TAG).onChildren().assertCountEquals(4)

composeTestRule
.onNodeWithTag(REGISTER_CARD_LIST_TEST_TAG)
Expand Down
3 changes: 3 additions & 0 deletions android/quest/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,7 @@
android:resource="@xml/file_paths" />
</provider>
</application>

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,8 @@
}
]
}
],
"logGpsLocation": [
"QUESTIONNAIRE"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,55 +16,74 @@

package org.smartregister.fhircore.quest.ui.questionnaire

import android.Manifest
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.location.Location
import android.os.Bundle
import android.os.Parcelable
import android.provider.Settings
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.core.os.bundleOf
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import com.google.android.fhir.datacapture.QuestionnaireFragment
import com.google.android.fhir.logicalId
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import dagger.hilt.android.AndroidEntryPoint
import java.io.Serializable
import java.util.LinkedList
import javax.inject.Inject
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig
import org.smartregister.fhircore.engine.configuration.app.LocationLogOptions
import org.smartregister.fhircore.engine.domain.model.ActionParameter
import org.smartregister.fhircore.engine.domain.model.ActionParameterType
import org.smartregister.fhircore.engine.domain.model.isEditable
import org.smartregister.fhircore.engine.domain.model.isReadOnly
import org.smartregister.fhircore.engine.ui.base.AlertDialogue
import org.smartregister.fhircore.engine.ui.base.BaseMultiLanguageActivity
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.extension.clearText
import org.smartregister.fhircore.engine.util.extension.encodeResourceToString
import org.smartregister.fhircore.engine.util.extension.parcelable
import org.smartregister.fhircore.engine.util.extension.parcelableArrayList
import org.smartregister.fhircore.engine.util.extension.showToast
import org.smartregister.fhircore.quest.R
import org.smartregister.fhircore.quest.databinding.QuestionnaireActivityBinding
import org.smartregister.fhircore.quest.util.LocationUtils
import org.smartregister.fhircore.quest.util.PermissionUtils
import org.smartregister.fhircore.quest.util.ResourceUtils
import timber.log.Timber

@AndroidEntryPoint
class QuestionnaireActivity : BaseMultiLanguageActivity() {

@Inject lateinit var dispatcherProvider: DispatcherProvider
val viewModel by viewModels<QuestionnaireViewModel>()
private lateinit var questionnaireConfig: QuestionnaireConfig
private lateinit var actionParameters: ArrayList<ActionParameter>
private lateinit var viewBinding: QuestionnaireActivityBinding
private var questionnaire: Questionnaire? = null
private var alertDialog: AlertDialog? = null
private lateinit var fusedLocationClient: FusedLocationProviderClient
var currentLocation: Location? = null
private lateinit var locationPermissionLauncher: ActivityResultLauncher<Array<String>>
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setTheme(org.smartregister.fhircore.engine.R.style.AppTheme_Questionnaire)
viewBinding = QuestionnaireActivityBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
Expand Down Expand Up @@ -97,6 +116,8 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() {

if (savedInstanceState == null) renderQuestionnaire()

setupLocationServices()

this.onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
Expand All @@ -107,6 +128,103 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() {
)
}

fun setupLocationServices() {
if (
viewModel.applicationConfiguration.logGpsLocation.contains(LocationLogOptions.QUESTIONNAIRE)
) {
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)

if (!LocationUtils.isLocationEnabled(this)) {
openLocationServicesSettings()
}

if (!hasLocationPermissions()) {
launchLocationPermissionsDialog()
}

if (LocationUtils.isLocationEnabled(this) && hasLocationPermissions()) {
fetchLocation(true)
}
}
}

fun hasLocationPermissions(): Boolean {
return PermissionUtils.checkPermissions(
this,
listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
),
)
}

fun openLocationServicesSettings() {
activityResultLauncher =
PermissionUtils.getStartActivityForResultLauncher(this) { resultCode, _ ->
if (resultCode == RESULT_OK || hasLocationPermissions()) {
fetchLocation()
}
}

val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
showLocationSettingsDialog(intent)
}

private fun showLocationSettingsDialog(intent: Intent) {
viewModel.setProgressState(QuestionnaireProgressState.QuestionnaireLaunch(false))
AlertDialog.Builder(this)
.setMessage(getString(R.string.location_services_disabled))
.setCancelable(true)
.setPositiveButton(getString(R.string.yes)) { _, _ -> activityResultLauncher.launch(intent) }
.setNegativeButton(getString(R.string.no)) { dialog, _ -> dialog.cancel() }
.show()
}

fun launchLocationPermissionsDialog() {
locationPermissionLauncher =
PermissionUtils.getLocationPermissionLauncher(
this,
onFineLocationPermissionGranted = { fetchLocation(true) },
onCoarseLocationPermissionGranted = { fetchLocation(false) },
onLocationPermissionDenied = {
Toast.makeText(
this,
getString(R.string.location_permissions_denied),
Toast.LENGTH_SHORT,
)
.show()
Timber.e("Location permissions denied")
},
)

locationPermissionLauncher.launch(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
),
)
}

fun fetchLocation(highAccuracy: Boolean = true) {
lifecycleScope.launch {
try {
if (highAccuracy) {
currentLocation =
LocationUtils.getAccurateLocation(fusedLocationClient, dispatcherProvider.io())
} else {
currentLocation =
LocationUtils.getApproximateLocation(fusedLocationClient, dispatcherProvider.io())
}
} catch (e: Exception) {
Timber.e(e, "Failed to get GPS location for questionnaire: ${questionnaireConfig.id}")
} finally {
if (currentLocation == null) {
this@QuestionnaireActivity.showToast("Failed to get GPS location", Toast.LENGTH_LONG)
}
}
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.clear()
Expand Down Expand Up @@ -241,6 +359,13 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() {
if (questionnaireResponse != null && questionnaire != null) {
viewModel.run {
setProgressState(QuestionnaireProgressState.ExtractionInProgress(true))

if (currentLocation != null) {
questionnaireResponse.contained.add(
ResourceUtils.createFhirLocationFromGpsLocation(gpsLocation = currentLocation!!),
)
}

handleQuestionnaireSubmission(
questionnaire = questionnaire!!,
currentQuestionnaireResponse = questionnaireResponse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,12 @@ import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.hl7.fhir.r4.model.StringType
import org.smartregister.fhircore.engine.BuildConfig
import org.smartregister.fhircore.engine.configuration.ConfigType
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.configuration.GroupResourceConfig
import org.smartregister.fhircore.engine.configuration.LinkIdType
import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig
import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration
import org.smartregister.fhircore.engine.data.local.DefaultRepository
import org.smartregister.fhircore.engine.domain.model.ActionParameter
import org.smartregister.fhircore.engine.domain.model.ActionParameterType
Expand Down Expand Up @@ -104,6 +107,7 @@ constructor(
val sharedPreferencesHelper: SharedPreferencesHelper,
val fhirOperator: FhirOperator,
val fhirPathDataExtractor: FhirPathDataExtractor,
val configurationRegistry: ConfigurationRegistry,
) : ViewModel() {
private val parser = FhirContext.forR4Cached().newJsonParser()

Expand All @@ -121,6 +125,10 @@ constructor(
val questionnaireProgressStateLiveData: LiveData<QuestionnaireProgressState?>
get() = _questionnaireProgressStateLiveData

val applicationConfiguration: ApplicationConfiguration by lazy {
configurationRegistry.retrieveConfiguration(ConfigType.Application)
}

/**
* This function retrieves the [Questionnaire] as configured via the [QuestionnaireConfig]. The
* retrieved [Questionnaire] can be pre-populated with computed values from the Rules engine.
Expand Down
Loading

0 comments on commit 402511d

Please sign in to comment.