From dae3eb29a1c6b4b7eb68845a77068ecc5abfdec4 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Thu, 18 Jan 2024 10:44:00 +0300 Subject: [PATCH 01/41] Add location permissions to get location details on submission --- .../app/ApplicationConfiguration.kt | 1 + android/quest/build.gradle.kts | 2 + android/quest/src/main/AndroidManifest.xml | 4 + .../ui/questionnaire/QuestionnaireActivity.kt | 78 +++++++++++++++++++ 4 files changed, 85 insertions(+) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt index 67ed65bb604..9a93d937dc6 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt @@ -41,4 +41,5 @@ data class ApplicationConfiguration( val showLogo: Boolean = true, val taskBackgroundWorkerBatchSize: Int = 500, val eventWorkflows: List = emptyList(), + val logQuestionnaireLocation: Boolean = true ) : Configuration() diff --git a/android/quest/build.gradle.kts b/android/quest/build.gradle.kts index 8ef2963d376..a7860d95236 100644 --- a/android/quest/build.gradle.kts +++ b/android/quest/build.gradle.kts @@ -369,6 +369,8 @@ dependencies { implementation(libs.dagger.hilt.android) implementation(libs.hilt.work) implementation(libs.cql.measure.evaluator) +// implementation("com.google.android.gms:play-services-location:{version}") + implementation("com.google.android.gms:play-services-location:21.0.1") // Annotation processors kapt(libs.hilt.compiler) diff --git a/android/quest/src/main/AndroidManifest.xml b/android/quest/src/main/AndroidManifest.xml index 670a510e67d..d9afaceb6b3 100644 --- a/android/quest/src/main/AndroidManifest.xml +++ b/android/quest/src/main/AndroidManifest.xml @@ -3,6 +3,10 @@ xmlns:tools="http://schemas.android.com/tools" package="org.smartregister.fhircore.quest"> + + + + + when{ + permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> + { + currentLocation = getCurrentLocation() + } + permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> + { + // request approx permissions + } + else -> { + // No location access granted. Notify and fail + } } } + + locationPermissionRequest.launch( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + ) + } else { + currentLocation = getCurrentLocation() } + return currentLocation + } + + @SuppressLint("MissingPermission") + private fun getCurrentLocation(): Location? { + var currentLocation: Location? = null + + fusedLocationClient + .getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null) + .addOnSuccessListener { location: Location? -> + currentLocation = location + } + + return currentLocation } private fun handleBackPress() { From 5762b8fbb0b2237b977fdaccfef480c567e6885c Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Thu, 18 Jan 2024 16:57:52 +0300 Subject: [PATCH 02/41] Edit current location --- .../fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 0590564fa28..c26f93878d2 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -246,7 +246,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { viewModel.run { setProgressState(QuestionnaireProgressState.ExtractionInProgress(true)) - val currentLocation = getCurrentLocation() + val currentLocation = getLocation() val loc = org.hl7.fhir.r4.model.Location() loc.position.altitude=currentLocation?.altitude?.toBigDecimal() From 9b4adb41461ec7c6655b2876eaaef97144bf1067 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Mon, 22 Jan 2024 17:26:18 +0300 Subject: [PATCH 03/41] Append precise and approximate location to the questionnaireResponse --- android/quest/src/main/AndroidManifest.xml | 1 + .../ui/questionnaire/QuestionnaireActivity.kt | 79 ++++++++++++------- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/android/quest/src/main/AndroidManifest.xml b/android/quest/src/main/AndroidManifest.xml index d9afaceb6b3..a2a04a9f46c 100644 --- a/android/quest/src/main/AndroidManifest.xml +++ b/android/quest/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" package="org.smartregister.fhircore.quest"> + diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index c26f93878d2..a3997f64b46 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -23,9 +23,11 @@ import android.app.AlertDialog import android.content.Intent import android.content.pm.PackageManager import android.location.Location +import android.location.LocationRequest import android.os.Bundle import android.os.Parcelable import android.view.View +import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels @@ -38,6 +40,9 @@ import com.google.android.fhir.logicalId import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import com.google.android.gms.location.Priority +import com.google.android.gms.tasks.CancellationToken +import com.google.android.gms.tasks.CancellationTokenSource +import com.google.android.gms.tasks.OnTokenCanceledListener import dagger.hilt.android.AndroidEntryPoint import java.io.Serializable import java.util.LinkedList @@ -46,7 +51,6 @@ 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.hl7.fhir.r4.model.Task import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.domain.model.ActionParameter import org.smartregister.fhircore.engine.domain.model.ActionParameterType @@ -73,11 +77,13 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { private var alertDialog: AlertDialog? = null private lateinit var fusedLocationClient: FusedLocationProviderClient + private val loc = org.hl7.fhir.r4.model.Location() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) + getLocation() setTheme(R.style.AppTheme_Questionnaire) viewBinding = QuestionnaireActivityBinding.inflate(layoutInflater) @@ -246,13 +252,6 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { viewModel.run { setProgressState(QuestionnaireProgressState.ExtractionInProgress(true)) - val currentLocation = getLocation() - - val loc = org.hl7.fhir.r4.model.Location() - loc.position.altitude=currentLocation?.altitude?.toBigDecimal() - loc.position.latitude=currentLocation?.latitude?.toBigDecimal() - loc.position.longitude=currentLocation?.longitude?.toBigDecimal() - questionnaireResponse.contained.add(loc) handleQuestionnaireSubmission( @@ -275,39 +274,39 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ) finish() } - - // our background processing } } } } - private fun getLocation(): Location? { - var currentLocation: Location ?= null - + private fun getLocation() { if (ActivityCompat.checkSelfPermission( - this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && - ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION - ) != PackageManager.PERMISSION_GRANTED - ) { - val locationPermissionRequest = this.registerForActivityResult( + this@QuestionnaireActivity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(this@QuestionnaireActivity, Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + val locationPermissionRequest = this@QuestionnaireActivity.registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions()) { permissions -> when{ permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> { - currentLocation = getCurrentLocation() + getCurrentLocation() } permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> { // request approx permissions + getCurrentApproximateLocation() } else -> { // No location access granted. Notify and fail + Toast.makeText( + this, "Location Access Permission not granted", + Toast.LENGTH_LONG + ).show() } } } - locationPermissionRequest.launch( arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, @@ -315,22 +314,48 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ) ) } else { - currentLocation = getCurrentLocation() + getCurrentLocation() } - return currentLocation } @SuppressLint("MissingPermission") - private fun getCurrentLocation(): Location? { - var currentLocation: Location? = null + private fun getCurrentLocation() { fusedLocationClient - .getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null) + .getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, object: CancellationToken(){ + override fun onCanceledRequested(p0: OnTokenCanceledListener) = CancellationTokenSource().token + + override fun isCancellationRequested() = false + }) .addOnSuccessListener { location: Location? -> - currentLocation = location + if (location != null) { + Timber.d("current location${location.latitude} ${location.longitude}") + loc.position.altitude=location.altitude.toBigDecimal() + loc.position.latitude=location.latitude.toBigDecimal() + loc.position.longitude=location.longitude.toBigDecimal() + } + } + + } + + @SuppressLint("MissingPermission") + private fun getCurrentApproximateLocation() { + + fusedLocationClient + .getCurrentLocation(Priority.PRIORITY_BALANCED_POWER_ACCURACY, object: CancellationToken(){ + override fun onCanceledRequested(p0: OnTokenCanceledListener) = CancellationTokenSource().token + + override fun isCancellationRequested() = false + }) + .addOnSuccessListener { approxLocation: Location? -> + if (approxLocation != null) { + Timber.d("current location${approxLocation.latitude} ${approxLocation.longitude}") + loc.position.altitude=approxLocation.altitude.toBigDecimal() + loc.position.latitude=approxLocation.latitude.toBigDecimal() + loc.position.longitude=approxLocation.longitude.toBigDecimal() + } } - return currentLocation } private fun handleBackPress() { From efc080d23e7ab29cf4a5a95f3cd2405761c77982 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Tue, 23 Jan 2024 11:19:27 +0300 Subject: [PATCH 04/41] Add application config checker --- .../engine/configuration/app/ApplicationConfiguration.kt | 2 +- .../quest/ui/questionnaire/QuestionnaireActivity.kt | 2 ++ .../quest/ui/questionnaire/QuestionnaireViewModel.kt | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt index 651047ac2d7..db0ce1b2deb 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt @@ -41,5 +41,5 @@ data class ApplicationConfiguration( val showLogo: Boolean = true, val taskBackgroundWorkerBatchSize: Int = 500, val eventWorkflows: List = emptyList(), - val logQuestionnaireLocation: Boolean = true + val logQuestionnaireLocation: Boolean = false ) : Configuration() diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 8b87d7e1b53..b2f80291300 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -31,6 +31,7 @@ import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels +import androidx.compose.runtime.remember import androidx.core.app.ActivityCompat import androidx.core.os.bundleOf import androidx.fragment.app.commit @@ -78,6 +79,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { private lateinit var fusedLocationClient: FusedLocationProviderClient private val loc = org.hl7.fhir.r4.model.Location() + val applicationConfiguration = viewModel.applicationConfiguration override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt index ad061afbb08..bbb32d97089 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt @@ -51,8 +51,11 @@ import org.hl7.fhir.r4.model.RelatedPerson 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.configuration.ConfigType +import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.GroupResourceConfig 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 @@ -95,6 +98,7 @@ constructor( val sharedPreferencesHelper: SharedPreferencesHelper, val fhirOperator: FhirOperator, val fhirPathDataExtractor: FhirPathDataExtractor, + val configurationRegistry: ConfigurationRegistry ) : ViewModel() { private val authenticatedOrganizationIds by lazy { @@ -111,6 +115,10 @@ constructor( val questionnaireProgressStateLiveData: LiveData 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. From 6404c514e0314c858930bbadd1e130a0ca1e898e Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Tue, 23 Jan 2024 16:16:52 +0300 Subject: [PATCH 05/41] Add logQuestionnaireLocation config to toggle functionality --- .../ui/questionnaire/QuestionnaireActivity.kt | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index b2f80291300..30447f39ebd 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -23,15 +23,14 @@ import android.app.AlertDialog import android.content.Intent import android.content.pm.PackageManager import android.location.Location -import android.location.LocationRequest 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.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.compose.runtime.remember import androidx.core.app.ActivityCompat import androidx.core.os.bundleOf import androidx.fragment.app.commit @@ -79,13 +78,17 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { private lateinit var fusedLocationClient: FusedLocationProviderClient private val loc = org.hl7.fhir.r4.model.Location() - val applicationConfiguration = viewModel.applicationConfiguration +// val applicationConfiguration = viewModel.applicationConfiguration override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) - getLocation() + val applicationConfiguration = viewModel.applicationConfiguration + + if (applicationConfiguration.logQuestionnaireLocation) { + fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) + getLocation() + } setTheme(R.style.AppTheme_Questionnaire) viewBinding = QuestionnaireActivityBinding.inflate(layoutInflater) @@ -338,7 +341,11 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { loc.position.longitude=location.longitude.toBigDecimal() } } - + .addOnFailureListener { + // Location services are not enabled, prompt the user to enable them + val settingsIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + this.startActivity(settingsIntent) + } } @SuppressLint("MissingPermission") @@ -358,6 +365,11 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { loc.position.longitude=approxLocation.longitude.toBigDecimal() } } + .addOnFailureListener { + // Location services are not enabled, prompt the user to enable them + val settingsIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + this.startActivity(settingsIntent) + } } From e88b47cefbf9fe47b8a336c04f51dd3381342996 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Tue, 23 Jan 2024 16:30:46 +0300 Subject: [PATCH 06/41] Run SpotlessApply --- .../app/ApplicationConfiguration.kt | 2 +- .../lint/CustomRuleSetProvider.kt | 2 +- .../FhirEngineCreateUpdateMethodCallRule.kt | 2 +- .../org/smartregister/lint/SecurityUtil2.kt | 2 +- .../lint/CustomRuleSetProvider.kt | 2 +- .../FhirEngineCreateUpdateMethodCallRule.kt | 2 +- .../org/smartregister/lint/SecurityUtil2.kt | 2 +- .../HouseholdProfileBenchmark.kt | 2 +- .../HouseholdRegisterMacrobenchmark.kt | 2 +- android/quest/build.gradle.kts | 2 +- .../ui/questionnaire/QuestionnaireActivity.kt | 100 ++++++++++-------- .../questionnaire/QuestionnaireViewModel.kt | 2 +- 12 files changed, 67 insertions(+), 55 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt index db0ce1b2deb..7bc2ac074f6 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt @@ -41,5 +41,5 @@ data class ApplicationConfiguration( val showLogo: Boolean = true, val taskBackgroundWorkerBatchSize: Int = 500, val eventWorkflows: List = emptyList(), - val logQuestionnaireLocation: Boolean = false + val logQuestionnaireLocation: Boolean = false, ) : Configuration() diff --git a/android/linting/bin/main/org/smartregister/lint/CustomRuleSetProvider.kt b/android/linting/bin/main/org/smartregister/lint/CustomRuleSetProvider.kt index f7ec3ebd2de..3f0562405a6 100644 --- a/android/linting/bin/main/org/smartregister/lint/CustomRuleSetProvider.kt +++ b/android/linting/bin/main/org/smartregister/lint/CustomRuleSetProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2024 Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/android/linting/bin/main/org/smartregister/lint/FhirEngineCreateUpdateMethodCallRule.kt b/android/linting/bin/main/org/smartregister/lint/FhirEngineCreateUpdateMethodCallRule.kt index 6ac14bd521a..30c23a6f0a6 100644 --- a/android/linting/bin/main/org/smartregister/lint/FhirEngineCreateUpdateMethodCallRule.kt +++ b/android/linting/bin/main/org/smartregister/lint/FhirEngineCreateUpdateMethodCallRule.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2024 Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/android/linting/bin/main/org/smartregister/lint/SecurityUtil2.kt b/android/linting/bin/main/org/smartregister/lint/SecurityUtil2.kt index f898fc25cca..841f68f106b 100644 --- a/android/linting/bin/main/org/smartregister/lint/SecurityUtil2.kt +++ b/android/linting/bin/main/org/smartregister/lint/SecurityUtil2.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2024 Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/android/linting/src/main/java/org/smartregister/lint/CustomRuleSetProvider.kt b/android/linting/src/main/java/org/smartregister/lint/CustomRuleSetProvider.kt index f7ec3ebd2de..3f0562405a6 100644 --- a/android/linting/src/main/java/org/smartregister/lint/CustomRuleSetProvider.kt +++ b/android/linting/src/main/java/org/smartregister/lint/CustomRuleSetProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2024 Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/android/linting/src/main/java/org/smartregister/lint/FhirEngineCreateUpdateMethodCallRule.kt b/android/linting/src/main/java/org/smartregister/lint/FhirEngineCreateUpdateMethodCallRule.kt index 6ac14bd521a..30c23a6f0a6 100644 --- a/android/linting/src/main/java/org/smartregister/lint/FhirEngineCreateUpdateMethodCallRule.kt +++ b/android/linting/src/main/java/org/smartregister/lint/FhirEngineCreateUpdateMethodCallRule.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2024 Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/android/linting/src/main/java/org/smartregister/lint/SecurityUtil2.kt b/android/linting/src/main/java/org/smartregister/lint/SecurityUtil2.kt index f898fc25cca..841f68f106b 100644 --- a/android/linting/src/main/java/org/smartregister/lint/SecurityUtil2.kt +++ b/android/linting/src/main/java/org/smartregister/lint/SecurityUtil2.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2024 Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/android/macrobenchmark/src/main/java/org/smartregister/opensrp/quest/macrobenchmark/HouseholdProfileBenchmark.kt b/android/macrobenchmark/src/main/java/org/smartregister/opensrp/quest/macrobenchmark/HouseholdProfileBenchmark.kt index ad49ac8bbd6..7c663008922 100644 --- a/android/macrobenchmark/src/main/java/org/smartregister/opensrp/quest/macrobenchmark/HouseholdProfileBenchmark.kt +++ b/android/macrobenchmark/src/main/java/org/smartregister/opensrp/quest/macrobenchmark/HouseholdProfileBenchmark.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2024 Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/android/macrobenchmark/src/main/java/org/smartregister/opensrp/quest/macrobenchmark/HouseholdRegisterMacrobenchmark.kt b/android/macrobenchmark/src/main/java/org/smartregister/opensrp/quest/macrobenchmark/HouseholdRegisterMacrobenchmark.kt index bab0d46da97..d4d33444ad3 100644 --- a/android/macrobenchmark/src/main/java/org/smartregister/opensrp/quest/macrobenchmark/HouseholdRegisterMacrobenchmark.kt +++ b/android/macrobenchmark/src/main/java/org/smartregister/opensrp/quest/macrobenchmark/HouseholdRegisterMacrobenchmark.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2024 Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/android/quest/build.gradle.kts b/android/quest/build.gradle.kts index f9f0afa30de..ecca33345a9 100644 --- a/android/quest/build.gradle.kts +++ b/android/quest/build.gradle.kts @@ -381,7 +381,7 @@ dependencies { implementation(libs.dagger.hilt.android) implementation(libs.hilt.work) implementation(libs.cql.measure.evaluator) -// implementation("com.google.android.gms:play-services-location:{version}") + // implementation("com.google.android.gms:play-services-location:{version}") implementation("com.google.android.gms:play-services-location:21.0.1") // Annotation processors diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 30447f39ebd..dfb08e05ea3 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -78,7 +78,6 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { private lateinit var fusedLocationClient: FusedLocationProviderClient private val loc = org.hl7.fhir.r4.model.Location() -// val applicationConfiguration = viewModel.applicationConfiguration override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -280,44 +279,52 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ) finish() } + // do background location processing + // check here that "logQuestionnaireLocation": true } } } } private fun getLocation() { - if (ActivityCompat.checkSelfPermission( - this@QuestionnaireActivity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && - ActivityCompat.checkSelfPermission(this@QuestionnaireActivity, Manifest.permission.ACCESS_COARSE_LOCATION - ) != PackageManager.PERMISSION_GRANTED + if ( + ActivityCompat.checkSelfPermission( + this@QuestionnaireActivity, + Manifest.permission.ACCESS_FINE_LOCATION, + ) != PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission( + this@QuestionnaireActivity, + Manifest.permission.ACCESS_COARSE_LOCATION, + ) != PackageManager.PERMISSION_GRANTED ) { - val locationPermissionRequest = this@QuestionnaireActivity.registerForActivityResult( - ActivityResultContracts.RequestMultiplePermissions()) - { permissions -> - when{ - permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> - { - getCurrentLocation() - } - permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> - { - // request approx permissions - getCurrentApproximateLocation() - } - else -> { - // No location access granted. Notify and fail - Toast.makeText( - this, "Location Access Permission not granted", - Toast.LENGTH_LONG - ).show() + val locationPermissionRequest = + this@QuestionnaireActivity.registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions(), + ) { permissions -> + when { + permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> { + getCurrentLocation() + } + permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> { + // request approx permissions + getCurrentApproximateLocation() + } + else -> { + // No location access granted. Notify and fail + Toast.makeText( + this, + "Location Access Permission not granted", + Toast.LENGTH_LONG, + ) + .show() + } } } - } locationPermissionRequest.launch( arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION - ) + Manifest.permission.ACCESS_COARSE_LOCATION, + ), ) } else { getCurrentLocation() @@ -326,19 +333,22 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { @SuppressLint("MissingPermission") private fun getCurrentLocation() { - fusedLocationClient - .getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, object: CancellationToken(){ - override fun onCanceledRequested(p0: OnTokenCanceledListener) = CancellationTokenSource().token + .getCurrentLocation( + Priority.PRIORITY_HIGH_ACCURACY, + object : CancellationToken() { + override fun onCanceledRequested(p0: OnTokenCanceledListener) = + CancellationTokenSource().token - override fun isCancellationRequested() = false - }) + override fun isCancellationRequested() = false + }, + ) .addOnSuccessListener { location: Location? -> if (location != null) { Timber.d("current location${location.latitude} ${location.longitude}") - loc.position.altitude=location.altitude.toBigDecimal() - loc.position.latitude=location.latitude.toBigDecimal() - loc.position.longitude=location.longitude.toBigDecimal() + loc.position.altitude = location.altitude.toBigDecimal() + loc.position.latitude = location.latitude.toBigDecimal() + loc.position.longitude = location.longitude.toBigDecimal() } } .addOnFailureListener { @@ -350,19 +360,22 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { @SuppressLint("MissingPermission") private fun getCurrentApproximateLocation() { - fusedLocationClient - .getCurrentLocation(Priority.PRIORITY_BALANCED_POWER_ACCURACY, object: CancellationToken(){ - override fun onCanceledRequested(p0: OnTokenCanceledListener) = CancellationTokenSource().token + .getCurrentLocation( + Priority.PRIORITY_BALANCED_POWER_ACCURACY, + object : CancellationToken() { + override fun onCanceledRequested(p0: OnTokenCanceledListener) = + CancellationTokenSource().token - override fun isCancellationRequested() = false - }) + override fun isCancellationRequested() = false + }, + ) .addOnSuccessListener { approxLocation: Location? -> if (approxLocation != null) { Timber.d("current location${approxLocation.latitude} ${approxLocation.longitude}") - loc.position.altitude=approxLocation.altitude.toBigDecimal() - loc.position.latitude=approxLocation.latitude.toBigDecimal() - loc.position.longitude=approxLocation.longitude.toBigDecimal() + loc.position.altitude = approxLocation.altitude.toBigDecimal() + loc.position.latitude = approxLocation.latitude.toBigDecimal() + loc.position.longitude = approxLocation.longitude.toBigDecimal() } } .addOnFailureListener { @@ -370,7 +383,6 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { val settingsIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) this.startActivity(settingsIntent) } - } private fun handleBackPress() { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt index bbb32d97089..3602c59af4d 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt @@ -98,7 +98,7 @@ constructor( val sharedPreferencesHelper: SharedPreferencesHelper, val fhirOperator: FhirOperator, val fhirPathDataExtractor: FhirPathDataExtractor, - val configurationRegistry: ConfigurationRegistry + val configurationRegistry: ConfigurationRegistry, ) : ViewModel() { private val authenticatedOrganizationIds by lazy { From a979c2607dd06b935eb2c73bceb96a3f9a9dd80e Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Fri, 2 Feb 2024 01:04:13 +0300 Subject: [PATCH 07/41] Run location calculation in background and add user checks --- .../ui/questionnaire/QuestionnaireActivity.kt | 182 +++++++++++------- .../util/extensions/ContextExtentions.kt | 17 ++ 2 files changed, 128 insertions(+), 71 deletions(-) create mode 100644 android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ContextExtentions.kt diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 4ef2b123a31..eb8b4fb35e7 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -21,7 +21,6 @@ import android.annotation.SuppressLint import android.app.Activity import android.app.AlertDialog import android.content.Intent -import android.content.pm.PackageManager import android.location.Location import android.os.Bundle import android.os.Parcelable @@ -31,7 +30,6 @@ import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.core.app.ActivityCompat import androidx.core.os.bundleOf import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope @@ -39,14 +37,18 @@ 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 com.google.android.gms.location.LocationSettingsRequest +import com.google.android.gms.location.LocationSettingsResponse import com.google.android.gms.location.Priority +import com.google.android.gms.location.SettingsClient import com.google.android.gms.tasks.CancellationToken import com.google.android.gms.tasks.CancellationTokenSource import com.google.android.gms.tasks.OnTokenCanceledListener +import com.google.android.gms.tasks.Task import dagger.hilt.android.AndroidEntryPoint -import java.io.Serializable -import java.util.LinkedList +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Resource @@ -65,7 +67,11 @@ 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.extensions.hasLocationPermission import timber.log.Timber +import java.io.Serializable +import java.util.LinkedList +import java.util.UUID @AndroidEntryPoint class QuestionnaireActivity : BaseMultiLanguageActivity() { @@ -80,6 +86,8 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { private lateinit var fusedLocationClient: FusedLocationProviderClient private val loc = org.hl7.fhir.r4.model.Location() + private var currLocation: Location? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -87,7 +95,25 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { if (applicationConfiguration.logQuestionnaireLocation) { fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) - getLocation() + + val builder = LocationSettingsRequest.Builder() + val client: SettingsClient = LocationServices.getSettingsClient(this) + val result: Task = client.checkLocationSettings(builder.build()) + result.addOnSuccessListener { _ -> + AlertDialog.Builder(this) + .setMessage("Location services are disabled. Do you want to enable them?") + .setCancelable(false) + .setPositiveButton("Yes") { _, _ -> + // Open GPS settings + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + startActivity(intent) + } + .setNegativeButton("No") { dialog, _ -> + dialog.cancel() + } + .show() + } + lifecycleScope.launch { getLocationPermission()} } setTheme(R.style.AppTheme_Questionnaire) @@ -263,6 +289,9 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { setProgressState(QuestionnaireProgressState.ExtractionInProgress(true)) questionnaireResponse.contained.add(loc) + if (currLocation != null) { + questionnaireResponse.contained.add(createLocationResource(currLocation)) + } handleQuestionnaireSubmission( questionnaire = questionnaire!!, @@ -284,43 +313,32 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ) finish() } - // do background location processing - // check here that "logQuestionnaireLocation": true } } } } - private fun getLocation() { - if ( - ActivityCompat.checkSelfPermission( - this@QuestionnaireActivity, - Manifest.permission.ACCESS_FINE_LOCATION, - ) != PackageManager.PERMISSION_GRANTED && - ActivityCompat.checkSelfPermission( - this@QuestionnaireActivity, - Manifest.permission.ACCESS_COARSE_LOCATION, - ) != PackageManager.PERMISSION_GRANTED - ) { + private fun getLocationPermission() { + if (!this@QuestionnaireActivity.hasLocationPermission()) { val locationPermissionRequest = this@QuestionnaireActivity.registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions(), ) { permissions -> when { permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> { - getCurrentLocation() + lifecycleScope.launch { getCurrentLocation() } } permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> { // request approx permissions - getCurrentApproximateLocation() + lifecycleScope.launch { getCurrentApproximateLocation()} } else -> { // No location access granted. Notify and fail Toast.makeText( - this, - "Location Access Permission not granted", - Toast.LENGTH_LONG, - ) + this@QuestionnaireActivity, + "Location currently disabled due to denied permission.", + Toast.LENGTH_LONG, + ) .show() } } @@ -332,64 +350,86 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ), ) } else { - getCurrentLocation() + lifecycleScope.launch { getCurrentLocation() } } } @SuppressLint("MissingPermission") - private fun getCurrentLocation() { - fusedLocationClient - .getCurrentLocation( - Priority.PRIORITY_HIGH_ACCURACY, - object : CancellationToken() { - override fun onCanceledRequested(p0: OnTokenCanceledListener) = - CancellationTokenSource().token - - override fun isCancellationRequested() = false - }, - ) - .addOnSuccessListener { location: Location? -> - if (location != null) { - Timber.d("current location${location.latitude} ${location.longitude}") - loc.position.altitude = location.altitude.toBigDecimal() - loc.position.latitude = location.latitude.toBigDecimal() - loc.position.longitude = location.longitude.toBigDecimal() + private suspend fun getCurrentLocation() { + withContext(Dispatchers.IO){ + fusedLocationClient + .getCurrentLocation( + Priority.PRIORITY_HIGH_ACCURACY, + object : CancellationToken() { + override fun onCanceledRequested(p0: OnTokenCanceledListener) = + CancellationTokenSource().token + + override fun isCancellationRequested() = false + }, + ) + .addOnSuccessListener { location: Location? -> + if (location != null) { + Timber.d("current location${location.latitude} ${location.longitude}") + loc.position.altitude = location.altitude.toBigDecimal() + loc.position.latitude = location.latitude.toBigDecimal() + loc.position.longitude = location.longitude.toBigDecimal() + Timber.d( + "Precise location - latitude: ${location.latitude}; longitude: ${location.longitude}; altitude: ${location.altitude}", + ) + currLocation = location + } else { + + } } - } - .addOnFailureListener { - // Location services are not enabled, prompt the user to enable them - val settingsIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) - this.startActivity(settingsIntent) - } + .addOnFailureListener {e -> + Timber.e(e, "Failed to get current location") + // Notify user about failure + Toast.makeText( + this@QuestionnaireActivity, + "Failed to get current location: ${e.message}", + Toast.LENGTH_LONG + ).show() + } + } } @SuppressLint("MissingPermission") - private fun getCurrentApproximateLocation() { - fusedLocationClient - .getCurrentLocation( - Priority.PRIORITY_BALANCED_POWER_ACCURACY, - object : CancellationToken() { - override fun onCanceledRequested(p0: OnTokenCanceledListener) = - CancellationTokenSource().token - - override fun isCancellationRequested() = false - }, - ) - .addOnSuccessListener { approxLocation: Location? -> - if (approxLocation != null) { - Timber.d("current location${approxLocation.latitude} ${approxLocation.longitude}") - loc.position.altitude = approxLocation.altitude.toBigDecimal() - loc.position.latitude = approxLocation.latitude.toBigDecimal() - loc.position.longitude = approxLocation.longitude.toBigDecimal() + private suspend fun getCurrentApproximateLocation() { + withContext(Dispatchers.IO) { + fusedLocationClient + .getCurrentLocation( + Priority.PRIORITY_BALANCED_POWER_ACCURACY, + object : CancellationToken() { + override fun onCanceledRequested(p0: OnTokenCanceledListener) = + CancellationTokenSource().token + + override fun isCancellationRequested() = false + }, + ) + .addOnSuccessListener { approxLocation: Location? -> + if (approxLocation != null) { + Timber.d("current location${approxLocation.latitude} ${approxLocation.longitude}") + loc.position.altitude = approxLocation.altitude.toBigDecimal() + loc.position.latitude = approxLocation.latitude.toBigDecimal() + loc.position.longitude = approxLocation.longitude.toBigDecimal() + } } - } - .addOnFailureListener { - // Location services are not enabled, prompt the user to enable them - val settingsIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) - this.startActivity(settingsIntent) - } + .addOnFailureListener { + + } + } } + private fun createLocationResource(gpsLocation: Location?): org.hl7.fhir.r4.model.Location { + var locationResource = org.hl7.fhir.r4.model.Location() + locationResource.id = UUID.randomUUID().toString() + locationResource.position.latitude = gpsLocation!!.latitude.toBigDecimal() + locationResource.position.longitude = gpsLocation!!.longitude.toBigDecimal() + locationResource.position.altitude = gpsLocation!!.altitude.toBigDecimal() + + return locationResource + } + private fun handleBackPress() { if (questionnaireConfig.isReadOnly()) { finish() diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ContextExtentions.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ContextExtentions.kt new file mode 100644 index 00000000000..ad7888452c7 --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ContextExtentions.kt @@ -0,0 +1,17 @@ +package org.smartregister.fhircore.quest.util.extensions + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat + +fun Context.hasLocationPermission(): Boolean { + return ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED +} \ No newline at end of file From 9909f008e7a87fe229aa15977427037f7928b947 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Fri, 2 Feb 2024 13:25:55 +0300 Subject: [PATCH 08/41] Update build gradle --- android/quest/build.gradle.kts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/android/quest/build.gradle.kts b/android/quest/build.gradle.kts index ecca33345a9..89df3b75745 100644 --- a/android/quest/build.gradle.kts +++ b/android/quest/build.gradle.kts @@ -381,8 +381,7 @@ dependencies { implementation(libs.dagger.hilt.android) implementation(libs.hilt.work) implementation(libs.cql.measure.evaluator) - // implementation("com.google.android.gms:play-services-location:{version}") - implementation("com.google.android.gms:play-services-location:21.0.1") + implementation(libs.play.services.location) // Annotation processors kapt(libs.hilt.compiler) From 16cd64737089e30ec0f02462c5b95a5602c82e8c Mon Sep 17 00:00:00 2001 From: Simon Njoroge Date: Tue, 6 Feb 2024 08:48:33 +0300 Subject: [PATCH 09/41] Code refactors --- android/gradle/libs.versions.toml | 2 + android/quest/src/main/AndroidManifest.xml | 8 +- .../ui/questionnaire/QuestionnaireActivity.kt | 126 +++++++++--------- .../fhircore/quest/util/LocationUtils.kt | 31 +++++ .../fhircore/quest/util/PermissionUtils.kt | 32 +++++ .../util/extensions/ContextExtentions.kt | 34 +++-- 6 files changed, 153 insertions(+), 80 deletions(-) create mode 100644 android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt create mode 100644 android/quest/src/main/java/org/smartregister/fhircore/quest/util/PermissionUtils.kt diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 79949fc6c0e..9b01d1fbfa7 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -81,6 +81,7 @@ 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" preference-ktx = "1.2.1" prettytime = "5.0.2.Final" retrofit = "2.9.0" @@ -202,6 +203,7 @@ orchestrator = { group = "androidx.test", name = "orchestrator", version.ref = " p2p-lib = { group = "org.smartregister", name = "p2p-lib", version.ref = "p2p-lib" } 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" } diff --git a/android/quest/src/main/AndroidManifest.xml b/android/quest/src/main/AndroidManifest.xml index a2a04a9f46c..6e3f468278d 100644 --- a/android/quest/src/main/AndroidManifest.xml +++ b/android/quest/src/main/AndroidManifest.xml @@ -3,11 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" package="org.smartregister.fhircore.quest"> - - - - - + + + diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index eb8b4fb35e7..7528eae192b 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -16,6 +16,7 @@ package org.smartregister.fhircore.quest.ui.questionnaire +import PermissionUtils import android.Manifest import android.annotation.SuppressLint import android.app.Activity @@ -28,6 +29,7 @@ import android.provider.Settings import android.view.View import android.widget.Toast import androidx.activity.OnBackPressedCallback +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.core.os.bundleOf @@ -37,15 +39,14 @@ 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 com.google.android.gms.location.LocationSettingsRequest -import com.google.android.gms.location.LocationSettingsResponse import com.google.android.gms.location.Priority -import com.google.android.gms.location.SettingsClient import com.google.android.gms.tasks.CancellationToken import com.google.android.gms.tasks.CancellationTokenSource import com.google.android.gms.tasks.OnTokenCanceledListener -import com.google.android.gms.tasks.Task import dagger.hilt.android.AndroidEntryPoint +import java.io.Serializable +import java.util.LinkedList +import java.util.UUID import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -67,11 +68,8 @@ 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.extensions.hasLocationPermission +import org.smartregister.fhircore.quest.util.LocationUtils import timber.log.Timber -import java.io.Serializable -import java.util.LinkedList -import java.util.UUID @AndroidEntryPoint class QuestionnaireActivity : BaseMultiLanguageActivity() { @@ -84,44 +82,42 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { private var alertDialog: AlertDialog? = null private lateinit var fusedLocationClient: FusedLocationProviderClient - private val loc = org.hl7.fhir.r4.model.Location() - private var currLocation: Location? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val applicationConfiguration = viewModel.applicationConfiguration + setTheme(R.style.AppTheme_Questionnaire) + viewBinding = QuestionnaireActivityBinding.inflate(layoutInflater) + setContentView(viewBinding.root) + with(intent) { + parcelable(QUESTIONNAIRE_CONFIG)?.also { questionnaireConfig = it } + actionParameters = parcelableArrayList(QUESTIONNAIRE_ACTION_PARAMETERS) ?: arrayListOf() + } - if (applicationConfiguration.logQuestionnaireLocation) { + if (true || viewModel.applicationConfiguration.logQuestionnaireLocation) { fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) - val builder = LocationSettingsRequest.Builder() - val client: SettingsClient = LocationServices.getSettingsClient(this) - val result: Task = client.checkLocationSettings(builder.build()) - result.addOnSuccessListener { _ -> + if (LocationUtils.isLocationEnabled(this)) { + getLocationPermission() + } else { + val locationSettingsLauncher: ActivityResultLauncher = + this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (LocationUtils.isLocationEnabled(this)) { + getLocationPermission() + } + } + AlertDialog.Builder(this) .setMessage("Location services are disabled. Do you want to enable them?") - .setCancelable(false) + .setCancelable(true) .setPositiveButton("Yes") { _, _ -> - // Open GPS settings val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) - startActivity(intent) - } - .setNegativeButton("No") { dialog, _ -> - dialog.cancel() + locationSettingsLauncher.launch(intent) } + .setNegativeButton("No") { dialog, _ -> dialog.cancel() } .show() } - lifecycleScope.launch { getLocationPermission()} - } - - setTheme(R.style.AppTheme_Questionnaire) - viewBinding = QuestionnaireActivityBinding.inflate(layoutInflater) - setContentView(viewBinding.root) - with(intent) { - parcelable(QUESTIONNAIRE_CONFIG)?.also { questionnaireConfig = it } - actionParameters = parcelableArrayList(QUESTIONNAIRE_ACTION_PARAMETERS) ?: arrayListOf() } if (!::questionnaireConfig.isInitialized) { @@ -288,7 +284,6 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { viewModel.run { setProgressState(QuestionnaireProgressState.ExtractionInProgress(true)) - questionnaireResponse.contained.add(loc) if (currLocation != null) { questionnaireResponse.contained.add(createLocationResource(currLocation)) } @@ -319,7 +314,15 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { } private fun getLocationPermission() { - if (!this@QuestionnaireActivity.hasLocationPermission()) { + if ( + !PermissionUtils.checkPermissions( + this, + listOf( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION + ), + ) + ) { val locationPermissionRequest = this@QuestionnaireActivity.registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions(), @@ -329,16 +332,14 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { lifecycleScope.launch { getCurrentLocation() } } permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> { - // request approx permissions - lifecycleScope.launch { getCurrentApproximateLocation()} + lifecycleScope.launch { getCurrentApproximateLocation() } } else -> { - // No location access granted. Notify and fail Toast.makeText( - this@QuestionnaireActivity, - "Location currently disabled due to denied permission.", - Toast.LENGTH_LONG, - ) + this@QuestionnaireActivity, + "Location currently disabled due to denied permission.", + Toast.LENGTH_LONG, + ) .show() } } @@ -350,13 +351,13 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ), ) } else { - lifecycleScope.launch { getCurrentLocation() } + lifecycleScope.launch { getCurrentLocation() } } } @SuppressLint("MissingPermission") private suspend fun getCurrentLocation() { - withContext(Dispatchers.IO){ + withContext(Dispatchers.IO) { fusedLocationClient .getCurrentLocation( Priority.PRIORITY_HIGH_ACCURACY, @@ -369,26 +370,21 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ) .addOnSuccessListener { location: Location? -> if (location != null) { - Timber.d("current location${location.latitude} ${location.longitude}") - loc.position.altitude = location.altitude.toBigDecimal() - loc.position.latitude = location.latitude.toBigDecimal() - loc.position.longitude = location.longitude.toBigDecimal() + currLocation = location Timber.d( - "Precise location - latitude: ${location.latitude}; longitude: ${location.longitude}; altitude: ${location.altitude}", + "Location - lat: ${location.latitude}; long: ${location.longitude}; alt: ${location.altitude}", ) - currLocation = location - } else { - - } + } else {} } - .addOnFailureListener {e -> + .addOnFailureListener { e -> Timber.e(e, "Failed to get current location") // Notify user about failure Toast.makeText( - this@QuestionnaireActivity, - "Failed to get current location: ${e.message}", - Toast.LENGTH_LONG - ).show() + this@QuestionnaireActivity, + "Failed to get current location: ${e.message}", + Toast.LENGTH_LONG, + ) + .show() } } } @@ -406,17 +402,15 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { override fun isCancellationRequested() = false }, ) - .addOnSuccessListener { approxLocation: Location? -> - if (approxLocation != null) { - Timber.d("current location${approxLocation.latitude} ${approxLocation.longitude}") - loc.position.altitude = approxLocation.altitude.toBigDecimal() - loc.position.latitude = approxLocation.latitude.toBigDecimal() - loc.position.longitude = approxLocation.longitude.toBigDecimal() + .addOnSuccessListener { location: Location? -> + if (location != null) { + currLocation = location + Timber.d( + "Location - lat: ${location.latitude}; long: ${location.longitude}; alt: ${location.altitude}", + ) } } - .addOnFailureListener { - - } + .addOnFailureListener {} } } @@ -428,7 +422,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { locationResource.position.altitude = gpsLocation!!.altitude.toBigDecimal() return locationResource - } + } private fun handleBackPress() { if (questionnaireConfig.isReadOnly()) { diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt new file mode 100644 index 00000000000..a265108570b --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.quest.util + +import android.content.Context +import android.location.LocationManager + +class LocationUtils { + + companion object { + fun isLocationEnabled(context: Context): Boolean { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || + locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } + } +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/PermissionUtils.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/PermissionUtils.kt new file mode 100644 index 00000000000..895563f0b16 --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/PermissionUtils.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat + +object PermissionUtils { + fun checkPermissions(context: Context, permissions: List): Boolean { + for (permission in permissions) { + if ( + ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED + ) { + return false + } + } + return true + } +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ContextExtentions.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ContextExtentions.kt index ad7888452c7..aab508c594e 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ContextExtentions.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ContextExtentions.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.smartregister.fhircore.quest.util.extensions import android.Manifest @@ -6,12 +22,12 @@ import android.content.pm.PackageManager import androidx.core.content.ContextCompat fun Context.hasLocationPermission(): Boolean { - return ContextCompat.checkSelfPermission( - this, - Manifest.permission.ACCESS_COARSE_LOCATION - ) == PackageManager.PERMISSION_GRANTED && - ContextCompat.checkSelfPermission( - this, - Manifest.permission.ACCESS_FINE_LOCATION - ) == PackageManager.PERMISSION_GRANTED -} \ No newline at end of file + return ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_COARSE_LOCATION, + ) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION, + ) == PackageManager.PERMISSION_GRANTED +} From 03141dcee6329c36eb3e6127808476c6e362b1b4 Mon Sep 17 00:00:00 2001 From: Simon Njoroge Date: Tue, 6 Feb 2024 08:51:38 +0300 Subject: [PATCH 10/41] Code cleanup --- .../fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 7528eae192b..665b32df464 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -95,7 +95,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { actionParameters = parcelableArrayList(QUESTIONNAIRE_ACTION_PARAMETERS) ?: arrayListOf() } - if (true || viewModel.applicationConfiguration.logQuestionnaireLocation) { + if (viewModel.applicationConfiguration.logQuestionnaireLocation) { fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) if (LocationUtils.isLocationEnabled(this)) { From f018e71daca0c0f30d2d186a90b0a4c9d6013789 Mon Sep 17 00:00:00 2001 From: Simon Njoroge Date: Tue, 6 Feb 2024 18:29:40 +0300 Subject: [PATCH 11/41] Refactor location implementation --- .../ui/questionnaire/QuestionnaireActivity.kt | 241 +++++++----------- .../fhircore/quest/util/LocationUtils.kt | 75 ++++++ .../fhircore/quest/util/PermissionUtils.kt | 38 +++ .../fhircore/quest/util/ResourceUtils.kt | 42 +++ .../util/extensions/ContextExtentions.kt | 33 --- android/quest/src/main/res/values/strings.xml | 3 + 6 files changed, 251 insertions(+), 181 deletions(-) create mode 100644 android/quest/src/main/java/org/smartregister/fhircore/quest/util/ResourceUtils.kt delete mode 100644 android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ContextExtentions.kt diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 665b32df464..30147946a7e 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -16,9 +16,7 @@ package org.smartregister.fhircore.quest.ui.questionnaire -import PermissionUtils import android.Manifest -import android.annotation.SuppressLint import android.app.Activity import android.app.AlertDialog import android.content.Intent @@ -30,7 +28,6 @@ import android.view.View import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.core.os.bundleOf import androidx.fragment.app.commit @@ -39,17 +36,10 @@ 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 com.google.android.gms.location.Priority -import com.google.android.gms.tasks.CancellationToken -import com.google.android.gms.tasks.CancellationTokenSource -import com.google.android.gms.tasks.OnTokenCanceledListener import dagger.hilt.android.AndroidEntryPoint import java.io.Serializable import java.util.LinkedList -import java.util.UUID -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Resource @@ -69,6 +59,8 @@ 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 @@ -80,13 +72,16 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { private lateinit var viewBinding: QuestionnaireActivityBinding private var questionnaire: Questionnaire? = null private var alertDialog: AlertDialog? = null - private lateinit var fusedLocationClient: FusedLocationProviderClient private var currLocation: Location? = null + private lateinit var locationPermissionLauncher: ActivityResultLauncher> + private lateinit var activityResultLauncher: ActivityResultLauncher override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + configureLocationServices() + setTheme(R.style.AppTheme_Questionnaire) viewBinding = QuestionnaireActivityBinding.inflate(layoutInflater) setContentView(viewBinding.root) @@ -95,31 +90,6 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { actionParameters = parcelableArrayList(QUESTIONNAIRE_ACTION_PARAMETERS) ?: arrayListOf() } - if (viewModel.applicationConfiguration.logQuestionnaireLocation) { - fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) - - if (LocationUtils.isLocationEnabled(this)) { - getLocationPermission() - } else { - val locationSettingsLauncher: ActivityResultLauncher = - this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (LocationUtils.isLocationEnabled(this)) { - getLocationPermission() - } - } - - AlertDialog.Builder(this) - .setMessage("Location services are disabled. Do you want to enable them?") - .setCancelable(true) - .setPositiveButton("Yes") { _, _ -> - val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) - locationSettingsLauncher.launch(intent) - } - .setNegativeButton("No") { dialog, _ -> dialog.cancel() } - .show() - } - } - if (!::questionnaireConfig.isInitialized) { showToast(getString(R.string.missing_questionnaire_config)) finish() @@ -154,11 +124,95 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ) } + private fun configureLocationServices() { + if (viewModel.applicationConfiguration.logQuestionnaireLocation) { + fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) + + if (LocationUtils.isLocationEnabled(this)) { + launchLocationPermissionsDialog() + } else { + activityResultLauncher = + PermissionUtils.getStartActivityForResultLauncher(this) { resultCode, _ -> + if (resultCode == RESULT_OK || LocationUtils.isLocationEnabled(this)) { + launchLocationPermissionsDialog() + } + } + + showLocationSettingsDialog() + } + } + } + + private fun hasLocationPermissions(): Boolean { + return PermissionUtils.checkPermissions( + this, + listOf( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + ), + ) + } + + private fun launchLocationPermissionsDialog() { + if (hasLocationPermissions()) { + fetchLocation() + } else { + 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, + ), + ) + } + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.clear() } + private fun fetchLocation(highAccuracy: Boolean = true) { + lifecycleScope.launch { + try { + if (highAccuracy) { + currLocation = LocationUtils.getAccurateLocation(fusedLocationClient) + } else { + currLocation = LocationUtils.getApproximateLocation(fusedLocationClient) + } + } catch (e: Exception) { + Timber.e(e, "Failed to get GPS location") + } + } + } + + private fun showLocationSettingsDialog() { + AlertDialog.Builder(this) + .setMessage(getString(R.string.location_services_disabled)) + .setCancelable(true) + .setPositiveButton(getString(R.string.yes)) { _, _ -> + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + activityResultLauncher.launch(intent) + } + .setNegativeButton(getString(R.string.no)) { dialog, _ -> dialog.cancel() } + .show() + } + private fun renderQuestionnaire() { lifecycleScope.launch { if (supportFragmentManager.findFragmentByTag(QUESTIONNAIRE_FRAGMENT_TAG) == null) { @@ -285,7 +339,9 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { setProgressState(QuestionnaireProgressState.ExtractionInProgress(true)) if (currLocation != null) { - questionnaireResponse.contained.add(createLocationResource(currLocation)) + questionnaireResponse.contained.add( + ResourceUtils.createLocationResource(gpsLocation = currLocation), + ) } handleQuestionnaireSubmission( @@ -313,117 +369,6 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { } } - private fun getLocationPermission() { - if ( - !PermissionUtils.checkPermissions( - this, - listOf( - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.ACCESS_FINE_LOCATION - ), - ) - ) { - val locationPermissionRequest = - this@QuestionnaireActivity.registerForActivityResult( - ActivityResultContracts.RequestMultiplePermissions(), - ) { permissions -> - when { - permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> { - lifecycleScope.launch { getCurrentLocation() } - } - permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> { - lifecycleScope.launch { getCurrentApproximateLocation() } - } - else -> { - Toast.makeText( - this@QuestionnaireActivity, - "Location currently disabled due to denied permission.", - Toast.LENGTH_LONG, - ) - .show() - } - } - } - locationPermissionRequest.launch( - arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION, - ), - ) - } else { - lifecycleScope.launch { getCurrentLocation() } - } - } - - @SuppressLint("MissingPermission") - private suspend fun getCurrentLocation() { - withContext(Dispatchers.IO) { - fusedLocationClient - .getCurrentLocation( - Priority.PRIORITY_HIGH_ACCURACY, - object : CancellationToken() { - override fun onCanceledRequested(p0: OnTokenCanceledListener) = - CancellationTokenSource().token - - override fun isCancellationRequested() = false - }, - ) - .addOnSuccessListener { location: Location? -> - if (location != null) { - currLocation = location - Timber.d( - "Location - lat: ${location.latitude}; long: ${location.longitude}; alt: ${location.altitude}", - ) - } else {} - } - .addOnFailureListener { e -> - Timber.e(e, "Failed to get current location") - // Notify user about failure - Toast.makeText( - this@QuestionnaireActivity, - "Failed to get current location: ${e.message}", - Toast.LENGTH_LONG, - ) - .show() - } - } - } - - @SuppressLint("MissingPermission") - private suspend fun getCurrentApproximateLocation() { - withContext(Dispatchers.IO) { - fusedLocationClient - .getCurrentLocation( - Priority.PRIORITY_BALANCED_POWER_ACCURACY, - object : CancellationToken() { - override fun onCanceledRequested(p0: OnTokenCanceledListener) = - CancellationTokenSource().token - - override fun isCancellationRequested() = false - }, - ) - .addOnSuccessListener { location: Location? -> - if (location != null) { - currLocation = location - Timber.d( - "Location - lat: ${location.latitude}; long: ${location.longitude}; alt: ${location.altitude}", - ) - } - } - .addOnFailureListener {} - } - } - - private fun createLocationResource(gpsLocation: Location?): org.hl7.fhir.r4.model.Location { - var locationResource = org.hl7.fhir.r4.model.Location() - locationResource.id = UUID.randomUUID().toString() - locationResource.position.latitude = gpsLocation!!.latitude.toBigDecimal() - locationResource.position.longitude = gpsLocation!!.longitude.toBigDecimal() - locationResource.position.altitude = gpsLocation!!.altitude.toBigDecimal() - - return locationResource - } - private fun handleBackPress() { if (questionnaireConfig.isReadOnly()) { finish() diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt index a265108570b..5351dec10a0 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt @@ -16,16 +16,91 @@ package org.smartregister.fhircore.quest.util +import android.annotation.SuppressLint import android.content.Context +import android.location.Location import android.location.LocationManager +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.Priority +import com.google.android.gms.tasks.CancellationToken +import com.google.android.gms.tasks.CancellationTokenSource +import com.google.android.gms.tasks.OnTokenCanceledListener +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber class LocationUtils { companion object { fun isLocationEnabled(context: Context): Boolean { val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) } + + @SuppressLint("MissingPermission") + suspend fun getAccurateLocation( + fusedLocationClient: FusedLocationProviderClient, + ): Location? { + return withContext(Dispatchers.IO) { + suspendCoroutine { continuation -> + fusedLocationClient + .getCurrentLocation( + Priority.PRIORITY_HIGH_ACCURACY, + object : CancellationToken() { + override fun onCanceledRequested(p0: OnTokenCanceledListener) = + CancellationTokenSource().token + + override fun isCancellationRequested() = false + }, + ) + .addOnSuccessListener { location: Location? -> + if (location != null) { + Timber.d( + "Accurate location - lat: ${location.latitude}; long: ${location.longitude}; alt: ${location.altitude}", + ) + continuation.resume(location) + } + } + .addOnFailureListener { e -> + Timber.e(e, "Failed to get accurate location") + continuation.resumeWithException(e) + } + } + } + } + + @SuppressLint("MissingPermission") + suspend fun getApproximateLocation( + fusedLocationClient: FusedLocationProviderClient, + ): Location? { + return withContext(Dispatchers.IO) { + suspendCoroutine { continuation -> + fusedLocationClient + .getCurrentLocation( + Priority.PRIORITY_BALANCED_POWER_ACCURACY, + object : CancellationToken() { + override fun onCanceledRequested(p0: OnTokenCanceledListener) = + CancellationTokenSource().token + + override fun isCancellationRequested() = false + }, + ) + .addOnSuccessListener { location: Location? -> + if (location != null) { + Timber.d( + "Approx location - lat: ${location.latitude}; long: ${location.longitude}; alt: ${location.altitude}", + ) + continuation.resume(location) + } + } + .addOnFailureListener { e -> continuation.resumeWithException(e) } + } + } + } } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/PermissionUtils.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/PermissionUtils.kt index 895563f0b16..b179e0b1b75 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/PermissionUtils.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/PermissionUtils.kt @@ -14,11 +14,19 @@ * limitations under the License. */ +package org.smartregister.fhircore.quest.util + +import android.Manifest import android.content.Context +import android.content.Intent import android.content.pm.PackageManager +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat object PermissionUtils { + fun checkPermissions(context: Context, permissions: List): Boolean { for (permission in permissions) { if ( @@ -29,4 +37,34 @@ object PermissionUtils { } return true } + + fun getLocationPermissionLauncher( + activity: AppCompatActivity, + onFineLocationPermissionGranted: () -> Unit, + onCoarseLocationPermissionGranted: () -> Unit, + onLocationPermissionDenied: () -> Unit, + ): ActivityResultLauncher> { + return activity.registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions(), + ) { permissions -> + if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true) { + onFineLocationPermissionGranted() + } else if (permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true) { + onCoarseLocationPermissionGranted() + } else { + onLocationPermissionDenied() + } + } + } + + fun getStartActivityForResultLauncher( + activity: AppCompatActivity, + onResult: (resultCode: Int, data: Intent?) -> Unit, + ): ActivityResultLauncher { + return activity.registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ) { result -> + onResult(result.resultCode, result.data) + } + } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/ResourceUtils.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/ResourceUtils.kt new file mode 100644 index 00000000000..135fdb32b27 --- /dev/null +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/ResourceUtils.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.quest.util + +import android.location.Location +import java.util.UUID + +class ResourceUtils { + companion object { + fun createLocationResource( + gpsLocation: Location?, + locationResource: org.hl7.fhir.r4.model.Location? = null, + ): org.hl7.fhir.r4.model.Location { + var locationResourceCopy = locationResource + + if (locationResourceCopy == null) { + locationResourceCopy = org.hl7.fhir.r4.model.Location() + } + + locationResourceCopy.id = UUID.randomUUID().toString() + locationResourceCopy.position.latitude = gpsLocation!!.latitude.toBigDecimal() + locationResourceCopy.position.longitude = gpsLocation!!.longitude.toBigDecimal() + locationResourceCopy.position.altitude = gpsLocation!!.altitude.toBigDecimal() + + return locationResourceCopy + } + } +} diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ContextExtentions.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ContextExtentions.kt deleted file mode 100644 index aab508c594e..00000000000 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ContextExtentions.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2021-2024 Ona Systems, Inc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.smartregister.fhircore.quest.util.extensions - -import android.Manifest -import android.content.Context -import android.content.pm.PackageManager -import androidx.core.content.ContextCompat - -fun Context.hasLocationPermission(): Boolean { - return ContextCompat.checkSelfPermission( - this, - Manifest.permission.ACCESS_COARSE_LOCATION, - ) == PackageManager.PERMISSION_GRANTED && - ContextCompat.checkSelfPermission( - this, - Manifest.permission.ACCESS_FINE_LOCATION, - ) == PackageManager.PERMISSION_GRANTED -} diff --git a/android/quest/src/main/res/values/strings.xml b/android/quest/src/main/res/values/strings.xml index 921621d752a..e8210bbf56f 100644 --- a/android/quest/src/main/res/values/strings.xml +++ b/android/quest/src/main/res/values/strings.xml @@ -110,4 +110,7 @@ Loading questionnaire… Clear All + Location Access Denied: To use this feature, please enable location permissions in your device settings. + Location services are disabled. Do you want to enable them? + From 4822025423d91a82fdfb280fb1b06997974cb58d Mon Sep 17 00:00:00 2001 From: Simon Njoroge Date: Tue, 6 Feb 2024 21:59:25 +0300 Subject: [PATCH 12/41] Rename location setup --- .../fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 30147946a7e..0a515a67e44 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -80,7 +80,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - configureLocationServices() + setupLocationServices() setTheme(R.style.AppTheme_Questionnaire) viewBinding = QuestionnaireActivityBinding.inflate(layoutInflater) @@ -124,7 +124,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ) } - private fun configureLocationServices() { + private fun setupLocationServices() { if (viewModel.applicationConfiguration.logQuestionnaireLocation) { fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) From 2d82ce96c9146ca166ac60b4b4898418a8b8c4ed Mon Sep 17 00:00:00 2001 From: Simon Njoroge Date: Tue, 6 Feb 2024 23:10:42 +0300 Subject: [PATCH 13/41] Fix crash on opening location service settings after permissions --- .../ui/questionnaire/QuestionnaireActivity.kt | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 0a515a67e44..a3dce30e5bb 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -128,17 +128,12 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { if (viewModel.applicationConfiguration.logQuestionnaireLocation) { fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) - if (LocationUtils.isLocationEnabled(this)) { - launchLocationPermissionsDialog() - } else { - activityResultLauncher = - PermissionUtils.getStartActivityForResultLauncher(this) { resultCode, _ -> - if (resultCode == RESULT_OK || LocationUtils.isLocationEnabled(this)) { - launchLocationPermissionsDialog() - } - } + if (!LocationUtils.isLocationEnabled(this)) { + openLocationServicesSettings() + } - showLocationSettingsDialog() + if (!hasLocationPermissions()) { + launchLocationPermissionsDialog() } } } @@ -153,38 +148,50 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ) } - private fun launchLocationPermissionsDialog() { - if (hasLocationPermissions()) { - fetchLocation() - } else { - 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") - }, - ) + private fun openLocationServicesSettings() { + activityResultLauncher = + PermissionUtils.getStartActivityForResultLauncher(this) { resultCode, _ -> + if (resultCode == RESULT_OK || hasLocationPermissions()) { + fetchLocation() + } + } - locationPermissionLauncher.launch( - arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION, - ), - ) - } + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + showLocationSettingsDialog(intent) } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.clear() + private fun showLocationSettingsDialog(intent: Intent) { + 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() + } + + private 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, + ), + ) } private fun fetchLocation(highAccuracy: Boolean = true) { @@ -201,16 +208,9 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { } } - private fun showLocationSettingsDialog() { - AlertDialog.Builder(this) - .setMessage(getString(R.string.location_services_disabled)) - .setCancelable(true) - .setPositiveButton(getString(R.string.yes)) { _, _ -> - val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) - activityResultLauncher.launch(intent) - } - .setNegativeButton(getString(R.string.no)) { dialog, _ -> dialog.cancel() } - .show() + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.clear() } private fun renderQuestionnaire() { From a873d7e522de4f6914740eadec7d2c93ed7a0ed5 Mon Sep 17 00:00:00 2001 From: brandy-kay Date: Thu, 8 Feb 2024 13:01:47 +0300 Subject: [PATCH 14/41] Resolve merge conflict --- android/gradle/libs.versions.toml | 2 -- android/quest/build.gradle.kts | 1 - .../fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt | 1 - android/quest/src/main/res/values/strings.xml | 2 ++ 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index dc8d36f4c67..68bcfb4cf20 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -171,8 +171,6 @@ p2p-lib = { group = "org.smartregister", name = "p2p-lib", version.ref = "p2p-li 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" } -paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" } -paging-runtime-ktx = { group = "androidx.paging", name = "paging-runtime-ktx", version.ref = "paging" } 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" } diff --git a/android/quest/build.gradle.kts b/android/quest/build.gradle.kts index dd5d14fba89..3e447c40a74 100644 --- a/android/quest/build.gradle.kts +++ b/android/quest/build.gradle.kts @@ -378,7 +378,6 @@ dependencies { implementation(libs.material) implementation(libs.dagger.hilt.android) implementation(libs.hilt.work) - implementation(libs.cql.measure.evaluator) implementation(libs.play.services.location) // Annotation processors diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 5baea08916e..c8060c48991 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -82,7 +82,6 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { setupLocationServices() - setTheme(R.style.AppTheme_Questionnaire) setTheme(org.smartregister.fhircore.engine.R.style.AppTheme_Questionnaire) viewBinding = QuestionnaireActivityBinding.inflate(layoutInflater) setContentView(viewBinding.root) diff --git a/android/quest/src/main/res/values/strings.xml b/android/quest/src/main/res/values/strings.xml index e8210bbf56f..f42a8eadff8 100644 --- a/android/quest/src/main/res/values/strings.xml +++ b/android/quest/src/main/res/values/strings.xml @@ -109,6 +109,8 @@ Processing questionnaire data… Loading questionnaire… Clear All + Yes + No Location Access Denied: To use this feature, please enable location permissions in your device settings. Location services are disabled. Do you want to enable them? From 334c7d549aa38efefdddf42b146ebf44460b4365 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Fri, 9 Feb 2024 10:19:20 +0300 Subject: [PATCH 15/41] Add tests --- .../configs/app/application_config.json | 3 +- .../QuestionnaireActivityTest.kt | 5 +++ .../QuestionnaireViewModelTest.kt | 1 + .../fhircore/quest/util/LocationUtilsTest.kt | 4 ++ .../quest/util/PermissionsUtilsTest.kt | 44 +++++++++++++++++++ .../fhircore/quest/util/ResourceUtilsTest.kt | 29 ++++++++++++ 6 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt create mode 100644 android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt create mode 100644 android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt diff --git a/android/quest/src/test/assets/configs/app/application_config.json b/android/quest/src/test/assets/configs/app/application_config.json index 9cbadf77c96..e51c1928080 100644 --- a/android/quest/src/test/assets/configs/app/application_config.json +++ b/android/quest/src/test/assets/configs/app/application_config.json @@ -28,5 +28,6 @@ "Questionnaire", "QuestionnaireResponse" ] - } + }, + "logQuestionnaireLocation": true } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt index d62c52320bf..f7db181c655 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt @@ -54,6 +54,7 @@ import org.robolectric.Shadows import org.robolectric.android.controller.ActivityController import org.robolectric.shadows.ShadowAlertDialog import org.robolectric.shadows.ShadowToast +import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.domain.model.ActionParameter @@ -62,6 +63,7 @@ import org.smartregister.fhircore.engine.domain.model.RuleConfig import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.extension.decodeResourceFromString import org.smartregister.fhircore.quest.R +import org.smartregister.fhircore.quest.app.fakes.Faker import org.smartregister.fhircore.quest.robolectric.RobolectricTest @OptIn(ExperimentalCoroutinesApi::class) @@ -82,6 +84,9 @@ class QuestionnaireActivityTest : RobolectricTest() { @BindValue lateinit var defaultRepository: DefaultRepository + @BindValue + val configurationRegistry: ConfigurationRegistry = spyk(Faker.buildTestConfigurationRegistry()) + @Before fun setUp() { hiltRule.inject() diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt index 4fc0e7c86c3..e6558fbea3d 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt @@ -189,6 +189,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { sharedPreferencesHelper = sharedPreferencesHelper, fhirOperator = fhirOperator, fhirPathDataExtractor = fhirPathDataExtractor, + configurationRegistry = configurationRegistry ), ) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt new file mode 100644 index 00000000000..b00da606bdd --- /dev/null +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt @@ -0,0 +1,4 @@ +package org.smartregister.fhircore.quest.util + +class LocationUtilsTest { +} \ No newline at end of file diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt new file mode 100644 index 00000000000..b171d5d57f1 --- /dev/null +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt @@ -0,0 +1,44 @@ +package org.smartregister.fhircore.quest.util + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import org.junit.Test + +class PermissionUtilsTest { + + val context = mockk() + + @Test + fun checkAllPermissionsGranted(){ + val permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION) + + every { ContextCompat.checkSelfPermission(context, any()) + } returns PackageManager.PERMISSION_GRANTED + + val result = PermissionUtils.checkPermissions(context, permissions) + + assertTrue(result) + } + + @Test + fun `checkPermissions should return false when any permission is not granted`(){ + val permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION) + + every { ContextCompat.checkSelfPermission(context, any()) + } returns PackageManager.PERMISSION_DENIED + + val result = PermissionUtils.checkPermissions(context, permissions) + + assertFalse(result) + } + + +} diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt new file mode 100644 index 00000000000..db295b5e45e --- /dev/null +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt @@ -0,0 +1,29 @@ +package org.smartregister.fhircore.quest.util + +import android.location.Location +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class ResourceUtilsTest { + + @Test + fun testLocationResourceIsCreated() { + val location: Location = mockk() + every { location.longitude } returns 10.0 + every { location.latitude } returns 20.0 + every { location.altitude } returns 30.0 + + val locationResource = ResourceUtils.createLocationResource(location) + + assertNotNull(locationResource.id) + assertEquals(locationResource.position.longitude.toDouble(), 10.0) + assertEquals(locationResource.position.latitude.toDouble(), 20.0) + assertEquals(locationResource.position.altitude.toDouble(), 30.0) + + } + + +} \ No newline at end of file From cb38732d7a798136ca4a66036c7287fd724c0b50 Mon Sep 17 00:00:00 2001 From: Simon Njoroge Date: Fri, 9 Feb 2024 18:02:36 +0300 Subject: [PATCH 16/41] Allow location picking when location service & permissions are already on --- .../fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index c8060c48991..6903f75f3bc 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -135,6 +135,10 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { if (!hasLocationPermissions()) { launchLocationPermissionsDialog() } + + if (LocationUtils.isLocationEnabled(this) && hasLocationPermissions()) { + fetchLocation(true) + } } } From c497bfca3870dc62b8f6ccf81175de4c84b38fa5 Mon Sep 17 00:00:00 2001 From: brandy-kay Date: Tue, 13 Feb 2024 17:02:56 +0300 Subject: [PATCH 17/41] add service location test on QuestionnaireActivityTest --- .../ui/questionnaire/QuestionnaireActivity.kt | 10 +-- .../QuestionnaireActivityTest.kt | 66 +++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 6903f75f3bc..6990cbf4a96 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -124,7 +124,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ) } - private fun setupLocationServices() { + fun setupLocationServices() { if (viewModel.applicationConfiguration.logQuestionnaireLocation) { fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) @@ -142,7 +142,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { } } - private fun hasLocationPermissions(): Boolean { + fun hasLocationPermissions(): Boolean { return PermissionUtils.checkPermissions( this, listOf( @@ -152,7 +152,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ) } - private fun openLocationServicesSettings() { + fun openLocationServicesSettings() { activityResultLauncher = PermissionUtils.getStartActivityForResultLauncher(this) { resultCode, _ -> if (resultCode == RESULT_OK || hasLocationPermissions()) { @@ -173,7 +173,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { .show() } - private fun launchLocationPermissionsDialog() { + fun launchLocationPermissionsDialog() { locationPermissionLauncher = PermissionUtils.getLocationPermissionLauncher( this, @@ -198,7 +198,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ) } - private fun fetchLocation(highAccuracy: Boolean = true) { + fun fetchLocation(highAccuracy: Boolean = true) { lifecycleScope.launch { try { if (highAccuracy) { diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt index f7db181c655..4b97791891b 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt @@ -24,6 +24,8 @@ import androidx.test.core.app.ApplicationProvider import com.google.android.fhir.FhirEngine import com.google.android.fhir.datacapture.QuestionnaireFragment import com.google.android.fhir.db.ResourceNotFoundException +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -65,6 +67,7 @@ import org.smartregister.fhircore.engine.util.extension.decodeResourceFromString import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.app.fakes.Faker import org.smartregister.fhircore.quest.robolectric.RobolectricTest +import org.smartregister.fhircore.quest.util.LocationUtils @OptIn(ExperimentalCoroutinesApi::class) @HiltAndroidTest @@ -202,6 +205,69 @@ class QuestionnaireActivityTest : RobolectricTest() { Assert.assertNotNull(dialog) } + @Test + fun `setupLocationServices should fetch location when location is enabled and permissions granted`() { + val viewModel = mockk() + val activity = spyk(QuestionnaireActivity()) + val fusedLocationProviderClient = mockk() + every { viewModel.applicationConfiguration.logQuestionnaireLocation } returns true + every { LocationServices.getFusedLocationProviderClient(activity) } returns fusedLocationProviderClient + every { LocationUtils.isLocationEnabled(activity) } returns true + every { activity.hasLocationPermissions() } returns true + every { activity.fetchLocation(any()) } just runs + + activity.setupLocationServices() + + verify(exactly = 1) { activity.fetchLocation(true) } + verify(exactly = 0) { activity.openLocationServicesSettings() } + verify(exactly = 0) { activity.launchLocationPermissionsDialog() } + } + + @Test + fun `setupLocationServices should open location settings if location is disabled`() { + val viewModel = mockk() + val activity = spyk(QuestionnaireActivity()) + val fusedLocationProviderClient = mockk() + every { viewModel.applicationConfiguration.logQuestionnaireLocation } returns true + every { LocationServices.getFusedLocationProviderClient(activity) } returns fusedLocationProviderClient + every { LocationUtils.isLocationEnabled(activity) } returns false + every { activity.hasLocationPermissions() } returns true + every { activity.fetchLocation(any()) } just runs + every { activity.openLocationServicesSettings() } just runs + every { activity.launchLocationPermissionsDialog() } just runs + + activity.setupLocationServices() + + verify(exactly = 1) { activity.openLocationServicesSettings() } + verify(exactly = 0) { activity.fetchLocation(any()) } + verify(exactly = 0) { activity.launchLocationPermissionsDialog() } + } + + + @Test + fun `setupLocationServices should launch location permissions dialog if permissions are not granted`() { + + val viewModel = mockk() + val activity = spyk(QuestionnaireActivity()) + val fusedLocationProviderClient = mockk() + every { viewModel.applicationConfiguration.logQuestionnaireLocation } returns true + every { LocationServices.getFusedLocationProviderClient(activity) } returns fusedLocationProviderClient + every { LocationUtils.isLocationEnabled(activity) } returns true + every { activity.hasLocationPermissions() } returns false + every { activity.fetchLocation(any()) } just runs + every { activity.openLocationServicesSettings() } just runs + every { activity.launchLocationPermissionsDialog() } just runs + + + activity.setupLocationServices() + + + verify(exactly = 1) { activity.launchLocationPermissionsDialog() } + verify(exactly = 0) { activity.fetchLocation(any()) } + verify(exactly = 0) { activity.openLocationServicesSettings() } + } + + private fun setupActivity() { val bundle = QuestionnaireActivity.intentBundle(questionnaireConfig, emptyList()) questionnaireActivityController = From da018c58beb1c514d397d5b83e26b1ec23bb218d Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Mon, 19 Feb 2024 15:58:34 +0300 Subject: [PATCH 18/41] Add tests and Run SpotlessApply --- .../questionnaire/QuestionnaireViewModel.kt | 2 +- .../fhircore/quest/util/LocationUtils.kt | 114 +++++++++--------- .../QuestionnaireActivityTest.kt | 14 +-- .../QuestionnaireViewModelTest.kt | 2 +- .../fhircore/quest/util/LocationUtilsTest.kt | 86 ++++++++++++- .../quest/util/PermissionsUtilsTest.kt | 57 +++++---- .../fhircore/quest/util/ResourceUtilsTest.kt | 51 +++++--- 7 files changed, 215 insertions(+), 111 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt index 8b9d355e7fa..afed86ed16c 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt @@ -57,9 +57,9 @@ import org.hl7.fhir.r4.model.RelatedPerson 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.BuildConfig import org.smartregister.fhircore.engine.configuration.GroupResourceConfig import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt index 5351dec10a0..51fb5a8edf7 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt @@ -32,74 +32,72 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber -class LocationUtils { +object LocationUtils { - companion object { - fun isLocationEnabled(context: Context): Boolean { - val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + fun isLocationEnabled(context: Context): Boolean { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || - locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) - } + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || + locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } - @SuppressLint("MissingPermission") - suspend fun getAccurateLocation( - fusedLocationClient: FusedLocationProviderClient, - ): Location? { - return withContext(Dispatchers.IO) { - suspendCoroutine { continuation -> - fusedLocationClient - .getCurrentLocation( - Priority.PRIORITY_HIGH_ACCURACY, - object : CancellationToken() { - override fun onCanceledRequested(p0: OnTokenCanceledListener) = - CancellationTokenSource().token + @SuppressLint("MissingPermission") + suspend fun getAccurateLocation( + fusedLocationClient: FusedLocationProviderClient, + ): Location? { + return withContext(Dispatchers.IO) { + suspendCoroutine { continuation -> + fusedLocationClient + .getCurrentLocation( + Priority.PRIORITY_HIGH_ACCURACY, + object : CancellationToken() { + override fun onCanceledRequested(p0: OnTokenCanceledListener) = + CancellationTokenSource().token - override fun isCancellationRequested() = false - }, - ) - .addOnSuccessListener { location: Location? -> - if (location != null) { - Timber.d( - "Accurate location - lat: ${location.latitude}; long: ${location.longitude}; alt: ${location.altitude}", - ) - continuation.resume(location) - } + override fun isCancellationRequested() = false + }, + ) + .addOnSuccessListener { location: Location? -> + if (location != null) { + Timber.d( + "Accurate location - lat: ${location.latitude}; long: ${location.longitude}; alt: ${location.altitude}", + ) + continuation.resume(location) } - .addOnFailureListener { e -> - Timber.e(e, "Failed to get accurate location") - continuation.resumeWithException(e) - } - } + } + .addOnFailureListener { e -> + Timber.e(e, "Failed to get accurate location") + continuation.resumeWithException(e) + } } } + } - @SuppressLint("MissingPermission") - suspend fun getApproximateLocation( - fusedLocationClient: FusedLocationProviderClient, - ): Location? { - return withContext(Dispatchers.IO) { - suspendCoroutine { continuation -> - fusedLocationClient - .getCurrentLocation( - Priority.PRIORITY_BALANCED_POWER_ACCURACY, - object : CancellationToken() { - override fun onCanceledRequested(p0: OnTokenCanceledListener) = - CancellationTokenSource().token + @SuppressLint("MissingPermission") + suspend fun getApproximateLocation( + fusedLocationClient: FusedLocationProviderClient, + ): Location? { + return withContext(Dispatchers.IO) { + suspendCoroutine { continuation -> + fusedLocationClient + .getCurrentLocation( + Priority.PRIORITY_BALANCED_POWER_ACCURACY, + object : CancellationToken() { + override fun onCanceledRequested(p0: OnTokenCanceledListener) = + CancellationTokenSource().token - override fun isCancellationRequested() = false - }, - ) - .addOnSuccessListener { location: Location? -> - if (location != null) { - Timber.d( - "Approx location - lat: ${location.latitude}; long: ${location.longitude}; alt: ${location.altitude}", - ) - continuation.resume(location) - } + override fun isCancellationRequested() = false + }, + ) + .addOnSuccessListener { location: Location? -> + if (location != null) { + Timber.d( + "Approx location - lat: ${location.latitude}; long: ${location.longitude}; alt: ${location.altitude}", + ) + continuation.resume(location) } - .addOnFailureListener { e -> continuation.resumeWithException(e) } - } + } + .addOnFailureListener { e -> continuation.resumeWithException(e) } } } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt index 4b97791891b..ba06c8b504d 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt @@ -211,7 +211,8 @@ class QuestionnaireActivityTest : RobolectricTest() { val activity = spyk(QuestionnaireActivity()) val fusedLocationProviderClient = mockk() every { viewModel.applicationConfiguration.logQuestionnaireLocation } returns true - every { LocationServices.getFusedLocationProviderClient(activity) } returns fusedLocationProviderClient + every { LocationServices.getFusedLocationProviderClient(activity) } returns + fusedLocationProviderClient every { LocationUtils.isLocationEnabled(activity) } returns true every { activity.hasLocationPermissions() } returns true every { activity.fetchLocation(any()) } just runs @@ -229,7 +230,8 @@ class QuestionnaireActivityTest : RobolectricTest() { val activity = spyk(QuestionnaireActivity()) val fusedLocationProviderClient = mockk() every { viewModel.applicationConfiguration.logQuestionnaireLocation } returns true - every { LocationServices.getFusedLocationProviderClient(activity) } returns fusedLocationProviderClient + every { LocationServices.getFusedLocationProviderClient(activity) } returns + fusedLocationProviderClient every { LocationUtils.isLocationEnabled(activity) } returns false every { activity.hasLocationPermissions() } returns true every { activity.fetchLocation(any()) } just runs @@ -243,31 +245,27 @@ class QuestionnaireActivityTest : RobolectricTest() { verify(exactly = 0) { activity.launchLocationPermissionsDialog() } } - @Test fun `setupLocationServices should launch location permissions dialog if permissions are not granted`() { - val viewModel = mockk() val activity = spyk(QuestionnaireActivity()) val fusedLocationProviderClient = mockk() every { viewModel.applicationConfiguration.logQuestionnaireLocation } returns true - every { LocationServices.getFusedLocationProviderClient(activity) } returns fusedLocationProviderClient + every { LocationServices.getFusedLocationProviderClient(activity) } returns + fusedLocationProviderClient every { LocationUtils.isLocationEnabled(activity) } returns true every { activity.hasLocationPermissions() } returns false every { activity.fetchLocation(any()) } just runs every { activity.openLocationServicesSettings() } just runs every { activity.launchLocationPermissionsDialog() } just runs - activity.setupLocationServices() - verify(exactly = 1) { activity.launchLocationPermissionsDialog() } verify(exactly = 0) { activity.fetchLocation(any()) } verify(exactly = 0) { activity.openLocationServicesSettings() } } - private fun setupActivity() { val bundle = QuestionnaireActivity.intentBundle(questionnaireConfig, emptyList()) questionnaireActivityController = diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt index e6558fbea3d..aab5b63cc22 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt @@ -189,7 +189,7 @@ class QuestionnaireViewModelTest : RobolectricTest() { sharedPreferencesHelper = sharedPreferencesHelper, fhirOperator = fhirOperator, fhirPathDataExtractor = fhirPathDataExtractor, - configurationRegistry = configurationRegistry + configurationRegistry = configurationRegistry, ), ) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt index b00da606bdd..732fa05dfbd 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt @@ -1,4 +1,88 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.smartregister.fhircore.quest.util +import android.content.Context +import android.location.Location +import android.location.LocationManager +import com.google.android.gms.location.FusedLocationProviderClient +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import kotlin.test.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test + class LocationUtilsTest { -} \ No newline at end of file + private lateinit var context: Context + private lateinit var fusedLocationProviderClient: FusedLocationProviderClient + + @Before + fun setUp() { + context = mockk(relaxed = true) + fusedLocationProviderClient = mockk(relaxed = true) + mockkObject(LocationUtils) + } + + @Test + fun `test isLocationEnabled when GPS provider is enabled`() { + val locationManager = mockk() + every { context.getSystemService(Context.LOCATION_SERVICE) } returns locationManager + every { locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) } returns true + every { locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) } returns false + + val result = LocationUtils.isLocationEnabled(context) + + assert(result) + } + + @Test + fun `test isLocationEnabled when Network provider is enabled`() { + val locationManager = mockk() + every { context.getSystemService(Context.LOCATION_SERVICE) } returns locationManager + every { locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) } returns false + every { locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) } returns true + + val result = LocationUtils.isLocationEnabled(context) + + assert(result) + } + + @Test + fun `test getAccurateLocation`() = runBlocking { + val location = mockk() + + coEvery { LocationUtils.getAccurateLocation(fusedLocationProviderClient) } returns location + + val result = LocationUtils.getAccurateLocation(fusedLocationProviderClient) + + assertEquals(location, result) + } + + @Test + fun `test getApproximateLocation`() = runBlocking { + val location = mockk() + + coEvery { LocationUtils.getApproximateLocation(fusedLocationProviderClient) } returns location + + val result = LocationUtils.getApproximateLocation(fusedLocationProviderClient) + + assertEquals(location, result) + } +} diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt index b171d5d57f1..e513e65f5fb 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt @@ -1,44 +1,55 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.smartregister.fhircore.quest.util import android.Manifest import android.content.Context import android.content.pm.PackageManager -import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import io.mockk.every import io.mockk.mockk -import io.mockk.slot import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue import org.junit.Test -class PermissionUtilsTest { - - val context = mockk() - - @Test - fun checkAllPermissionsGranted(){ - val permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION) - - every { ContextCompat.checkSelfPermission(context, any()) - } returns PackageManager.PERMISSION_GRANTED +class PermissionsUtilsTest { + val context = mockk() - val result = PermissionUtils.checkPermissions(context, permissions) + @Test + fun checkAllPermissionsGranted() { + val permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION) - assertTrue(result) - } + every { ContextCompat.checkSelfPermission(context, any()) } returns + PackageManager.PERMISSION_GRANTED - @Test - fun `checkPermissions should return false when any permission is not granted`(){ - val permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION) + val result = PermissionUtils.checkPermissions(context, permissions) - every { ContextCompat.checkSelfPermission(context, any()) - } returns PackageManager.PERMISSION_DENIED + assertTrue(result) + } - val result = PermissionUtils.checkPermissions(context, permissions) + @Test + fun `checkPermissions should return false when any permission is not granted`() { + val permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION) - assertFalse(result) - } + every { ContextCompat.checkSelfPermission(context, any()) } returns + PackageManager.PERMISSION_DENIED + val result = PermissionUtils.checkPermissions(context, permissions) + assertFalse(result) + } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt index db295b5e45e..c8d7c94ca80 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt @@ -1,29 +1,42 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.smartregister.fhircore.quest.util import android.location.Location import io.mockk.every import io.mockk.mockk -import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import org.junit.Test class ResourceUtilsTest { - @Test - fun testLocationResourceIsCreated() { - val location: Location = mockk() - every { location.longitude } returns 10.0 - every { location.latitude } returns 20.0 - every { location.altitude } returns 30.0 - - val locationResource = ResourceUtils.createLocationResource(location) - - assertNotNull(locationResource.id) - assertEquals(locationResource.position.longitude.toDouble(), 10.0) - assertEquals(locationResource.position.latitude.toDouble(), 20.0) - assertEquals(locationResource.position.altitude.toDouble(), 30.0) - - } - - -} \ No newline at end of file + @Test + fun testLocationResourceIsCreated() { + val location: Location = mockk() + every { location.longitude } returns 10.0 + every { location.latitude } returns 20.0 + every { location.altitude } returns 30.0 + + val locationResource = ResourceUtils.createLocationResource(location) + + assertNotNull(locationResource.id) + assertEquals(locationResource.position.longitude.toDouble(), 10.0) + assertEquals(locationResource.position.latitude.toDouble(), 20.0) + assertEquals(locationResource.position.altitude.toDouble(), 30.0) + } +} From c96df7bd7e6a179cc947036fe7b32a0b88dfb01e Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Mon, 19 Feb 2024 20:14:28 +0300 Subject: [PATCH 19/41] Empty commit From 25a0a96181d4c5e2a93df56dcf575f3314e883c9 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Tue, 20 Feb 2024 13:48:23 +0300 Subject: [PATCH 20/41] Update RegisterCardList tests --- .../ui/register/components/RegisterCardListTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/components/RegisterCardListTest.kt b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/components/RegisterCardListTest.kt index 8696a0bcbef..42cab5ea823 100644 --- a/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/components/RegisterCardListTest.kt +++ b/android/quest/src/androidTest/java/org/smartregister/fhircore/quest/integration/ui/register/components/RegisterCardListTest.kt @@ -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) @@ -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) From 890134b6d6fce92a47b9eac70a95225d3b603dc3 Mon Sep 17 00:00:00 2001 From: Simon Njoroge Date: Fri, 23 Feb 2024 13:06:54 +0300 Subject: [PATCH 21/41] Log GPS location fetch failure --- .../ui/questionnaire/QuestionnaireActivity.kt | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index d668ac6125b..32a8899ec73 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -19,7 +19,6 @@ package org.smartregister.fhircore.quest.ui.questionnaire import android.Manifest import android.app.Activity import android.app.AlertDialog -import android.content.Context import android.content.Intent import android.location.Location import android.os.Bundle @@ -78,7 +77,6 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { private lateinit var locationPermissionLauncher: ActivityResultLauncher> private lateinit var activityResultLauncher: ActivityResultLauncher - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -112,11 +110,10 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { } } - setupLocationServices() - - if (savedInstanceState == null) renderQuestionnaire() + setupLocationServices() + this.onBackPressedDispatcher.addCallback( this, object : OnBackPressedCallback(true) { @@ -202,21 +199,20 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { ) } - fun fetchLocation(highAccuracy: Boolean = true, questionnaireId: String? = null, context: Context = this ) { + fun fetchLocation(highAccuracy: Boolean = true) { lifecycleScope.launch { try { -// throw Exception("Failed to get Location") if (highAccuracy) { currentLocation = LocationUtils.getAccurateLocation(fusedLocationClient) } else { currentLocation = LocationUtils.getApproximateLocation(fusedLocationClient) } } catch (e: Exception) { - Timber.e(e, "Failed to get GPS location for questionnaire with ID: $questionnaireId") - context.showToast( - "Failed to get GPS location for questionnaire with ID: $questionnaireId", - Toast.LENGTH_LONG - ) + 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) + } } } } From 3d49e8c390dd45f312d660b757fa2bd52b29d505 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Fri, 23 Feb 2024 16:54:50 +0300 Subject: [PATCH 22/41] Update QuestionnaireActivityTests --- .../ui/questionnaire/QuestionnaireActivity.kt | 2 +- .../QuestionnaireActivityTest.kt | 111 +++++++++--------- 2 files changed, 59 insertions(+), 54 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index d668ac6125b..4737f99a331 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -74,7 +74,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { private var questionnaire: Questionnaire? = null private var alertDialog: AlertDialog? = null private lateinit var fusedLocationClient: FusedLocationProviderClient - private var currentLocation: Location? = null + var currentLocation: Location? = null private lateinit var locationPermissionLauncher: ActivityResultLauncher> private lateinit var activityResultLauncher: ActivityResultLauncher diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt index ba06c8b504d..717045130ee 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt @@ -19,6 +19,8 @@ package org.smartregister.fhircore.quest.ui.questionnaire import android.app.Application import android.content.Context import android.content.Intent +import android.location.LocationManager +import android.provider.Settings import android.widget.Toast import androidx.test.core.app.ApplicationProvider import com.google.android.fhir.FhirEngine @@ -38,8 +40,9 @@ import io.mockk.runs import io.mockk.spyk import io.mockk.unmockkStatic import io.mockk.verify -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest @@ -52,7 +55,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.robolectric.Robolectric -import org.robolectric.Shadows +import org.robolectric.Shadows.shadowOf import org.robolectric.android.controller.ActivityController import org.robolectric.shadows.ShadowAlertDialog import org.robolectric.shadows.ShadowToast @@ -68,6 +71,9 @@ import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.app.fakes.Faker import org.smartregister.fhircore.quest.robolectric.RobolectricTest import org.smartregister.fhircore.quest.util.LocationUtils +import javax.inject.Inject +import kotlin.test.assertNotNull +import kotlin.time.Duration.Companion.seconds @OptIn(ExperimentalCoroutinesApi::class) @HiltAndroidTest @@ -82,6 +88,10 @@ class QuestionnaireActivityTest : RobolectricTest() { private lateinit var questionnaire: Questionnaire private lateinit var questionnaireActivityController: ActivityController private lateinit var questionnaireActivity: QuestionnaireActivity +// private var fusedLocationProviderClient: FusedLocationProviderClient = mockk() + private lateinit var locationUtil: LocationUtils + + private lateinit var locationManager: LocationManager @Inject lateinit var testDispatcherProvider: DispatcherProvider @@ -201,69 +211,64 @@ class QuestionnaireActivityTest : RobolectricTest() { fun testThatOnBackPressShowsConfirmationAlertDialog() = runTest { setupActivity() questionnaireActivity.onBackPressedDispatcher.onBackPressed() - val dialog = Shadows.shadowOf(ShadowAlertDialog.getLatestAlertDialog()) + val dialog = shadowOf(ShadowAlertDialog.getLatestAlertDialog()) Assert.assertNotNull(dialog) } @Test fun `setupLocationServices should fetch location when location is enabled and permissions granted`() { - val viewModel = mockk() - val activity = spyk(QuestionnaireActivity()) - val fusedLocationProviderClient = mockk() - every { viewModel.applicationConfiguration.logQuestionnaireLocation } returns true - every { LocationServices.getFusedLocationProviderClient(activity) } returns - fusedLocationProviderClient - every { LocationUtils.isLocationEnabled(activity) } returns true - every { activity.hasLocationPermissions() } returns true - every { activity.fetchLocation(any()) } just runs - - activity.setupLocationServices() - - verify(exactly = 1) { activity.fetchLocation(true) } - verify(exactly = 0) { activity.openLocationServicesSettings() } - verify(exactly = 0) { activity.launchLocationPermissionsDialog() } + setupActivity() + assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logQuestionnaireLocation) + + val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) + assertNotNull(fusedLocationProviderClient) + shadowOf(questionnaireActivity).grantPermissions(android.Manifest.permission.ACCESS_FINE_LOCATION) + + assertTrue(LocationUtils.isLocationEnabled(questionnaireActivity)) + + questionnaireActivity.setupLocationServices() + assertTrue(questionnaireActivity.hasLocationPermissions()) + questionnaireActivity.fetchLocation() + assertNotNull(questionnaireActivity.currentLocation) + } @Test fun `setupLocationServices should open location settings if location is disabled`() { - val viewModel = mockk() - val activity = spyk(QuestionnaireActivity()) - val fusedLocationProviderClient = mockk() - every { viewModel.applicationConfiguration.logQuestionnaireLocation } returns true - every { LocationServices.getFusedLocationProviderClient(activity) } returns - fusedLocationProviderClient - every { LocationUtils.isLocationEnabled(activity) } returns false - every { activity.hasLocationPermissions() } returns true - every { activity.fetchLocation(any()) } just runs - every { activity.openLocationServicesSettings() } just runs - every { activity.launchLocationPermissionsDialog() } just runs - - activity.setupLocationServices() - - verify(exactly = 1) { activity.openLocationServicesSettings() } - verify(exactly = 0) { activity.fetchLocation(any()) } - verify(exactly = 0) { activity.launchLocationPermissionsDialog() } + setupActivity() + assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logQuestionnaireLocation) + + val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) + assertNotNull(fusedLocationProviderClient) + + shadowOf(questionnaireActivity).grantPermissions(android.Manifest.permission.ACCESS_FINE_LOCATION) + val locationManager = + questionnaireActivity.getSystemService(Context.LOCATION_SERVICE) as LocationManager + locationManager.setTestProviderEnabled(LocationManager.GPS_PROVIDER, false) + locationManager.setTestProviderEnabled(LocationManager.NETWORK_PROVIDER, false) + + questionnaireActivity.fetchLocation() + val startedIntent = shadowOf(questionnaireActivity).nextStartedActivity + val expectedIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + + assertEquals(expectedIntent.component, startedIntent.component) } @Test fun `setupLocationServices should launch location permissions dialog if permissions are not granted`() { - val viewModel = mockk() - val activity = spyk(QuestionnaireActivity()) - val fusedLocationProviderClient = mockk() - every { viewModel.applicationConfiguration.logQuestionnaireLocation } returns true - every { LocationServices.getFusedLocationProviderClient(activity) } returns - fusedLocationProviderClient - every { LocationUtils.isLocationEnabled(activity) } returns true - every { activity.hasLocationPermissions() } returns false - every { activity.fetchLocation(any()) } just runs - every { activity.openLocationServicesSettings() } just runs - every { activity.launchLocationPermissionsDialog() } just runs - - activity.setupLocationServices() - - verify(exactly = 1) { activity.launchLocationPermissionsDialog() } - verify(exactly = 0) { activity.fetchLocation(any()) } - verify(exactly = 0) { activity.openLocationServicesSettings() } + + setupActivity() + assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logQuestionnaireLocation) + + val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) + assertNotNull(fusedLocationProviderClient) + + assertTrue(LocationUtils.isLocationEnabled(questionnaireActivity)) + assertFalse(questionnaireActivity.hasLocationPermissions()) + + val dialog = questionnaireActivity.launchLocationPermissionsDialog() + assertNotNull(dialog) + } private fun setupActivity() { From 18a9c9d992ad414dda6a6d18b81ce25845e96050 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Fri, 23 Feb 2024 17:09:06 +0300 Subject: [PATCH 23/41] Remove comments --- .../fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt index 717045130ee..d5edec705b6 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt @@ -88,7 +88,6 @@ class QuestionnaireActivityTest : RobolectricTest() { private lateinit var questionnaire: Questionnaire private lateinit var questionnaireActivityController: ActivityController private lateinit var questionnaireActivity: QuestionnaireActivity -// private var fusedLocationProviderClient: FusedLocationProviderClient = mockk() private lateinit var locationUtil: LocationUtils private lateinit var locationManager: LocationManager From 73e6338415584a00818e8591ff95925f74bf5023 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Mon, 26 Feb 2024 12:05:40 +0300 Subject: [PATCH 24/41] Update tests WIP --- .../ui/questionnaire/QuestionnaireActivity.kt | 5 +- .../fhircore/quest/util/LocationUtils.kt | 5 +- .../QuestionnaireActivityTest.kt | 2 +- .../fhircore/quest/util/LocationUtilsTest.kt | 47 +++++++++++++------ 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 90a716a4813..20008680331 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -51,6 +51,7 @@ 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 @@ -62,10 +63,12 @@ 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 +import javax.inject.Inject @AndroidEntryPoint class QuestionnaireActivity : BaseMultiLanguageActivity() { + @Inject lateinit var dispatcherProvider: DispatcherProvider val viewModel by viewModels() private lateinit var questionnaireConfig: QuestionnaireConfig private lateinit var actionParameters: ArrayList @@ -203,7 +206,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { lifecycleScope.launch { try { if (highAccuracy) { - currentLocation = LocationUtils.getAccurateLocation(fusedLocationClient) + currentLocation = LocationUtils.getAccurateLocation(fusedLocationClient, dispatcherProvider.io()) } else { currentLocation = LocationUtils.getApproximateLocation(fusedLocationClient) } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt index 51fb5a8edf7..1e0abf6ab29 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt @@ -31,6 +31,7 @@ import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber +import kotlin.coroutines.CoroutineContext object LocationUtils { @@ -43,9 +44,9 @@ object LocationUtils { @SuppressLint("MissingPermission") suspend fun getAccurateLocation( - fusedLocationClient: FusedLocationProviderClient, + fusedLocationClient: FusedLocationProviderClient, coroutineContext: CoroutineContext ): Location? { - return withContext(Dispatchers.IO) { + return withContext(coroutineContext) { suspendCoroutine { continuation -> fusedLocationClient .getCurrentLocation( diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt index d5edec705b6..25bdb6da7dd 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt @@ -219,7 +219,7 @@ class QuestionnaireActivityTest : RobolectricTest() { setupActivity() assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logQuestionnaireLocation) - val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) + val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) assertNotNull(fusedLocationProviderClient) shadowOf(questionnaireActivity).grantPermissions(android.Manifest.permission.ACCESS_FINE_LOCATION) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt index 732fa05dfbd..434ab9e8129 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt @@ -16,10 +16,14 @@ package org.smartregister.fhircore.quest.util +import android.app.Application import android.content.Context +import android.content.Intent import android.location.Location import android.location.LocationManager +import androidx.test.core.app.ApplicationProvider import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices import io.mockk.coEvery import io.mockk.every import io.mockk.mockk @@ -28,24 +32,24 @@ import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test +import org.robolectric.Robolectric +import org.robolectric.android.controller.ActivityController +import org.smartregister.fhircore.engine.util.test.HiltActivityForTest +import org.smartregister.fhircore.quest.robolectric.RobolectricTest -class LocationUtilsTest { - private lateinit var context: Context +class LocationUtilsTest: RobolectricTest() { private lateinit var fusedLocationProviderClient: FusedLocationProviderClient - + private val activityController: ActivityController = Robolectric.buildActivity( HiltActivityForTest::class.java ) + private lateinit var context: HiltActivityForTest @Before fun setUp() { - context = mockk(relaxed = true) - fusedLocationProviderClient = mockk(relaxed = true) - mockkObject(LocationUtils) + context = activityController.create().resume().get() } @Test fun `test isLocationEnabled when GPS provider is enabled`() { - val locationManager = mockk() - every { context.getSystemService(Context.LOCATION_SERVICE) } returns locationManager - every { locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) } returns true - every { locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) } returns false + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + locationManager.setTestProviderEnabled(LocationManager.GPS_PROVIDER, true) val result = LocationUtils.isLocationEnabled(context) @@ -54,15 +58,30 @@ class LocationUtilsTest { @Test fun `test isLocationEnabled when Network provider is enabled`() { - val locationManager = mockk() - every { context.getSystemService(Context.LOCATION_SERVICE) } returns locationManager - every { locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) } returns false - every { locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) } returns true + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + locationManager.setTestProviderEnabled(LocationManager.NETWORK_PROVIDER, true) val result = LocationUtils.isLocationEnabled(context) assert(result) } + +// @Test +// fun `test getAccurateLocation`() = runBlocking { +// val location = Location("").apply { +// latitude = 36.0 +// longitude = 1.0 +// } +// fusedLocationProviderClient.setMockLocation(location) +// +// val testDispatcher = this.coroutineContext +// +// +// val result = LocationUtils.getAccurateLocation(fusedLocationProviderClient, testDispatcher) +// +// assertEquals(location.latitude, result?.latitude ?: 0.0, 0.0) +// assertEquals(location.longitude, result?.longitude ?: 0.0, 0.0) +// } @Test fun `test getAccurateLocation`() = runBlocking { From 350a7471fbbe21eb7029c5c37dddbde13f80d2d8 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Wed, 28 Feb 2024 11:01:04 +0300 Subject: [PATCH 25/41] Update LocationUtils tests --- .../configs/app/application_config.json | 3 +- .../ui/questionnaire/QuestionnaireActivity.kt | 2 +- .../fhircore/quest/util/LocationUtils.kt | 4 +- .../fhircore/quest/util/LocationUtilsTest.kt | 71 ++++++++++--------- 4 files changed, 41 insertions(+), 39 deletions(-) diff --git a/android/quest/src/main/assets/configs/app/application_config.json b/android/quest/src/main/assets/configs/app/application_config.json index 646a8ea8439..3db42837ddc 100644 --- a/android/quest/src/main/assets/configs/app/application_config.json +++ b/android/quest/src/main/assets/configs/app/application_config.json @@ -81,5 +81,6 @@ } ] } - ] + ], + "logQuestionnaireLocation": true } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 20008680331..e74fc4a03c1 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -208,7 +208,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { if (highAccuracy) { currentLocation = LocationUtils.getAccurateLocation(fusedLocationClient, dispatcherProvider.io()) } else { - currentLocation = LocationUtils.getApproximateLocation(fusedLocationClient) + currentLocation = LocationUtils.getApproximateLocation(fusedLocationClient, dispatcherProvider.io()) } } catch (e: Exception) { Timber.e(e, "Failed to get GPS location for questionnaire: ${questionnaireConfig.id}") diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt index 1e0abf6ab29..ae37a34e610 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt @@ -76,9 +76,9 @@ object LocationUtils { @SuppressLint("MissingPermission") suspend fun getApproximateLocation( - fusedLocationClient: FusedLocationProviderClient, + fusedLocationClient: FusedLocationProviderClient, coroutineContext: CoroutineContext ): Location? { - return withContext(Dispatchers.IO) { + return withContext(coroutineContext) { suspendCoroutine { continuation -> fusedLocationClient .getCurrentLocation( diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt index 434ab9e8129..dfca89472a4 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt @@ -16,19 +16,13 @@ package org.smartregister.fhircore.quest.util -import android.app.Application import android.content.Context -import android.content.Intent import android.location.Location import android.location.LocationManager -import androidx.test.core.app.ApplicationProvider import com.google.android.gms.location.FusedLocationProviderClient -import com.google.android.gms.location.LocationServices -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import kotlin.test.assertEquals +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test @@ -36,6 +30,8 @@ import org.robolectric.Robolectric import org.robolectric.android.controller.ActivityController import org.smartregister.fhircore.engine.util.test.HiltActivityForTest import org.smartregister.fhircore.quest.robolectric.RobolectricTest +import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.assertEquals class LocationUtilsTest: RobolectricTest() { private lateinit var fusedLocationProviderClient: FusedLocationProviderClient @@ -66,42 +62,47 @@ class LocationUtilsTest: RobolectricTest() { assert(result) } -// @Test -// fun `test getAccurateLocation`() = runBlocking { -// val location = Location("").apply { -// latitude = 36.0 -// longitude = 1.0 -// } -// fusedLocationProviderClient.setMockLocation(location) -// -// val testDispatcher = this.coroutineContext -// -// -// val result = LocationUtils.getAccurateLocation(fusedLocationProviderClient, testDispatcher) -// -// assertEquals(location.latitude, result?.latitude ?: 0.0, 0.0) -// assertEquals(location.longitude, result?.longitude ?: 0.0, 0.0) -// } - @Test fun `test getAccurateLocation`() = runBlocking { - val location = mockk() - - coEvery { LocationUtils.getAccurateLocation(fusedLocationProviderClient) } returns location + val location = Location("").apply { + latitude = 36.0 + longitude = 1.0 + } + fusedLocationProviderClient.setMockLocation(location) - val result = LocationUtils.getAccurateLocation(fusedLocationProviderClient) + val result = LocationUtils.getAccurateLocation(fusedLocationProviderClient, coroutineContext) - assertEquals(location, result) + assertEquals(location.latitude, result?.latitude ?: 0.0, 0.0) + assertEquals(location.longitude, result?.longitude ?: 0.0, 0.0) } @Test fun `test getApproximateLocation`() = runBlocking { - val location = mockk() + val location = Location("").apply { + latitude = 36.0 + longitude = 1.0 + } + fusedLocationProviderClient.setMockLocation(location) + + val result = LocationUtils.getApproximateLocation(fusedLocationProviderClient, coroutineContext) + assertEquals(location.latitude, result?.latitude ?: 0.0, 0.0) + assertEquals(location.longitude, result?.longitude ?: 0.0, 0.0) + } + + @Test + fun `test getAccurateLocation with cancellation`() = runBlocking { + val job = launch { + delay(500) + coroutineContext.cancel() + } - coEvery { LocationUtils.getApproximateLocation(fusedLocationProviderClient) } returns location + val result = runCatching { + LocationUtils.getAccurateLocation(fusedLocationProviderClient, coroutineContext) + } - val result = LocationUtils.getApproximateLocation(fusedLocationProviderClient) + assertEquals(true, result.isFailure) + assertEquals(true, result.exceptionOrNull() is CancellationException) - assertEquals(location, result) + job.join() } } From 3b7c18302a70c07f588e151d7b66b01285d0c58b Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Wed, 28 Feb 2024 11:06:36 +0300 Subject: [PATCH 26/41] Run spotlessApply --- .../ui/questionnaire/QuestionnaireActivity.kt | 8 +++-- .../fhircore/quest/util/LocationUtils.kt | 9 +++--- .../QuestionnaireActivityTest.kt | 25 ++++++++-------- .../fhircore/quest/util/LocationUtilsTest.kt | 30 +++++++++++-------- 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index e74fc4a03c1..0662ffe1b43 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -39,6 +39,7 @@ 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 @@ -63,7 +64,6 @@ 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 -import javax.inject.Inject @AndroidEntryPoint class QuestionnaireActivity : BaseMultiLanguageActivity() { @@ -206,9 +206,11 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { lifecycleScope.launch { try { if (highAccuracy) { - currentLocation = LocationUtils.getAccurateLocation(fusedLocationClient, dispatcherProvider.io()) + currentLocation = + LocationUtils.getAccurateLocation(fusedLocationClient, dispatcherProvider.io()) } else { - currentLocation = LocationUtils.getApproximateLocation(fusedLocationClient, dispatcherProvider.io()) + currentLocation = + LocationUtils.getApproximateLocation(fusedLocationClient, dispatcherProvider.io()) } } catch (e: Exception) { Timber.e(e, "Failed to get GPS location for questionnaire: ${questionnaireConfig.id}") diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt index ae37a34e610..4e370d20f1f 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt @@ -25,13 +25,12 @@ import com.google.android.gms.location.Priority import com.google.android.gms.tasks.CancellationToken import com.google.android.gms.tasks.CancellationTokenSource import com.google.android.gms.tasks.OnTokenCanceledListener +import kotlin.coroutines.CoroutineContext import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber -import kotlin.coroutines.CoroutineContext object LocationUtils { @@ -44,7 +43,8 @@ object LocationUtils { @SuppressLint("MissingPermission") suspend fun getAccurateLocation( - fusedLocationClient: FusedLocationProviderClient, coroutineContext: CoroutineContext + fusedLocationClient: FusedLocationProviderClient, + coroutineContext: CoroutineContext, ): Location? { return withContext(coroutineContext) { suspendCoroutine { continuation -> @@ -76,7 +76,8 @@ object LocationUtils { @SuppressLint("MissingPermission") suspend fun getApproximateLocation( - fusedLocationClient: FusedLocationProviderClient, coroutineContext: CoroutineContext + fusedLocationClient: FusedLocationProviderClient, + coroutineContext: CoroutineContext, ): Location? { return withContext(coroutineContext) { suspendCoroutine { continuation -> diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt index 25bdb6da7dd..bbc71662d42 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt @@ -26,7 +26,6 @@ import androidx.test.core.app.ApplicationProvider import com.google.android.fhir.FhirEngine import com.google.android.fhir.datacapture.QuestionnaireFragment import com.google.android.fhir.db.ResourceNotFoundException -import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidRule @@ -40,9 +39,12 @@ import io.mockk.runs import io.mockk.spyk import io.mockk.unmockkStatic import io.mockk.verify +import javax.inject.Inject import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue +import kotlin.test.assertNotNull +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest @@ -71,9 +73,6 @@ import org.smartregister.fhircore.quest.R import org.smartregister.fhircore.quest.app.fakes.Faker import org.smartregister.fhircore.quest.robolectric.RobolectricTest import org.smartregister.fhircore.quest.util.LocationUtils -import javax.inject.Inject -import kotlin.test.assertNotNull -import kotlin.time.Duration.Companion.seconds @OptIn(ExperimentalCoroutinesApi::class) @HiltAndroidTest @@ -219,9 +218,11 @@ class QuestionnaireActivityTest : RobolectricTest() { setupActivity() assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logQuestionnaireLocation) - val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) + val fusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient(questionnaireActivity) assertNotNull(fusedLocationProviderClient) - shadowOf(questionnaireActivity).grantPermissions(android.Manifest.permission.ACCESS_FINE_LOCATION) + shadowOf(questionnaireActivity) + .grantPermissions(android.Manifest.permission.ACCESS_FINE_LOCATION) assertTrue(LocationUtils.isLocationEnabled(questionnaireActivity)) @@ -229,7 +230,6 @@ class QuestionnaireActivityTest : RobolectricTest() { assertTrue(questionnaireActivity.hasLocationPermissions()) questionnaireActivity.fetchLocation() assertNotNull(questionnaireActivity.currentLocation) - } @Test @@ -237,10 +237,12 @@ class QuestionnaireActivityTest : RobolectricTest() { setupActivity() assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logQuestionnaireLocation) - val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) + val fusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient(questionnaireActivity) assertNotNull(fusedLocationProviderClient) - shadowOf(questionnaireActivity).grantPermissions(android.Manifest.permission.ACCESS_FINE_LOCATION) + shadowOf(questionnaireActivity) + .grantPermissions(android.Manifest.permission.ACCESS_FINE_LOCATION) val locationManager = questionnaireActivity.getSystemService(Context.LOCATION_SERVICE) as LocationManager locationManager.setTestProviderEnabled(LocationManager.GPS_PROVIDER, false) @@ -255,11 +257,11 @@ class QuestionnaireActivityTest : RobolectricTest() { @Test fun `setupLocationServices should launch location permissions dialog if permissions are not granted`() { - setupActivity() assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logQuestionnaireLocation) - val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) + val fusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient(questionnaireActivity) assertNotNull(fusedLocationProviderClient) assertTrue(LocationUtils.isLocationEnabled(questionnaireActivity)) @@ -267,7 +269,6 @@ class QuestionnaireActivityTest : RobolectricTest() { val dialog = questionnaireActivity.launchLocationPermissionsDialog() assertNotNull(dialog) - } private fun setupActivity() { diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt index dfca89472a4..a63f2047595 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt @@ -20,6 +20,8 @@ import android.content.Context import android.location.Location import android.location.LocationManager import com.google.android.gms.location.FusedLocationProviderClient +import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.assertEquals import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -30,13 +32,13 @@ import org.robolectric.Robolectric import org.robolectric.android.controller.ActivityController import org.smartregister.fhircore.engine.util.test.HiltActivityForTest import org.smartregister.fhircore.quest.robolectric.RobolectricTest -import kotlin.coroutines.cancellation.CancellationException -import kotlin.test.assertEquals -class LocationUtilsTest: RobolectricTest() { +class LocationUtilsTest : RobolectricTest() { private lateinit var fusedLocationProviderClient: FusedLocationProviderClient - private val activityController: ActivityController = Robolectric.buildActivity( HiltActivityForTest::class.java ) + private val activityController: ActivityController = + Robolectric.buildActivity(HiltActivityForTest::class.java) private lateinit var context: HiltActivityForTest + @Before fun setUp() { context = activityController.create().resume().get() @@ -61,13 +63,14 @@ class LocationUtilsTest: RobolectricTest() { assert(result) } - + @Test fun `test getAccurateLocation`() = runBlocking { - val location = Location("").apply { - latitude = 36.0 - longitude = 1.0 - } + val location = + Location("").apply { + latitude = 36.0 + longitude = 1.0 + } fusedLocationProviderClient.setMockLocation(location) val result = LocationUtils.getAccurateLocation(fusedLocationProviderClient, coroutineContext) @@ -78,10 +81,11 @@ class LocationUtilsTest: RobolectricTest() { @Test fun `test getApproximateLocation`() = runBlocking { - val location = Location("").apply { - latitude = 36.0 - longitude = 1.0 - } + val location = + Location("").apply { + latitude = 36.0 + longitude = 1.0 + } fusedLocationProviderClient.setMockLocation(location) val result = LocationUtils.getApproximateLocation(fusedLocationProviderClient, coroutineContext) From 84faa02be06a5d75b065902172d6cf967072f01c Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Wed, 28 Feb 2024 11:47:38 +0300 Subject: [PATCH 27/41] Refactor PermissionsUtilsTest --- .../quest/util/PermissionsUtilsTest.kt | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt index e513e65f5fb..3b1cfdc8fe1 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt @@ -17,24 +17,31 @@ package org.smartregister.fhircore.quest.util import android.Manifest -import android.content.Context -import android.content.pm.PackageManager -import androidx.core.content.ContextCompat -import io.mockk.every -import io.mockk.mockk import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue +import org.junit.Before import org.junit.Test - -class PermissionsUtilsTest { - val context = mockk() +import org.robolectric.Robolectric +import org.robolectric.Shadows.shadowOf +import org.robolectric.android.controller.ActivityController +import org.smartregister.fhircore.engine.util.test.HiltActivityForTest +import org.smartregister.fhircore.quest.robolectric.RobolectricTest + +class PermissionsUtilsTest : RobolectricTest() { + private val activityController: ActivityController = + Robolectric.buildActivity(HiltActivityForTest::class.java) + private lateinit var context: HiltActivityForTest + + @Before + fun setUp() { + context = activityController.create().resume().get() + } @Test fun checkAllPermissionsGranted() { val permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION) - every { ContextCompat.checkSelfPermission(context, any()) } returns - PackageManager.PERMISSION_GRANTED + shadowOf(context).grantPermissions(Manifest.permission.ACCESS_FINE_LOCATION) val result = PermissionUtils.checkPermissions(context, permissions) @@ -45,8 +52,7 @@ class PermissionsUtilsTest { fun `checkPermissions should return false when any permission is not granted`() { val permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION) - every { ContextCompat.checkSelfPermission(context, any()) } returns - PackageManager.PERMISSION_DENIED + shadowOf(context).denyPermissions(Manifest.permission.ACCESS_FINE_LOCATION) val result = PermissionUtils.checkPermissions(context, permissions) From 67d532048b3a24532c0c904f680d2de2083974a0 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Wed, 28 Feb 2024 13:03:44 +0300 Subject: [PATCH 28/41] Refactor ResourceUtilsTest --- .../fhircore/quest/util/ResourceUtilsTest.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt index c8d7c94ca80..91227aa87ab 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt @@ -27,16 +27,17 @@ class ResourceUtilsTest { @Test fun testLocationResourceIsCreated() { - val location: Location = mockk() - every { location.longitude } returns 10.0 - every { location.latitude } returns 20.0 - every { location.altitude } returns 30.0 + val location = Location("").apply { + longitude = 10.0 + latitude = 20.0 + altitude = 30.0 + } - val locationResource = ResourceUtils.createLocationResource(location) + val locationResource = ResourceUtils.createLocationResource(location, ) assertNotNull(locationResource.id) - assertEquals(locationResource.position.longitude.toDouble(), 10.0) - assertEquals(locationResource.position.latitude.toDouble(), 20.0) - assertEquals(locationResource.position.altitude.toDouble(), 30.0) + assertEquals(location.longitude.toBigDecimal(), locationResource.position.longitude) + assertEquals(location.latitude.toBigDecimal(), locationResource.position.latitude) + assertEquals(location.altitude.toBigDecimal(), locationResource.position.altitude) } } From 5aafb6bde63a0bb46d7dcfe3fd2eef88170c49c0 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Wed, 28 Feb 2024 13:05:13 +0300 Subject: [PATCH 29/41] Run SpotlessApply --- .../fhircore/quest/util/ResourceUtilsTest.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt index 91227aa87ab..d4b5228384f 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt @@ -17,8 +17,6 @@ package org.smartregister.fhircore.quest.util import android.location.Location -import io.mockk.every -import io.mockk.mockk import kotlin.test.assertEquals import kotlin.test.assertNotNull import org.junit.Test @@ -27,13 +25,14 @@ class ResourceUtilsTest { @Test fun testLocationResourceIsCreated() { - val location = Location("").apply { - longitude = 10.0 - latitude = 20.0 - altitude = 30.0 - } + val location = + Location("").apply { + longitude = 10.0 + latitude = 20.0 + altitude = 30.0 + } - val locationResource = ResourceUtils.createLocationResource(location, ) + val locationResource = ResourceUtils.createLocationResource(location) assertNotNull(locationResource.id) assertEquals(location.longitude.toBigDecimal(), locationResource.position.longitude) From 745defe12bdb6fd5f65b37feca86081ef37eda98 Mon Sep 17 00:00:00 2001 From: Simon Njoroge Date: Tue, 5 Mar 2024 18:31:29 +0300 Subject: [PATCH 30/41] Update ApplicationConfiguration docs --- .../configuring/config-types/application.mdx | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/docs/engineering/android-app/configuring/config-types/application.mdx b/docs/engineering/android-app/configuring/config-types/application.mdx index d92af76c0d5..a2c08c11464 100644 --- a/docs/engineering/android-app/configuring/config-types/application.mdx +++ b/docs/engineering/android-app/configuring/config-types/application.mdx @@ -23,7 +23,7 @@ There can only be one instance of application configuration for the entire appli ], "useDarkTheme": false, "syncInterval": 15, - "syncStrategy": [ + "syncStrategies": [ "Location", "Organization", "CareTeam", @@ -48,7 +48,8 @@ There can only be one instance of application configuration for the entire appli "Questionnaire", "QuestionnaireResponse" ] - } + }, + "logQuestionnaireLocation": false } ``` @@ -58,17 +59,26 @@ There can only be one instance of application configuration for the entire appli |Property | Description | Required | Default | |--|--|:--:|:--:| -appId | Unique identifier for the application | Yes | | -configType | Type of configuration | Yes | `application` | -appTitle | Name of the application displayed on side menu (drawer) | No | "" | -remoteSyncPageSize | Sync batch size | Yes | 100 | -languages | Supported languages | Yes | `['en']` | -useDarkTheme | Indicate whether to apply dark theme | Yes | `false` | -syncInterval | Configuration duration for periodic sync | Yes | `30` | -syncStrategy | Tag every resource with the values for the resource types indicated here | Yes | `emptyList()` | -loginConfig.showLogo | Display logo in login page | Yes | `true` | -loginConfig.enablePin | Request user for pin after login; to be used for subsequent logins | No | `false` | -loginConfig.logoHeight | Set the maximum height a logo can have | No | 120 | -loginConfig.logoWidth | Set the maximum width a logo can have | No | 140 | -loginConfig.showAppTitle | Toggle App title in LoginScreen visibility | No | true | -deviceToDeviceSync.resourcesToSync | Types of resource to be synced from one device to another during peer connection | No | `false` | +`appId` | Unique identifier for the application | Yes | | +`configType` | Type of configuration | Yes | `application` | +`appTitle` | Name of the application displayed on side menu (drawer) | No | `""` | +`remoteSyncPageSize` | Sync batch size | Yes | `100` | +`languages` | Supported languages | Yes | `['en']` | +`useDarkTheme` | Indicate whether to apply dark theme | Yes | `false` | +`syncInterval` | Configuration duration for periodic sync | Yes | `30` | +`syncStrategies` | Tag every resource with the values for the resource types indicated here | Yes | `emptyList()` | +`loginConfig.showLogo` | Display logo in login page | Yes | `true` | +`loginConfig.enablePin` | Request user for pin after login; to be used for subsequent logins | No | `false` | +`loginConfig.logoHeight` | Set the maximum height a logo can have | No | `120` | +`loginConfig.logoWidth` | Set the maximum width a logo can have | No | `140` | +`loginConfig.showAppTitle` | Toggle App title in LoginScreen visibility | No | `true` | +`deviceToDeviceSync.resourcesToSync` | Types of resource to be synced from one device to another during peer connection | No | `false` | +`snackBarTheme` | Color styling for the SnackBar | Yes | `SnackBarThemeConfig()` | +`reportRepeatTime` | Time when to re-run reporting | Yes | `""` | +`taskStatusUpdateJobDuration` | Interval of when to run status update service | Yes | `PT15M` | +`taskExpireJobDuration` | Interval of when to run task expiry service | Yes | `PT30M` | +`taskCompleteCarePlanJobDuration` | Interval of when to run task completion service | Yes | `PT60M` | +`showLogo` | Toggle whether to show the logo | Yes | `true` | +`taskBackgroundWorkerBatchSize` | Batch size of tasks to be fetched from the server | Yes | `500` | +`eventWorkflows` | A list of `EventWorkflow`s | Yes | `emptyList()` | +`logQuestionnaireLocation` | Toggle whether to capture GPS coordinates of where a questionnaire is filled | Yes | `false` | \ No newline at end of file From 7e47f5c3070abdf134acf9c098bac926ec7f9bc7 Mon Sep 17 00:00:00 2001 From: Simon Njoroge Date: Mon, 11 Mar 2024 10:22:30 +0300 Subject: [PATCH 31/41] Make logGpsLocations config a list in ApplicationConfiguration - Update docs --- .../engine/configuration/app/ApplicationConfiguration.kt | 6 +++++- .../quest/ui/questionnaire/QuestionnaireActivity.kt | 3 ++- .../quest/ui/questionnaire/QuestionnaireActivityTest.kt | 6 +++--- .../android-app/configuring/config-types/application.mdx | 4 +++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt index 7bc2ac074f6..52048fc0a01 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt @@ -41,5 +41,9 @@ data class ApplicationConfiguration( val showLogo: Boolean = true, val taskBackgroundWorkerBatchSize: Int = 500, val eventWorkflows: List = emptyList(), - val logQuestionnaireLocation: Boolean = false, + val logGpsLocations: List = listOf(), ) : Configuration() + +enum class LocationLogOptions { + QUESTIONNAIRE +} \ No newline at end of file diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 0662ffe1b43..00408b00f62 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -46,6 +46,7 @@ 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 @@ -128,7 +129,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { } fun setupLocationServices() { - if (viewModel.applicationConfiguration.logQuestionnaireLocation) { + if (viewModel.applicationConfiguration.logGpsLocations.contains(LocationLogOptions.QUESTIONNAIRE)) { fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) if (!LocationUtils.isLocationEnabled(this)) { diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt index bbc71662d42..b868c518040 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt @@ -216,7 +216,7 @@ class QuestionnaireActivityTest : RobolectricTest() { @Test fun `setupLocationServices should fetch location when location is enabled and permissions granted`() { setupActivity() - assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logQuestionnaireLocation) + assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logGpsLocations) val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) @@ -235,7 +235,7 @@ class QuestionnaireActivityTest : RobolectricTest() { @Test fun `setupLocationServices should open location settings if location is disabled`() { setupActivity() - assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logQuestionnaireLocation) + assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logGpsLocations) val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) @@ -258,7 +258,7 @@ class QuestionnaireActivityTest : RobolectricTest() { @Test fun `setupLocationServices should launch location permissions dialog if permissions are not granted`() { setupActivity() - assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logQuestionnaireLocation) + assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logGpsLocations) val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) diff --git a/docs/engineering/android-app/configuring/config-types/application.mdx b/docs/engineering/android-app/configuring/config-types/application.mdx index a2c08c11464..c4958e50293 100644 --- a/docs/engineering/android-app/configuring/config-types/application.mdx +++ b/docs/engineering/android-app/configuring/config-types/application.mdx @@ -49,7 +49,9 @@ There can only be one instance of application configuration for the entire appli "QuestionnaireResponse" ] }, - "logQuestionnaireLocation": false + "logGpsLocations": [ + "QUESTIONNAIRE" + ] } ``` From 17289f8b2c8324b3d6e78fba0a6abaea1b02df7b Mon Sep 17 00:00:00 2001 From: Simon Njoroge Date: Mon, 11 Mar 2024 10:40:41 +0300 Subject: [PATCH 32/41] Update tests --- .../quest/ui/questionnaire/QuestionnaireActivityTest.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt index b868c518040..d4985bf0ea8 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt @@ -63,6 +63,7 @@ import org.robolectric.shadows.ShadowAlertDialog import org.robolectric.shadows.ShadowToast import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig +import org.smartregister.fhircore.engine.configuration.app.LocationLogOptions import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.domain.model.ActionParameter import org.smartregister.fhircore.engine.domain.model.ActionParameterType @@ -216,7 +217,7 @@ class QuestionnaireActivityTest : RobolectricTest() { @Test fun `setupLocationServices should fetch location when location is enabled and permissions granted`() { setupActivity() - assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logGpsLocations) + assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logGpsLocations.contains(LocationLogOptions.QUESTIONNAIRE)) val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) @@ -235,7 +236,7 @@ class QuestionnaireActivityTest : RobolectricTest() { @Test fun `setupLocationServices should open location settings if location is disabled`() { setupActivity() - assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logGpsLocations) + assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logGpsLocations.contains(LocationLogOptions.QUESTIONNAIRE)) val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) @@ -258,7 +259,7 @@ class QuestionnaireActivityTest : RobolectricTest() { @Test fun `setupLocationServices should launch location permissions dialog if permissions are not granted`() { setupActivity() - assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logGpsLocations) + assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logGpsLocations.contains(LocationLogOptions.QUESTIONNAIRE)) val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) From 833966268c8995c7dd33f23748a54ba7a284a96f Mon Sep 17 00:00:00 2001 From: Simon Njoroge Date: Mon, 11 Mar 2024 11:54:01 +0300 Subject: [PATCH 33/41] Update test configs --- .../app/ApplicationConfiguration.kt | 6 +++--- .../assets/configs/app/application_config.json | 4 +++- .../ui/questionnaire/QuestionnaireActivity.kt | 4 +++- .../assets/configs/app/application_config.json | 4 +++- .../questionnaire/QuestionnaireActivityTest.kt | 18 +++++++++++++++--- 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt index 52048fc0a01..486bfcc30e9 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt @@ -41,9 +41,9 @@ data class ApplicationConfiguration( val showLogo: Boolean = true, val taskBackgroundWorkerBatchSize: Int = 500, val eventWorkflows: List = emptyList(), - val logGpsLocations: List = listOf(), + val logGpsLocation: List = listOf(), ) : Configuration() enum class LocationLogOptions { - QUESTIONNAIRE -} \ No newline at end of file + QUESTIONNAIRE, +} diff --git a/android/quest/src/main/assets/configs/app/application_config.json b/android/quest/src/main/assets/configs/app/application_config.json index 3db42837ddc..2e4a334048c 100644 --- a/android/quest/src/main/assets/configs/app/application_config.json +++ b/android/quest/src/main/assets/configs/app/application_config.json @@ -82,5 +82,7 @@ ] } ], - "logQuestionnaireLocation": true + "logGpsLocation": [ + "QUESTIONNAIRE" + ] } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 00408b00f62..2586e7b860c 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -129,7 +129,9 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { } fun setupLocationServices() { - if (viewModel.applicationConfiguration.logGpsLocations.contains(LocationLogOptions.QUESTIONNAIRE)) { + if ( + viewModel.applicationConfiguration.logGpsLocation.contains(LocationLogOptions.QUESTIONNAIRE) + ) { fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) if (!LocationUtils.isLocationEnabled(this)) { diff --git a/android/quest/src/test/assets/configs/app/application_config.json b/android/quest/src/test/assets/configs/app/application_config.json index e51c1928080..85a96d76b35 100644 --- a/android/quest/src/test/assets/configs/app/application_config.json +++ b/android/quest/src/test/assets/configs/app/application_config.json @@ -29,5 +29,7 @@ "QuestionnaireResponse" ] }, - "logQuestionnaireLocation": true + "logGpsLocation": [ + "QUESTIONNAIRE" + ] } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt index d4985bf0ea8..0c1326bd56a 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt @@ -217,7 +217,11 @@ class QuestionnaireActivityTest : RobolectricTest() { @Test fun `setupLocationServices should fetch location when location is enabled and permissions granted`() { setupActivity() - assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logGpsLocations.contains(LocationLogOptions.QUESTIONNAIRE)) + assertTrue( + questionnaireActivity.viewModel.applicationConfiguration.logGpsLocation.contains( + LocationLogOptions.QUESTIONNAIRE, + ), + ) val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) @@ -236,7 +240,11 @@ class QuestionnaireActivityTest : RobolectricTest() { @Test fun `setupLocationServices should open location settings if location is disabled`() { setupActivity() - assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logGpsLocations.contains(LocationLogOptions.QUESTIONNAIRE)) + assertTrue( + questionnaireActivity.viewModel.applicationConfiguration.logGpsLocation.contains( + LocationLogOptions.QUESTIONNAIRE, + ), + ) val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) @@ -259,7 +267,11 @@ class QuestionnaireActivityTest : RobolectricTest() { @Test fun `setupLocationServices should launch location permissions dialog if permissions are not granted`() { setupActivity() - assertTrue(questionnaireActivity.viewModel.applicationConfiguration.logGpsLocations.contains(LocationLogOptions.QUESTIONNAIRE)) + assertTrue( + questionnaireActivity.viewModel.applicationConfiguration.logGpsLocation.contains( + LocationLogOptions.QUESTIONNAIRE, + ), + ) val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(questionnaireActivity) From e2e5bec9c526d4acd30e3f5a428d39554bb081a7 Mon Sep 17 00:00:00 2001 From: Simon Njoroge Date: Tue, 12 Mar 2024 14:49:20 +0300 Subject: [PATCH 34/41] Update docs on logGpsLocation config option --- .../engine/configuration/app/ApplicationConfiguration.kt | 2 +- .../android-app/configuring/config-types/application.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt index 486bfcc30e9..2906d1e715c 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/app/ApplicationConfiguration.kt @@ -41,7 +41,7 @@ data class ApplicationConfiguration( val showLogo: Boolean = true, val taskBackgroundWorkerBatchSize: Int = 500, val eventWorkflows: List = emptyList(), - val logGpsLocation: List = listOf(), + val logGpsLocation: List = emptyList(), ) : Configuration() enum class LocationLogOptions { diff --git a/docs/engineering/android-app/configuring/config-types/application.mdx b/docs/engineering/android-app/configuring/config-types/application.mdx index c4958e50293..fc2fb224b0a 100644 --- a/docs/engineering/android-app/configuring/config-types/application.mdx +++ b/docs/engineering/android-app/configuring/config-types/application.mdx @@ -83,4 +83,4 @@ There can only be one instance of application configuration for the entire appli `showLogo` | Toggle whether to show the logo | Yes | `true` | `taskBackgroundWorkerBatchSize` | Batch size of tasks to be fetched from the server | Yes | `500` | `eventWorkflows` | A list of `EventWorkflow`s | Yes | `emptyList()` | -`logQuestionnaireLocation` | Toggle whether to capture GPS coordinates of where a questionnaire is filled | Yes | `false` | \ No newline at end of file +`logGpsLocation` | A list of `LocationLogOptions` to toggle whether to capture GPS coordinates | Yes | emptyList() | \ No newline at end of file From 8381db13ae1c468bc5e538e86f98e76e70cbacf0 Mon Sep 17 00:00:00 2001 From: Simon Kiarie <696759+qiarie@users.noreply.github.com> Date: Wed, 13 Mar 2024 12:42:31 +0300 Subject: [PATCH 35/41] Update android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt Co-authored-by: Benjamin Mwalimu --- .../java/org/smartregister/fhircore/quest/util/LocationUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt index 4e370d20f1f..3632f7aea29 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/LocationUtils.kt @@ -94,7 +94,7 @@ object LocationUtils { .addOnSuccessListener { location: Location? -> if (location != null) { Timber.d( - "Approx location - lat: ${location.latitude}; long: ${location.longitude}; alt: ${location.altitude}", + "Approximate location - lat: ${location.latitude}; long: ${location.longitude}; alt: ${location.altitude}", ) continuation.resume(location) } From efdbe30e23c670f8335d2736490112d2c15356e3 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Wed, 13 Mar 2024 15:49:10 +0300 Subject: [PATCH 36/41] Run SpotlessApply --- android/engine/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/engine/build.gradle.kts b/android/engine/build.gradle.kts index 2508bc876de..0a3af2259fc 100644 --- a/android/engine/build.gradle.kts +++ b/android/engine/build.gradle.kts @@ -160,7 +160,7 @@ 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.material3) api(libs.foundation) api(libs.fhir.common.utils) api(libs.kotlinx.serialization.json) From 5c43134879dc7ec40bf0c8fce913e7000cebf05f Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Thu, 14 Mar 2024 12:48:23 +0300 Subject: [PATCH 37/41] Update fhir Location from GPS and remove null safety --- android/engine/build.gradle.kts | 1 - .../ui/questionnaire/QuestionnaireActivity.kt | 2 +- .../fhircore/quest/util/ResourceUtils.kt | 35 +++++++++---------- .../fhircore/quest/util/LocationUtilsTest.kt | 8 ++--- .../fhircore/quest/util/ResourceUtilsTest.kt | 2 +- 5 files changed, 22 insertions(+), 26 deletions(-) diff --git a/android/engine/build.gradle.kts b/android/engine/build.gradle.kts index 0a3af2259fc..da1f371ca62 100644 --- a/android/engine/build.gradle.kts +++ b/android/engine/build.gradle.kts @@ -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) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt index 2586e7b860c..eac77810bac 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivity.kt @@ -359,7 +359,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() { if (currentLocation != null) { questionnaireResponse.contained.add( - ResourceUtils.createLocationResource(gpsLocation = currentLocation), + ResourceUtils.createFhirLocationFromGpsLocation(gpsLocation = currentLocation!!), ) } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/ResourceUtils.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/ResourceUtils.kt index 135fdb32b27..823bbbcd3c9 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/ResourceUtils.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/ResourceUtils.kt @@ -16,27 +16,24 @@ package org.smartregister.fhircore.quest.util -import android.location.Location import java.util.UUID +import org.hl7.fhir.r4.model.Location -class ResourceUtils { - companion object { - fun createLocationResource( - gpsLocation: Location?, - locationResource: org.hl7.fhir.r4.model.Location? = null, - ): org.hl7.fhir.r4.model.Location { - var locationResourceCopy = locationResource +typealias GpsLocation = android.location.Location - if (locationResourceCopy == null) { - locationResourceCopy = org.hl7.fhir.r4.model.Location() - } - - locationResourceCopy.id = UUID.randomUUID().toString() - locationResourceCopy.position.latitude = gpsLocation!!.latitude.toBigDecimal() - locationResourceCopy.position.longitude = gpsLocation!!.longitude.toBigDecimal() - locationResourceCopy.position.altitude = gpsLocation!!.altitude.toBigDecimal() - - return locationResourceCopy +object ResourceUtils { + fun createFhirLocationFromGpsLocation( + gpsLocation: GpsLocation, + fhirLocation: Location? = null + ): Location { + return (fhirLocation ?: Location()).apply { + id = UUID.randomUUID().toString() + position = + Location.LocationPositionComponent().apply { + latitude = gpsLocation.latitude.toBigDecimal() + longitude = gpsLocation.longitude.toBigDecimal() + altitude = gpsLocation.altitude.toBigDecimal() + } } } -} +} \ No newline at end of file diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt index a63f2047595..0555b893398 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt @@ -75,8 +75,8 @@ class LocationUtilsTest : RobolectricTest() { val result = LocationUtils.getAccurateLocation(fusedLocationProviderClient, coroutineContext) - assertEquals(location.latitude, result?.latitude ?: 0.0, 0.0) - assertEquals(location.longitude, result?.longitude ?: 0.0, 0.0) + assertEquals(location.latitude, result!!.latitude , 0.0) + assertEquals(location.longitude, result.longitude , 0.0) } @Test @@ -89,8 +89,8 @@ class LocationUtilsTest : RobolectricTest() { fusedLocationProviderClient.setMockLocation(location) val result = LocationUtils.getApproximateLocation(fusedLocationProviderClient, coroutineContext) - assertEquals(location.latitude, result?.latitude ?: 0.0, 0.0) - assertEquals(location.longitude, result?.longitude ?: 0.0, 0.0) + assertEquals(location.latitude, result!!.latitude, 0.0) + assertEquals(location.longitude, result.longitude , 0.0) } @Test diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt index d4b5228384f..0ac8969583d 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/ResourceUtilsTest.kt @@ -32,7 +32,7 @@ class ResourceUtilsTest { altitude = 30.0 } - val locationResource = ResourceUtils.createLocationResource(location) + val locationResource = ResourceUtils.createFhirLocationFromGpsLocation(location) assertNotNull(locationResource.id) assertEquals(location.longitude.toBigDecimal(), locationResource.position.longitude) From 21d74e0d0651b514d04a52b71c78fbf5203ea760 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Thu, 14 Mar 2024 12:57:56 +0300 Subject: [PATCH 38/41] Run SpotlessApply --- .../org/smartregister/fhircore/quest/util/ResourceUtils.kt | 4 ++-- .../smartregister/fhircore/quest/util/LocationUtilsTest.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/ResourceUtils.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/ResourceUtils.kt index 823bbbcd3c9..56e4166ef3b 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/ResourceUtils.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/ResourceUtils.kt @@ -24,7 +24,7 @@ typealias GpsLocation = android.location.Location object ResourceUtils { fun createFhirLocationFromGpsLocation( gpsLocation: GpsLocation, - fhirLocation: Location? = null + fhirLocation: Location? = null, ): Location { return (fhirLocation ?: Location()).apply { id = UUID.randomUUID().toString() @@ -36,4 +36,4 @@ object ResourceUtils { } } } -} \ No newline at end of file +} diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt index 0555b893398..a2643752ef7 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/LocationUtilsTest.kt @@ -75,8 +75,8 @@ class LocationUtilsTest : RobolectricTest() { val result = LocationUtils.getAccurateLocation(fusedLocationProviderClient, coroutineContext) - assertEquals(location.latitude, result!!.latitude , 0.0) - assertEquals(location.longitude, result.longitude , 0.0) + assertEquals(location.latitude, result!!.latitude, 0.0) + assertEquals(location.longitude, result.longitude, 0.0) } @Test @@ -90,7 +90,7 @@ class LocationUtilsTest : RobolectricTest() { val result = LocationUtils.getApproximateLocation(fusedLocationProviderClient, coroutineContext) assertEquals(location.latitude, result!!.latitude, 0.0) - assertEquals(location.longitude, result.longitude , 0.0) + assertEquals(location.longitude, result.longitude, 0.0) } @Test From a6752479b72c50a395cbb616af8923450f49fb3f Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Thu, 14 Mar 2024 16:04:36 +0300 Subject: [PATCH 39/41] Update test --- .../quest/ui/questionnaire/QuestionnaireActivityTest.kt | 2 +- .../smartregister/fhircore/quest/util/PermissionsUtilsTest.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt index 0c1326bd56a..ffc7ae4d518 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireActivityTest.kt @@ -97,7 +97,7 @@ class QuestionnaireActivityTest : RobolectricTest() { @BindValue lateinit var defaultRepository: DefaultRepository @BindValue - val configurationRegistry: ConfigurationRegistry = spyk(Faker.buildTestConfigurationRegistry()) + val configurationRegistry: ConfigurationRegistry = Faker.buildTestConfigurationRegistry() @Before fun setUp() { diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt index 3b1cfdc8fe1..f16986ead2a 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/PermissionsUtilsTest.kt @@ -50,8 +50,9 @@ class PermissionsUtilsTest : RobolectricTest() { @Test fun `checkPermissions should return false when any permission is not granted`() { - val permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION) + val permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.INTERNET) + shadowOf(context).grantPermissions(Manifest.permission.INTERNET) shadowOf(context).denyPermissions(Manifest.permission.ACCESS_FINE_LOCATION) val result = PermissionUtils.checkPermissions(context, permissions) From 6328d0fb8c1c45a8399c4792ad168c0726cecc13 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Fri, 15 Mar 2024 15:27:41 +0300 Subject: [PATCH 40/41] Update application config docs --- .../android-app/configuring/config-types/application.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/engineering/android-app/configuring/config-types/application.mdx b/docs/engineering/android-app/configuring/config-types/application.mdx index fc2fb224b0a..2a8a8e0c626 100644 --- a/docs/engineering/android-app/configuring/config-types/application.mdx +++ b/docs/engineering/android-app/configuring/config-types/application.mdx @@ -7,7 +7,8 @@ title: Application These are app wide configurations used to control the application behaviour globally e.g. application theme, app language etc. :::note -There can only be one instance of application configuration for the entire application. There are instances where the `Event Workflow` is added to the application config. See [here](https://docs.opensrp.io/engineering/android-app/configuring/event-management/resource-closure-by-background-worker) +There can only be one instance of application configuration for the entire application. There are instances where the `Event Workflow` is added to the application config. See [here](https://docs.opensrp.io/engineering/android-app/configuring/event-management/resource-closure-by-background-worker) +The `logGpsLocation` config takes in a list of `LocationLogOptions` to toggle whether to capture GPS coordinates in the application. We can currently define this to capture Location GPS on Questionnaire submission by passing the log option `QUESTIONNAIRE` as shown in the sample JSON. ::: ## Sample JSON From 304b0437173f537950e74aa28280bbd13fa4cb59 Mon Sep 17 00:00:00 2001 From: Debbie Mong'are Date: Fri, 15 Mar 2024 15:33:39 +0300 Subject: [PATCH 41/41] Update location permission denied string --- android/quest/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/quest/src/main/res/values/strings.xml b/android/quest/src/main/res/values/strings.xml index 31e8b2335e1..172380e7151 100644 --- a/android/quest/src/main/res/values/strings.xml +++ b/android/quest/src/main/res/values/strings.xml @@ -112,7 +112,7 @@ Yes No - Location Access Denied: To use this feature, please enable location permissions in your device settings. + Location Access Denied: To capture GPS coordinates, please enable location permissions in your device settings. Location services are disabled. Do you want to enable them? Link %1$s copied successfully