Skip to content

Commit

Permalink
Fix: Fixes issue with mal-scanning dense QR codes.
Browse files Browse the repository at this point in the history
  • Loading branch information
gstamatop committed Sep 4, 2024
1 parent bf6839d commit 67d2e8b
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package eu.europa.ec.businesslogic.validator

import android.net.Uri
import android.util.Patterns
import com.google.i18n.phonenumbers.PhoneNumberUtil
import eu.europa.ec.businesslogic.controller.log.LogController
Expand All @@ -24,17 +25,21 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext

interface FormValidator {
fun validateForm(form: Form): Flow<FormValidationResult>
fun validateForms(forms: List<Form>): Flow<FormsValidationResult>
fun validateFormFlow(form: Form): Flow<FormValidationResult>
fun validateFormsFlow(forms: List<Form>): Flow<FormsValidationResult>

suspend fun validateForm(form: Form): FormValidationResult
suspend fun validateForms(forms: List<Form>): FormsValidationResult
}

class FormValidatorImpl(
private val logController: LogController
) : FormValidator {

override fun validateForm(form: Form): Flow<FormValidationResult> = flow {
override fun validateFormFlow(form: Form): Flow<FormValidationResult> = flow {
form.inputs.forEach { (rules, value) ->
rules.forEach { rule ->
validateRule(rule, value)?.let {
Expand All @@ -46,7 +51,20 @@ class FormValidatorImpl(
emit(FormValidationResult(isValid = true))
}.flowOn(Dispatchers.IO)

override fun validateForms(forms: List<Form>): Flow<FormsValidationResult> = flow {
override suspend fun validateForm(form: Form): FormValidationResult =
withContext(Dispatchers.IO) {
for (input in form.inputs) {
val (rules, value) = input
for (rule in rules) {
validateRule(rule, value)?.let {
return@withContext it
}
}
}
return@withContext FormValidationResult(isValid = true)
}

override fun validateFormsFlow(forms: List<Form>): Flow<FormsValidationResult> = flow {
val errors = mutableListOf<String>()
var isValid = true
forms.forEach { form ->
Expand All @@ -62,9 +80,32 @@ class FormValidatorImpl(
emit(FormsValidationResult(isValid, errors))
}.flowOn(Dispatchers.IO)

override suspend fun validateForms(forms: List<Form>): FormsValidationResult =
withContext(Dispatchers.IO) {
val errorMessages = mutableListOf<String>()
var allValid = true
for (form in forms) {
for (input in form.inputs) {
val (rules, value) = input
for (rule in rules) {
validateRule(rule, value)?.let {
allValid = false
errorMessages.add(it.message)
}
}
}
}
return@withContext FormsValidationResult(allValid, errorMessages)
}

private fun validateRule(rule: Rule, value: String): FormValidationResult? {
return when (rule) {
is Rule.ValidateEmail -> checkValidationResult(isEmailValid(value), rule.errorMessage)
is Rule.ValidateProjectUrl -> checkValidationResult(
isValidProjectUrl(value),
rule.errorMessage
)

is Rule.ValidatePhoneNumber -> checkValidationResult(
isPhoneNumberValid(value, rule.countryCode),
rule.errorMessage
Expand Down Expand Up @@ -180,6 +221,16 @@ class FormValidatorImpl(
private fun isEmailValid(value: String): Boolean =
value.isNotEmpty() && Patterns.EMAIL_ADDRESS.matcher(value).matches()

private fun isValidProjectUrl(value: String): Boolean {
if (value.isEmpty()) return false
return try {
val uri = Uri.parse(Uri.decode(value))
!uri.scheme.isNullOrEmpty() && !uri.host.isNullOrEmpty() && !uri.query.isNullOrEmpty()
} catch (e: Exception) {
false
}
}

private fun isPhoneNumberValid(value: String, countryCode: String): Boolean {
val phoneNumberUtil = PhoneNumberUtil.getInstance()
return try {
Expand Down Expand Up @@ -301,6 +352,7 @@ data class FormsValidationResult(val isValid: Boolean, val messages: List<String
sealed class Rule(val errorMsg: String) {
data class ValidateNotEmpty(val errorMessage: String) : Rule(errorMessage)
data class ValidateEmail(val errorMessage: String) : Rule(errorMessage)
data class ValidateProjectUrl(val errorMessage: String) : Rule(errorMessage)
data class ValidatePhoneNumber(val errorMessage: String, val countryCode: String) :
Rule(errorMessage)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,71 @@ class TestFormValidator {
)
}

@Test
fun testValidateProjectUrlRule() = coroutineRule.runTest {
val rules = listOf(Rule.ValidateProjectUrl(plainErrorMessage))

// Test with invalid URLs
validateForm(
rules = rules,
value = "",
validationResult = validationError
)
validateForm(
rules = rules,
value = "invalid_url",
validationResult = validationError
)
validateForm(
rules = rules,
value = "123456789",
validationResult = validationError
)
validateForm(
rules = rules,
value = "http://example.com",
validationResult = validationError
)
validateForm(
rules = rules,
value = "https://notarealproject.com/otherpath",
validationResult = validationError
)
validateForm(
rules = rules,
value = "https://notarealproject.com/bad_query_param?",
validationResult = validationError
)
validateForm(
rules = rules,
value = "ftp://projectsite.com",
validationResult = validationError
)

// Test with valid URLs
validateForm(
rules = rules,
value = "mocked_scheme://mocked_host?mocked_query_param=some_value",
validationResult = validationSuccess
)
validateForm(
rules = rules,
value = "mocked_scheme://mocked_host?mocked_query_param1=some_value1&mocked_query_param2=some_value2",
validationResult = validationSuccess
)
validateForm(
rules = rules,
value = "mocked-scheme://mocked-host?mocked-query-param=some-value",
validationResult = validationSuccess
)
validateForm(
rules = rules,
value = "mocked.scheme://mocked.host?mocked.query.param=some.value",
validationResult = validationSuccess
)
}


@Test
fun testValidateEmailRule() = coroutineRule.runTest {
val rules = listOf(Rule.ValidateEmail(plainErrorMessage))
Expand Down Expand Up @@ -627,7 +692,7 @@ class TestFormValidator {
)
)

formValidation.validateForms(forms).collect {
formValidation.validateFormsFlow(forms).collect {
assertEquals(
FormsValidationResult(
false,
Expand Down Expand Up @@ -811,7 +876,7 @@ class TestFormValidator {
value: String,
validationResult: FormValidationResult
) {
formValidation.validateForm(
formValidation.validateFormFlow(
Form(mapOf(rules to value))
).collect {
assertEquals(validationResult, it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import eu.europa.ec.commonfeature.interactor.BiometricInteractor
import eu.europa.ec.commonfeature.interactor.BiometricInteractorImpl
import eu.europa.ec.commonfeature.interactor.DeviceAuthenticationInteractor
import eu.europa.ec.commonfeature.interactor.DeviceAuthenticationInteractorImpl
import eu.europa.ec.commonfeature.interactor.QrScanInteractor
import eu.europa.ec.commonfeature.interactor.QrScanInteractorImpl
import eu.europa.ec.commonfeature.interactor.QuickPinInteractor
import eu.europa.ec.commonfeature.interactor.QuickPinInteractorImpl
import eu.europa.ec.resourceslogic.provider.ResourceProvider
Expand Down Expand Up @@ -63,4 +65,11 @@ fun provideDeviceAuthenticationInteractor(
deviceAuthenticationController: DeviceAuthenticationController
): DeviceAuthenticationInteractor {
return DeviceAuthenticationInteractorImpl(deviceAuthenticationController)
}

@Factory
fun provideQrScanInteractor(
formValidator: FormValidator
): QrScanInteractor {
return QrScanInteractorImpl(formValidator)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 European Commission
*
* Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European
* Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work
* except in compliance with the Licence.
*
* You may obtain a copy of the Licence at:
* https://joinup.ec.europa.eu/software/page/eupl
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the Licence for the specific language
* governing permissions and limitations under the Licence.
*/

package eu.europa.ec.commonfeature.interactor

import eu.europa.ec.businesslogic.validator.FormValidator

interface QrScanInteractor : FormValidator

class QrScanInteractorImpl(
private val formValidator: FormValidator
) : FormValidator by formValidator, QrScanInteractor
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ class PinViewModel(

private fun validateForm(pin: String) {
viewModelScope.launch {
interactor.validateForm(getListOfRules(pin)).collect {
interactor.validateFormFlow(getListOfRules(pin)).collect {
setState {
copy(
validationResult = it,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,20 @@ import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
Expand All @@ -45,6 +48,7 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
Expand All @@ -53,18 +57,27 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import eu.europa.ec.commonfeature.config.QrScanFlow
import eu.europa.ec.commonfeature.config.QrScanUiConfig
import eu.europa.ec.commonfeature.ui.qr_scan.component.QrCodeAnalyzer
import eu.europa.ec.commonfeature.ui.qr_scan.component.qrBorderCanvas
import eu.europa.ec.resourceslogic.R
import eu.europa.ec.uilogic.component.AppIcons
import eu.europa.ec.uilogic.component.ErrorInfo
import eu.europa.ec.uilogic.component.content.ContentScreen
import eu.europa.ec.uilogic.component.content.ContentTitle
import eu.europa.ec.uilogic.component.content.ScreenNavigateAction
import eu.europa.ec.uilogic.component.preview.PreviewTheme
import eu.europa.ec.uilogic.component.preview.ThemeModePreviews
import eu.europa.ec.uilogic.component.utils.SIZE_EXTRA_SMALL
import eu.europa.ec.uilogic.component.utils.SIZE_LARGE
import eu.europa.ec.uilogic.component.utils.SIZE_MEDIUM
import eu.europa.ec.uilogic.component.utils.SIZE_XX_LARGE
import eu.europa.ec.uilogic.component.utils.SPACING_LARGE
import eu.europa.ec.uilogic.component.utils.SPACING_SMALL
import eu.europa.ec.uilogic.component.utils.VSpacer
import eu.europa.ec.uilogic.component.wrap.WrapCard
import eu.europa.ec.uilogic.component.wrap.WrapIcon
import eu.europa.ec.uilogic.extension.openAppSettings
import eu.europa.ec.uilogic.extension.throttledClickable
import eu.europa.ec.uilogic.navigation.CommonScreens
Expand Down Expand Up @@ -152,6 +165,10 @@ private fun Content(
onEventSend(Event.OnQrScanned(resultQr = qrCode))
}
)

VSpacer.Large()

InformativeText(state)
}
}

Expand Down Expand Up @@ -274,4 +291,39 @@ private fun OpenCamera(
}
}
}
}

@Composable
private fun InformativeText(state: State) {
AnimatedVisibility(state.showInformativeText) {
WrapCard {
Row(
modifier = Modifier.padding(all = SPACING_SMALL.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
WrapIcon(AppIcons.Error)
Text(
modifier = Modifier.padding(all = SPACING_SMALL.dp),
text = state.informativeText,
style = MaterialTheme.typography.headlineLarge,
textAlign = TextAlign.Center
)
}
}
}
}

@ThemeModePreviews
@Composable
private fun InformativeTextPreview() {
PreviewTheme {
InformativeText(
state = State(
qrScannedConfig = QrScanUiConfig("title", "subtitle", QrScanFlow.Presentation),
showInformativeText = true,
informativeText = stringResource(R.string.qr_scan_informative_text_presentation_flow)
)
)
}
}
Loading

0 comments on commit 67d2e8b

Please sign in to comment.