diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml
index fceec7641e..101fe3833f 100644
--- a/android/gradle/libs.versions.toml
+++ b/android/gradle/libs.versions.toml
@@ -2,8 +2,9 @@
accompanist = "0.23.1"
activity-compose = "1.8.2"
androidJunit5 = "1.8.2.1"
+androidx-camera = "1.4.0-rc01"
androidx-paging = "3.3.0"
-androidx-test= "1.6.1"
+androidx-test = "1.6.1"
appcompat = "1.7.0"
benchmark-junit = "1.2.4"
cardview = "1.0.0"
@@ -56,6 +57,7 @@ leakcanary-android = "2.10"
lifecycle= "2.8.3"
mapbox-sdk-turf = "4.8.0"
material = "1.12.0"
+mlkit-barcode-scanning = "17.3.0"
mockk = "1.13.8"
mockk-android = "1.13.8"
msg-simple = "1.2"
@@ -88,6 +90,11 @@ xercesImpl = "2.12.2"
accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist" }
accompanist-placeholder = { group = "com.google.accompanist", name = "accompanist-placeholder", version.ref = "accompanist" }
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
+androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "androidx-camera"}
+androidx-camera-extensions = { group = "androidx.camera", name = "camera-extensions", version.ref = "androidx-camera"}
+androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "androidx-camera"}
+androidx-camera-mlkit-vision = {group = "androidx.camera", name = "camera-mlkit-vision", version.ref = "androidx-camera"}
+androidx-camera-view = {group = "androidx.camera", name = "camera-view", version.ref = "androidx-camera"}
activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity-compose" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
benchmark-junit = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "benchmark-junit" }
@@ -154,6 +161,7 @@ lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-
lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
mapbox-sdk-turf = { group = "com.mapbox.mapboxsdk", name = "mapbox-sdk-turf", version.ref = "mapbox-sdk-turf" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+mlkit-barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkit-barcode-scanning"}
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk-android" }
msg-simple = { group = "com.github.java-json-tools", name = "msg-simple", version.ref = "msg-simple" }
@@ -206,6 +214,7 @@ org-owasp-dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "o
[bundles]
accompanist = ["accompanist-placeholder", "accompanist-flowlayout"]
+cameraX = ["androidx-camera-camera2", "androidx-camera-extensions", "androidx-camera-lifecycle", "androidx-camera-mlkit-vision", "androidx-camera-view"]
compose = ["activity-compose","activity-ktx", "ui", "ui-tooling-preview", "constraintlayout-compose", "foundation","runtime-livedata"]
compose-ui-test = ["ui-test-junit4","ui-test-manifest"]
coroutines = ["kotlinx-coroutines-core", "kotlinx-coroutines-android"]
diff --git a/android/quest/build.gradle.kts b/android/quest/build.gradle.kts
index 6e64030d03..bf7ca9de70 100644
--- a/android/quest/build.gradle.kts
+++ b/android/quest/build.gradle.kts
@@ -11,7 +11,7 @@ import org.json.JSONObject
plugins {
`jacoco-report`
`project-properties`
- `ktlint`
+ ktlint
id("com.android.application")
id("kotlin-android")
id("kotlin-kapt")
@@ -27,7 +27,7 @@ plugins {
sonar {
properties {
property("sonar.projectKey", "fhircore")
- property("sonar.kotlin.source.version", libs.kotlin)
+ property("sonar.kotlin.source.version", libs.versions.kotlin)
property(
"sonar.androidLint.reportPaths",
"${project.layout.buildDirectory.get()}/reports/lint-results-opensrpDebug.xml",
@@ -429,6 +429,9 @@ dependencies {
implementation(libs.dagger.hilt.android)
implementation(libs.hilt.work)
implementation(libs.gms.play.services.location)
+ implementation(libs.mlkit.barcode.scanning)
+
+ implementation(libs.bundles.cameraX)
// Annotation processors
kapt(libs.hilt.compiler)
diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/QrCodeCameraDialogFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/CameraPermissionsDialogFragment.kt
similarity index 53%
rename from android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/QrCodeCameraDialogFragment.kt
rename to android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/CameraPermissionsDialogFragment.kt
index 5a08e6c1cf..0202c11c72 100644
--- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/QrCodeCameraDialogFragment.kt
+++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/CameraPermissionsDialogFragment.kt
@@ -17,29 +17,26 @@
package org.smartregister.fhircore.quest.ui.sdc.qrCode
import android.Manifest
-import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.VisibleForTesting
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
-import com.google.android.fhir.datacapture.contrib.views.barcode.mlkit.md.LiveBarcodeScanningFragment
import org.smartregister.fhircore.engine.util.location.PermissionUtils
import org.smartregister.fhircore.quest.R
-class QrCodeCameraDialogFragment : DialogFragment(R.layout.fragment_camera_permission) {
+class CameraPermissionsDialogFragment : DialogFragment(R.layout.fragment_camera_permission) {
@VisibleForTesting
val cameraPermissionRequest =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted ->
+ parentFragmentManager.setFragmentResult(
+ CAMERA_PERMISSION_REQUEST_RESULT_KEY,
+ bundleOf(CAMERA_PERMISSION_REQUEST_RESULT_KEY to permissionGranted),
+ )
+
if (permissionGranted) {
- showQrCodeScanner()
+ dismiss()
} else {
- Toast.makeText(
- requireContext(),
- requireContext().getString(R.string.barcode_camera_permission_denied),
- Toast.LENGTH_SHORT,
- )
- .show()
dismiss()
}
}
@@ -49,7 +46,7 @@ class QrCodeCameraDialogFragment : DialogFragment(R.layout.fragment_camera_permi
when {
PermissionUtils.checkPermissions(requireContext(), listOf(Manifest.permission.CAMERA)) -> {
- showQrCodeScanner()
+ dismiss()
}
else -> {
cameraPermissionRequest.launch(Manifest.permission.CAMERA)
@@ -57,33 +54,8 @@ class QrCodeCameraDialogFragment : DialogFragment(R.layout.fragment_camera_permi
}
}
- private fun showQrCodeScanner() =
- parentFragmentManager.apply {
- setFragmentResultListener(
- LiveBarcodeScanningFragment.RESULT_REQUEST_KEY,
- requireActivity(),
- ) { _, result ->
- val qrCode = result.getString(LiveBarcodeScanningFragment.RESULT_REQUEST_KEY)?.trim()
- this.setFragmentResult(
- RESULT_REQUEST_KEY,
- bundleOf(RESULT_REQUEST_KEY to qrCode),
- )
- }
-
- val qrCodeScannerFragment = this.findFragmentByTag(QR_CODE_SCANNER_FRAGMENT_TAG)
- if (qrCodeScannerFragment == null) {
- LiveBarcodeScanningFragment()
- .show(
- this@apply,
- QR_CODE_SCANNER_FRAGMENT_TAG,
- )
- }
-
- dismiss()
- }
-
companion object {
- const val RESULT_REQUEST_KEY = "qr-code-result"
- const val QR_CODE_SCANNER_FRAGMENT_TAG = "QrCodeCameraDialogFragment"
+ const val CAMERA_PERMISSION_REQUEST_RESULT_KEY =
+ "quest.ui.sdc.qrCode.CameraPermissionsDialogFragment"
}
}
diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/scan/QRCodeScannerDialogFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/scan/QRCodeScannerDialogFragment.kt
new file mode 100644
index 0000000000..1f675c1a52
--- /dev/null
+++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/scan/QRCodeScannerDialogFragment.kt
@@ -0,0 +1,188 @@
+/*
+ * 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.ui.sdc.qrCode.scan
+
+import android.content.res.Resources
+import android.graphics.RectF
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.TextView
+import android.widget.Toast
+import androidx.annotation.VisibleForTesting
+import androidx.camera.core.ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED
+import androidx.camera.mlkit.vision.MlKitAnalyzer
+import androidx.camera.view.LifecycleCameraController
+import androidx.camera.view.PreviewView
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.toRect
+import androidx.core.os.bundleOf
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.mlkit.vision.barcode.BarcodeScanner
+import com.google.mlkit.vision.barcode.BarcodeScannerOptions
+import com.google.mlkit.vision.barcode.BarcodeScanning
+import com.google.mlkit.vision.barcode.common.Barcode
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import org.smartregister.fhircore.engine.util.location.PermissionUtils
+import org.smartregister.fhircore.quest.R
+import org.smartregister.fhircore.quest.ui.sdc.qrCode.CameraPermissionsDialogFragment
+
+internal class QRCodeScannerDialogFragment :
+ BottomSheetDialogFragment(R.layout.fragment_qr_code_scan) {
+
+ private lateinit var cameraExecutor: ExecutorService
+ private lateinit var cameraController: LifecycleCameraController
+ private lateinit var mlKitImageAnalyzer: MlKitAnalyzer
+ private val barcodeScanner: BarcodeScanner by lazy {
+ val options = BarcodeScannerOptions.Builder().setBarcodeFormats(Barcode.FORMAT_QR_CODE).build()
+ BarcodeScanning.getClient(options)
+ }
+
+ private lateinit var previewView: PreviewView
+ private lateinit var cancelScanButton: ImageButton
+ private lateinit var viewFinderImageView: ImageView
+ private lateinit var placeQrCodeScanTextView: TextView
+
+ @VisibleForTesting
+ val viewFinderBounds: RectF
+ get() {
+ val viewFinderImageViewHeight = viewFinderImageView.height
+ val viewFinderImageViewWidth = viewFinderImageView.width
+ return RectF(
+ viewFinderImageView.x,
+ viewFinderImageView.y,
+ viewFinderImageView.x + viewFinderImageViewWidth + 10,
+ viewFinderImageView.y + viewFinderImageViewHeight + 10,
+ )
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ previewView = view.findViewById(R.id.previewView)
+ viewFinderImageView = view.findViewById(R.id.viewFinderImageView)
+ placeQrCodeScanTextView = view.findViewById(R.id.placeQrCodeScanTextView)
+ cancelScanButton = view.findViewById(R.id.cancelImageButton)
+ cancelScanButton.setOnClickListener { dismiss() }
+
+ val parent = view.parent as View
+ val behavior = BottomSheetBehavior.from(parent)
+ val layoutParams = parent.layoutParams
+ layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
+ parent.layoutParams = layoutParams
+ behavior.maxHeight = (0.8 * Resources.getSystem().displayMetrics.heightPixels).toInt()
+ behavior.peekHeight = (0.8 * Resources.getSystem().displayMetrics.heightPixels).toInt()
+ behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ behavior.isDraggable = false
+
+ cameraExecutor = Executors.newSingleThreadExecutor()
+ cameraController = LifecycleCameraController(requireContext())
+ mlKitImageAnalyzer =
+ MlKitAnalyzer(
+ listOf(barcodeScanner),
+ COORDINATE_SYSTEM_VIEW_REFERENCED,
+ ContextCompat.getMainExecutor(requireActivity()),
+ ) { result: MlKitAnalyzer.Result? ->
+ val barcodeResults = result?.getValue(barcodeScanner)
+ if (
+ (barcodeResults == null) || (barcodeResults.size == 0) || (barcodeResults.first() == null)
+ ) {
+ return@MlKitAnalyzer
+ }
+
+ onQrCodeDetected(barcodeResults[0])
+ }
+
+ cameraController.setImageAnalysisAnalyzer(
+ ContextCompat.getMainExecutor(requireActivity()),
+ mlKitImageAnalyzer,
+ )
+
+ parentFragmentManager.setFragmentResultListener(
+ CameraPermissionsDialogFragment.CAMERA_PERMISSION_REQUEST_RESULT_KEY,
+ this,
+ ) { _, result ->
+ val permissionGranted =
+ result.getBoolean(CameraPermissionsDialogFragment.CAMERA_PERMISSION_REQUEST_RESULT_KEY)
+ if (!permissionGranted) {
+ Toast.makeText(
+ requireActivity(),
+ requireContext().getString(R.string.barcode_camera_permission_denied),
+ Toast.LENGTH_SHORT,
+ )
+ .show()
+ dismiss()
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ if (
+ !PermissionUtils.checkPermissions(
+ requireContext(),
+ listOf(android.Manifest.permission.CAMERA),
+ )
+ ) {
+ requestCameraPermissions()
+ } else {
+ bindCamera()
+ }
+ }
+
+ private fun requestCameraPermissions() {
+ CameraPermissionsDialogFragment().show(parentFragmentManager, TAG)
+ }
+
+ private fun bindCamera() {
+ cameraController.bindToLifecycle(this)
+ previewView.controller = cameraController
+ }
+
+ private fun isBarcodeWithinExpectedBounds(barcode: Barcode): Boolean {
+ return barcode.boundingBox?.let { viewFinderBounds.toRect().contains(it) } ?: false
+ }
+
+ @VisibleForTesting
+ fun onQrCodeDetected(barcode: Barcode) {
+ if (isBarcodeWithinExpectedBounds(barcode)) {
+ parentFragmentManager.setFragmentResult(
+ RESULT_REQUEST_KEY,
+ bundleOf(RESULT_REQUEST_KEY to barcode.rawValue),
+ )
+ dismiss()
+ } else {
+ placeQrCodeScanTextView.visibility = View.VISIBLE
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ cameraExecutor.shutdown()
+ barcodeScanner.close()
+ }
+
+ companion object {
+ private const val TAG = "QRCodeScannerDialogFragment"
+ const val RESULT_REQUEST_KEY = "quest.ui.sdc.qrCode.scan.QRCodeScannerDialogFragment"
+ }
+}
diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/QrCodeScanUtils.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/QrCodeScanUtils.kt
index 2ad380b4e6..62de1fb396 100644
--- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/QrCodeScanUtils.kt
+++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/QrCodeScanUtils.kt
@@ -17,7 +17,7 @@
package org.smartregister.fhircore.quest.util
import androidx.fragment.app.FragmentActivity
-import org.smartregister.fhircore.quest.ui.sdc.qrCode.QrCodeCameraDialogFragment
+import org.smartregister.fhircore.quest.ui.sdc.qrCode.scan.QRCodeScannerDialogFragment
object QrCodeScanUtils {
@@ -26,14 +26,14 @@ object QrCodeScanUtils {
fun scanQrCode(lifecycleOwner: FragmentActivity, onQrCodeScanResult: (String?) -> Unit) {
lifecycleOwner.supportFragmentManager.apply {
setFragmentResultListener(
- QrCodeCameraDialogFragment.RESULT_REQUEST_KEY,
+ QRCodeScannerDialogFragment.RESULT_REQUEST_KEY,
lifecycleOwner,
) { _, result ->
- val barcode = result.getString(QrCodeCameraDialogFragment.RESULT_REQUEST_KEY)
+ val barcode = result.getString(QRCodeScannerDialogFragment.RESULT_REQUEST_KEY)
onQrCodeScanResult.invoke(barcode)
}
- QrCodeCameraDialogFragment().show(this@apply, QR_CODE_SCAN_UTILS_TAG)
+ QRCodeScannerDialogFragment().show(this@apply, QR_CODE_SCAN_UTILS_TAG)
}
}
}
diff --git a/android/quest/src/main/res/drawable-hdpi/ic_scan_qr_viewfinder.png b/android/quest/src/main/res/drawable-hdpi/ic_scan_qr_viewfinder.png
new file mode 100644
index 0000000000..063c1f5f48
Binary files /dev/null and b/android/quest/src/main/res/drawable-hdpi/ic_scan_qr_viewfinder.png differ
diff --git a/android/quest/src/main/res/drawable-mdpi/ic_scan_qr_viewfinder.png b/android/quest/src/main/res/drawable-mdpi/ic_scan_qr_viewfinder.png
new file mode 100644
index 0000000000..d76359263b
Binary files /dev/null and b/android/quest/src/main/res/drawable-mdpi/ic_scan_qr_viewfinder.png differ
diff --git a/android/quest/src/main/res/drawable-xhdpi/ic_scan_qr_viewfinder.png b/android/quest/src/main/res/drawable-xhdpi/ic_scan_qr_viewfinder.png
new file mode 100644
index 0000000000..2537f0b952
Binary files /dev/null and b/android/quest/src/main/res/drawable-xhdpi/ic_scan_qr_viewfinder.png differ
diff --git a/android/quest/src/main/res/drawable-xxhdpi/ic_scan_qr_viewfinder.png b/android/quest/src/main/res/drawable-xxhdpi/ic_scan_qr_viewfinder.png
new file mode 100644
index 0000000000..da085ae8cb
Binary files /dev/null and b/android/quest/src/main/res/drawable-xxhdpi/ic_scan_qr_viewfinder.png differ
diff --git a/android/quest/src/main/res/drawable-xxxhdpi/ic_scan_qr_viewfinder.png b/android/quest/src/main/res/drawable-xxxhdpi/ic_scan_qr_viewfinder.png
new file mode 100644
index 0000000000..b3fc69c15b
Binary files /dev/null and b/android/quest/src/main/res/drawable-xxxhdpi/ic_scan_qr_viewfinder.png differ
diff --git a/android/quest/src/main/res/layout/fragment_qr_code_scan.xml b/android/quest/src/main/res/layout/fragment_qr_code_scan.xml
new file mode 100644
index 0000000000..1e5d2e3ded
--- /dev/null
+++ b/android/quest/src/main/res/layout/fragment_qr_code_scan.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/quest/src/main/res/values/strings.xml b/android/quest/src/main/res/values/strings.xml
index 91402fb480..50fde9a582 100644
--- a/android/quest/src/main/res/values/strings.xml
+++ b/android/quest/src/main/res/values/strings.xml
@@ -129,4 +129,6 @@
Camera permission request denied. Barcode may not work as expected
Place your camera over the QR Code to start scanning
Add QR code
+ Scan QR Code
+ Place your camera over the entire QR Code to start scanning
diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/CameraPermissionsDialogFragmentTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/CameraPermissionsDialogFragmentTest.kt
new file mode 100644
index 0000000000..d56dfa7494
--- /dev/null
+++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/CameraPermissionsDialogFragmentTest.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.ui.sdc.qrCode
+
+import android.Manifest
+import android.app.Application
+import androidx.fragment.app.commitNow
+import androidx.test.core.app.ApplicationProvider
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import io.mockk.spyk
+import io.mockk.verify
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.util.ReflectionHelpers
+import org.smartregister.fhircore.quest.hiltActivityForTestScenario
+import org.smartregister.fhircore.quest.robolectric.RobolectricTest
+
+@HiltAndroidTest
+class CameraPermissionsDialogFragmentTest : RobolectricTest() {
+ @get:Rule(order = 0) var hiltAndroidRule = HiltAndroidRule(this)
+
+ private val applicationContext = ApplicationProvider.getApplicationContext()
+
+ @Before
+ fun setUp() {
+ hiltAndroidRule.inject()
+ }
+
+ @Test
+ fun onResumeShouldNotLaunchCameraPermissionRequestWhenCameraPermissionGranted() {
+ shadowOf(applicationContext).grantPermissions(Manifest.permission.CAMERA)
+ hiltActivityForTestScenario().use { scenario ->
+ scenario.onActivity { activity ->
+ val qrCodeFragment = CameraPermissionsDialogFragment()
+ val cameraPermissionRequestSpy = spyk(qrCodeFragment.cameraPermissionRequest)
+ ReflectionHelpers.setField(
+ qrCodeFragment,
+ "cameraPermissionRequest",
+ cameraPermissionRequestSpy,
+ )
+
+ activity.supportFragmentManager.commitNow {
+ add(qrCodeFragment, CameraPermissionsDialogFragmentTest::class.java.simpleName)
+ }
+ verify(exactly = 0) { cameraPermissionRequestSpy.launch(Manifest.permission.CAMERA) }
+ }
+ }
+ }
+
+ @Test
+ fun onResumeShouldLaunchCameraPermissionRequestWhenPermissionDenied() {
+ shadowOf(applicationContext).denyPermissions(Manifest.permission.CAMERA)
+ hiltActivityForTestScenario().use { scenario ->
+ scenario.onActivity { activity ->
+ val qrCodeFragment = CameraPermissionsDialogFragment()
+ val cameraPermissionRequestSpy = spyk(qrCodeFragment.cameraPermissionRequest)
+ ReflectionHelpers.setField(
+ qrCodeFragment,
+ "cameraPermissionRequest",
+ cameraPermissionRequestSpy,
+ )
+
+ activity.supportFragmentManager.commitNow {
+ add(qrCodeFragment, CameraPermissionsDialogFragmentTest::class.java.simpleName)
+ }
+ verify { cameraPermissionRequestSpy.launch(Manifest.permission.CAMERA) }
+ }
+ }
+ }
+}
diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeItemViewHolderFactoryTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeItemViewHolderFactoryTest.kt
index d04814f58a..7edddb6eca 100644
--- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeItemViewHolderFactoryTest.kt
+++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeItemViewHolderFactoryTest.kt
@@ -45,6 +45,7 @@ import org.robolectric.Robolectric
import org.smartregister.fhircore.quest.R
import org.smartregister.fhircore.quest.hiltActivityForTestScenario
import org.smartregister.fhircore.quest.robolectric.RobolectricTest
+import org.smartregister.fhircore.quest.ui.sdc.qrCode.scan.QRCodeScannerDialogFragment
import org.smartregister.fhircore.quest.util.QrCodeScanUtils
@HiltAndroidTest
@@ -71,7 +72,7 @@ class EditTextQrCodeItemViewHolderFactoryTest : RobolectricTest() {
@Test
fun shouldUpdateTextCorrectlyWhenScanQrCodeReceived() {
- mockkConstructor(QrCodeCameraDialogFragment::class)
+ mockkConstructor(QRCodeScannerDialogFragment::class)
val sampleQrCode = "d84fbd12-4f22-423a-8645-5525504e1bcb"
/**
* Using style 'com.google.android.material.R.style.Theme_Material3_DayNight' to prevent
@@ -90,13 +91,13 @@ class EditTextQrCodeItemViewHolderFactoryTest : RobolectricTest() {
textInputLayout.findViewById(R.id.text_input_edit_text)
Assert.assertNotNull(textInputEditText)
every {
- anyConstructed()
+ anyConstructed()
.show(any(), QrCodeScanUtils.QR_CODE_SCAN_UTILS_TAG)
} answers
{
activity.supportFragmentManager.setFragmentResult(
- QrCodeCameraDialogFragment.RESULT_REQUEST_KEY,
- bundleOf(QrCodeCameraDialogFragment.RESULT_REQUEST_KEY to sampleQrCode),
+ QRCodeScannerDialogFragment.RESULT_REQUEST_KEY,
+ bundleOf(QRCodeScannerDialogFragment.RESULT_REQUEST_KEY to sampleQrCode),
)
}
@@ -106,7 +107,7 @@ class EditTextQrCodeItemViewHolderFactoryTest : RobolectricTest() {
Assert.assertEquals(sampleQrCode, textInputEditText.text.toString())
}
}
- unmockkConstructor(QrCodeCameraDialogFragment::class)
+ unmockkConstructor(QRCodeScannerDialogFragment::class)
}
@Test
diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/QrCodeCameraDialogFragmentTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/QrCodeCameraDialogFragmentTest.kt
deleted file mode 100644
index c8c891eb9e..0000000000
--- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/QrCodeCameraDialogFragmentTest.kt
+++ /dev/null
@@ -1,139 +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.ui.sdc.qrCode
-
-import android.Manifest
-import android.app.Application
-import android.widget.Toast
-import androidx.core.os.bundleOf
-import androidx.fragment.app.FragmentManager
-import androidx.fragment.app.commitNow
-import androidx.test.core.app.ApplicationProvider
-import com.google.android.fhir.datacapture.contrib.views.barcode.mlkit.md.LiveBarcodeScanningFragment
-import dagger.hilt.android.testing.HiltAndroidRule
-import dagger.hilt.android.testing.HiltAndroidTest
-import io.mockk.every
-import io.mockk.just
-import io.mockk.mockkConstructor
-import io.mockk.runs
-import io.mockk.spyk
-import io.mockk.unmockkConstructor
-import io.mockk.unmockkStatic
-import io.mockk.verify
-import org.junit.Assert
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.robolectric.Shadows.shadowOf
-import org.robolectric.util.ReflectionHelpers
-import org.smartregister.fhircore.quest.hiltActivityForTestScenario
-import org.smartregister.fhircore.quest.launchFragmentInHiltContainer
-import org.smartregister.fhircore.quest.robolectric.RobolectricTest
-import org.smartregister.fhircore.quest.ui.sdc.qrCode.QrCodeCameraDialogFragment.Companion.QR_CODE_SCANNER_FRAGMENT_TAG
-
-@HiltAndroidTest
-class QrCodeCameraDialogFragmentTest : RobolectricTest() {
- @get:Rule(order = 0) var hiltAndroidRule = HiltAndroidRule(this)
-
- private val applicationContext = ApplicationProvider.getApplicationContext()
-
- @Before
- fun setUp() {
- hiltAndroidRule.inject()
- }
-
- @Test
- fun onResumeShouldShowQrCodeScannerWhenCameraPermissionGranted() {
- shadowOf(applicationContext).grantPermissions(Manifest.permission.CAMERA)
- mockkConstructor(LiveBarcodeScanningFragment::class)
- every {
- anyConstructed().show(any(), any())
- } just runs
- launchFragmentInHiltContainer {
- verify {
- anyConstructed()
- .show(
- this@launchFragmentInHiltContainer.parentFragmentManager,
- QR_CODE_SCANNER_FRAGMENT_TAG,
- )
- }
- }
- unmockkConstructor(LiveBarcodeScanningFragment::class)
- }
-
- @Test
- fun onResumeShouldReturnCorrectCodeReceivedFromQrCodeScanningWhenPermissionGranted() {
- shadowOf(applicationContext).grantPermissions(Manifest.permission.CAMERA)
- mockkConstructor(LiveBarcodeScanningFragment::class)
- val sampleBarcodeResult = "13462889"
- var receivedCode: String? = null
-
- hiltActivityForTestScenario().use { scenario ->
- scenario.onActivity { activity ->
- val activityFragmentManager = activity.supportFragmentManager
- every {
- anyConstructed().show(any(), any())
- } answers
- {
- activityFragmentManager.setFragmentResult(
- LiveBarcodeScanningFragment.RESULT_REQUEST_KEY,
- bundleOf(LiveBarcodeScanningFragment.RESULT_REQUEST_KEY to sampleBarcodeResult),
- )
- }
- activityFragmentManager.setFragmentResultListener(
- QrCodeCameraDialogFragment.RESULT_REQUEST_KEY,
- activity,
- ) { _, result ->
- val code = result.getString(QrCodeCameraDialogFragment.RESULT_REQUEST_KEY)
- Assert.assertEquals(sampleBarcodeResult, code)
- receivedCode = code
- }
-
- activityFragmentManager.commitNow {
- add(QrCodeCameraDialogFragment(), QrCodeCameraDialogFragmentTest::class.java.simpleName)
- }
-
- Assert.assertNotNull(receivedCode)
- Assert.assertEquals(sampleBarcodeResult, receivedCode)
- }
- }
-
- unmockkConstructor(LiveBarcodeScanningFragment::class)
- }
-
- @Test
- fun onResumeShouldLaunchCameraPermissionRequestWhenPermissionDenied() {
- shadowOf(applicationContext).denyPermissions(Manifest.permission.CAMERA)
- hiltActivityForTestScenario().use { scenario ->
- scenario.onActivity { activity ->
- val qrCodeFragment = QrCodeCameraDialogFragment()
- val cameraPermissionRequestSpy = spyk(qrCodeFragment.cameraPermissionRequest)
- ReflectionHelpers.setField(
- qrCodeFragment,
- "cameraPermissionRequest",
- cameraPermissionRequestSpy,
- )
-
- activity.supportFragmentManager.commitNow {
- add(qrCodeFragment, QrCodeCameraDialogFragmentTest::class.java.simpleName)
- }
- verify { cameraPermissionRequestSpy.launch(Manifest.permission.CAMERA) }
- }
- }
- unmockkStatic(Toast::class)
- }
-}
diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/scan/QRCodeScannerDialogFragmentTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/scan/QRCodeScannerDialogFragmentTest.kt
new file mode 100644
index 0000000000..efc3203659
--- /dev/null
+++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/scan/QRCodeScannerDialogFragmentTest.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.ui.sdc.qrCode.scan
+
+import android.Manifest
+import android.app.Application
+import androidx.core.graphics.toRect
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
+import androidx.test.core.app.ApplicationProvider
+import com.google.mlkit.vision.barcode.BarcodeScanner
+import com.google.mlkit.vision.barcode.BarcodeScannerOptions
+import com.google.mlkit.vision.barcode.BarcodeScanning
+import com.google.mlkit.vision.barcode.common.Barcode
+import com.google.mlkit.vision.interfaces.Detector
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkConstructor
+import io.mockk.mockkStatic
+import io.mockk.runs
+import io.mockk.unmockkConstructor
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import org.junit.After
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.robolectric.Shadows.shadowOf
+import org.smartregister.fhircore.quest.hiltActivityForTestScenario
+import org.smartregister.fhircore.quest.robolectric.RobolectricTest
+import org.smartregister.fhircore.quest.ui.sdc.qrCode.CameraPermissionsDialogFragment
+
+@HiltAndroidTest
+class QRCodeScannerDialogFragmentTest : RobolectricTest() {
+ @get:Rule(order = 0) var hiltAndroidRule = HiltAndroidRule(this)
+
+ private val application = ApplicationProvider.getApplicationContext()
+
+ @Before
+ fun setUp() {
+ hiltAndroidRule.inject()
+ }
+
+ @Before
+ fun setUpQRCodeScannerDialogFragmentMocks() {
+ val barcodeScanner =
+ mockk() {
+ every { detectorType } returns Detector.TYPE_BARCODE_SCANNING
+ every { close() } just runs
+ }
+ mockkStatic(BarcodeScanning::class)
+ every { BarcodeScanning.getClient(any()) } returns barcodeScanner
+ mockkConstructor(CameraPermissionsDialogFragment::class)
+ every {
+ anyConstructed().show(any(), any())
+ } just runs
+ }
+
+ @After
+ fun unSetUpQRCodeScannerDialogFragmentMocks() {
+ unmockkConstructor(CameraPermissionsDialogFragment::class)
+ unmockkStatic(BarcodeScanning::class)
+ }
+
+ @Test
+ fun shouldShowQrCodeCameraPermissionsDialogWhenNoPermission() {
+ shadowOf(application).denyPermissions(Manifest.permission.CAMERA)
+
+ hiltActivityForTestScenario().use { scenario ->
+ scenario.onActivity { activity ->
+ QRCodeScannerDialogFragment()
+ .show(
+ activity.supportFragmentManager,
+ "shouldShowQrCodeCameraPermissionsDialogWhenNoPermission",
+ )
+ }
+ }
+
+ verify {
+ anyConstructed().show(any(), any())
+ }
+ }
+
+ @Test
+ fun shouldSetFragmentWhenBarCodeDetectedWithinBounds() {
+ shadowOf(application).grantPermissions(Manifest.permission.CAMERA)
+
+ var barcodeValueResult: String? = null
+ hiltActivityForTestScenario().use { scenario ->
+ scenario.onActivity { activity ->
+ QRCodeScannerDialogFragment()
+ .show(
+ activity.supportFragmentManager,
+ "shouldShowQrCodeCameraPermissionsDialogWhenNoPermission",
+ )
+ activity.supportFragmentManager.setFragmentResultListener(
+ QRCodeScannerDialogFragment.RESULT_REQUEST_KEY,
+ activity,
+ ) { _, result ->
+ barcodeValueResult = result.getString(QRCodeScannerDialogFragment.RESULT_REQUEST_KEY)!!
+ }
+
+ activity.supportFragmentManager.registerFragmentLifecycleCallbacks(
+ object : FragmentLifecycleCallbacks() {
+ override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
+ super.onFragmentResumed(fm, f)
+ if (f is QRCodeScannerDialogFragment) {
+ val sampleBarcodeWithinBounds = mockk()
+ every { sampleBarcodeWithinBounds.boundingBox } returns
+ f.viewFinderBounds.toRect().apply {
+ bottom -= 20
+ right -= 20
+ }
+ every { sampleBarcodeWithinBounds.rawValue } returns
+ "ad2ae0df-01dd-4e65-a3ee-01e3174b5744"
+
+ f.onQrCodeDetected(sampleBarcodeWithinBounds)
+ }
+ }
+ },
+ false,
+ )
+ }
+ }
+ Assert.assertEquals("ad2ae0df-01dd-4e65-a3ee-01e3174b5744", barcodeValueResult)
+ }
+}
diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/QrCodeScanUtilsTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/QrCodeScanUtilsTest.kt
index 1787e9d8aa..38a7e6eb74 100644
--- a/android/quest/src/test/java/org/smartregister/fhircore/quest/util/QrCodeScanUtilsTest.kt
+++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/util/QrCodeScanUtilsTest.kt
@@ -30,7 +30,7 @@ import org.junit.Rule
import org.junit.Test
import org.smartregister.fhircore.quest.hiltActivityForTestScenario
import org.smartregister.fhircore.quest.robolectric.RobolectricTest
-import org.smartregister.fhircore.quest.ui.sdc.qrCode.QrCodeCameraDialogFragment
+import org.smartregister.fhircore.quest.ui.sdc.qrCode.scan.QRCodeScannerDialogFragment
@HiltAndroidTest
class QrCodeScanUtilsTest : RobolectricTest() {
@@ -39,13 +39,13 @@ class QrCodeScanUtilsTest : RobolectricTest() {
@Before
fun setUp() {
hiltAndroidRule.inject()
- mockkConstructor(QrCodeCameraDialogFragment::class)
+ mockkConstructor(QRCodeScannerDialogFragment::class)
}
@After
override fun tearDown() {
super.tearDown()
- unmockkConstructor(QrCodeCameraDialogFragment::class)
+ unmockkConstructor(QRCodeScannerDialogFragment::class)
}
@Test
@@ -57,13 +57,13 @@ class QrCodeScanUtilsTest : RobolectricTest() {
scenario.onActivity { activity ->
val sampleQrCode = "d84fbd12-4f22-423a-8645-5525504e1bcb"
every {
- anyConstructed()
+ anyConstructed()
.show(any(), QrCodeScanUtils.QR_CODE_SCAN_UTILS_TAG)
} answers
{
activity.supportFragmentManager.setFragmentResult(
- QrCodeCameraDialogFragment.RESULT_REQUEST_KEY,
- bundleOf(QrCodeCameraDialogFragment.RESULT_REQUEST_KEY to sampleQrCode),
+ QRCodeScannerDialogFragment.RESULT_REQUEST_KEY,
+ bundleOf(QRCodeScannerDialogFragment.RESULT_REQUEST_KEY to sampleQrCode),
)
}
QrCodeScanUtils.scanQrCode(activity, onQrCodeScanListener)
diff --git a/docs/engineering/app/configuring/config-types/forms/questionnaire.mdx b/docs/engineering/app/configuring/config-types/forms/questionnaire.mdx
index c71464ab21..225be946ae 100644
--- a/docs/engineering/app/configuring/config-types/forms/questionnaire.mdx
+++ b/docs/engineering/app/configuring/config-types/forms/questionnaire.mdx
@@ -552,7 +552,7 @@ Below is the specific extension for this. The extension is validated in this cla
## QR Code Support
-QR Code widget can be set up for a Questionnaire by adding `qr_code-widget` extension to a QuestionnaireItem
+QR Code widget can be set up for a Questionnaire by adding `qr_code-widget` extension, with url `https://github.com/opensrp/android-fhir/StructureDefinition/qr-code-widget`, to a QuestionnaireItem
```json
{
@@ -560,22 +560,22 @@ QR Code widget can be set up for a Questionnaire by adding `qr_code-widget` exte
"extension": []
}
```
-The QR Code widget can also be configured to allow only setting the QR code and then have the field as readOnly by using `qr-code-entry-mode` extension
+The QR Code widget extension can be configured to take another extension with url `set-only-readonly` that takes in a value `Boolean`, which when set to `true`, the QR code widget only allows set QR code once and thereafter the field would behave as `readOnly`
```json
{
"url": "https://github.com/opensrp/android-fhir/StructureDefinition/qr-code-widget",
"extension": [
{
- "url": "qr-code-entry-mode",
- "valueString": "set-only-readonly"
+ "url": "set-only-readonly",
+ "valueBoolean": true
}
]
}
```
-Other option for the `qr-code-entry-mode` extension is `"normal"` whereby the widget would allow a new scan to repopulate the field
+Normal behaviour of the `qr_code-widget` extension or if the `set-only-readonly` extension has a value of `false`, would be to allow setting QR code multiple times whereby subsequent QR codes would replace current
-The QR code widget supports adding an arbitrary number of QR codes by setting QuestionnaireItem to `"repeats": true`
+The QR code widget supports adding an arbitrary number of QR codes, implemented by showing `+Add QR Code` button. This can be configured by setting QuestionnaireItem with the `qr_code-widget` extension to `"repeats": true`
```json
"repeats": true,
@@ -584,11 +584,12 @@ The QR code widget supports adding an arbitrary number of QR codes by setting Qu
"url": "https://github.com/opensrp/android-fhir/StructureDefinition/qr-code-widget",
"extension": [
{
- "url": "qr-code-entry-mode",
- "valueString": "set-only-readonly"
+ "url": "set-only-readonly",
+ "valueBoolean": true
}
]
}
]
}
```
+The extension's implementation can be found [here](https://github.com/opensrp/fhircore/blob/main/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeViewHolderFactory.kt)
\ No newline at end of file