diff --git a/registration/registration-services/src/main/java/io/mosip/registration/constants/AuditEvent.java b/registration/registration-services/src/main/java/io/mosip/registration/constants/AuditEvent.java index 937d040a908..c0448fe463c 100644 --- a/registration/registration-services/src/main/java/io/mosip/registration/constants/AuditEvent.java +++ b/registration/registration-services/src/main/java/io/mosip/registration/constants/AuditEvent.java @@ -280,6 +280,13 @@ public enum AuditEvent { MDM_NO_DEVICE_AVAILABLE("REG-EVT-087", USER_EVENT.getCode(), "DEVICE_NOT_FOUND", "No device is available"), MDM_DEVICE_FOUND("REG-EVT-088", USER_EVENT.getCode(), "MDM_DEVICE_FOUND", "Device is found"), + // SDK Quality Evaluation + SDK_QUALITY_SUCCESS("REG-EVT-104", SYSTEM_EVENT.getCode(), "SDK_QUALITY_SUCCESS", "SDK biometric quality evaluation succeeded"), + SDK_QUALITY_FAILED("REG-EVT-105", SYSTEM_EVENT.getCode(), "SDK_QUALITY_FAILED", "SDK biometric quality evaluation failed, registration blocked"), + SDK_FALLBACK_TO_SBI("REG-EVT-106", SYSTEM_EVENT.getCode(), "SDK_FALLBACK_TO_SBI", "SDK unavailable, fell back to SBI quality score"), + SDK_QUALITY_DISABLED("REG-EVT-107", SYSTEM_EVENT.getCode(), "SDK_QUALITY_DISABLED", "SDK quality evaluation disabled, using SBI score"), + NO_VALID_QUALITY_SCORE("REG-EVT-108", SYSTEM_EVENT.getCode(), "NO_VALID_QUALITY_SCORE", "No valid quality score from SDK or SBI, registration blocked"), + REG_DOC_SCAN("REG-EVT-089", USER_EVENT.getCode(), "REG_DOC_SCAN", "Doc: Click of Scan"), REG_DOC_VIEW("REG-EVT-090", USER_EVENT.getCode(), "REG_DOC_VIEW", "Doc: View"), REG_DOC_DELETE("REG-EVT-091", USER_EVENT.getCode(), "REG_DOC_DELETE", "Doc: Delete"), diff --git a/registration/registration-services/src/main/java/io/mosip/registration/constants/RegistrationConstants.java b/registration/registration-services/src/main/java/io/mosip/registration/constants/RegistrationConstants.java index 76befc7dd2b..756f05a9482 100644 --- a/registration/registration-services/src/main/java/io/mosip/registration/constants/RegistrationConstants.java +++ b/registration/registration-services/src/main/java/io/mosip/registration/constants/RegistrationConstants.java @@ -1266,6 +1266,8 @@ private RegistrationConstants() { // flag for quality check with SDK public static final String QUALITY_CHECK_WITH_SDK = "mosip.registration.quality_check_with_sdk"; public static final String UPDATE_SDK_QUALITY_SCORE = "mosip.registration.replace_sdk_quality_score"; + public static final String SDK_QUALITY_TIMEOUT = "mosip.registration.sdk_quality_timeout_ms"; + public static final long SDK_QUALITY_TIMEOUT_DEFAULT_MS = 5000L; // Packet Sync public static final String PACKET_SYNC = "packet_sync"; diff --git a/registration/registration-services/src/main/java/io/mosip/registration/dto/biometric/QualityEvaluationResult.java b/registration/registration-services/src/main/java/io/mosip/registration/dto/biometric/QualityEvaluationResult.java new file mode 100644 index 00000000000..06cc0f22335 --- /dev/null +++ b/registration/registration-services/src/main/java/io/mosip/registration/dto/biometric/QualityEvaluationResult.java @@ -0,0 +1,35 @@ +package io.mosip.registration.dto.biometric; + +/** + * Holds the outcome of a biometric quality evaluation, including the score, + * the source that produced it (SDK or SBI), and whether a fallback occurred. + */ +public class QualityEvaluationResult { + + private final double score; + private final String source; + private final boolean fallback; + + public QualityEvaluationResult(double score, String source, boolean fallback) { + this.score = score; + this.source = source; + this.fallback = fallback; + } + + public double getScore() { + return score; + } + + public String getSource() { + return source; + } + + public boolean isFallback() { + return fallback; + } + + @Override + public String toString() { + return "QualityEvaluationResult{score=" + score + ", source='" + source + "', fallback=" + fallback + "}"; + } +} diff --git a/registration/registration-services/src/main/java/io/mosip/registration/dto/packetmanager/BiometricsDto.java b/registration/registration-services/src/main/java/io/mosip/registration/dto/packetmanager/BiometricsDto.java index c05b52fa34e..1d681f09d0b 100644 --- a/registration/registration-services/src/main/java/io/mosip/registration/dto/packetmanager/BiometricsDto.java +++ b/registration/registration-services/src/main/java/io/mosip/registration/dto/packetmanager/BiometricsDto.java @@ -20,6 +20,7 @@ public class BiometricsDto { private boolean isCaptured; private String subType; private double sdkScore; + private String qualitySource; private String payLoad; private String signature; private String specVersion; diff --git a/registration/registration-services/src/main/java/io/mosip/registration/exception/RegistrationExceptionConstants.java b/registration/registration-services/src/main/java/io/mosip/registration/exception/RegistrationExceptionConstants.java index 1b029f1f174..f8ba85b89a4 100644 --- a/registration/registration-services/src/main/java/io/mosip/registration/exception/RegistrationExceptionConstants.java +++ b/registration/registration-services/src/main/java/io/mosip/registration/exception/RegistrationExceptionConstants.java @@ -91,6 +91,11 @@ public enum RegistrationExceptionConstants { REG_FINGERPRINT_SCANNING_ERROR(RegistrationConstants.USER_REG_FINGERPRINT_CAPTURE_EXP_CODE+"FCS-002", "Exception while scanning fingerprints of the individual"), REG_BIOMETRIC_QUALITY_CHECK_ERROR("REG-BQC-001", "Exception while evaluating the biometrics quality with SDK"), REG_BIOMETRIC_QUALITY_SCORE_RANGE_ERROR("REG-BQC-002", "Exception while evaluating the biometrics quality score"), + REG_SDK_TIMEOUT("REG-BQC-003", "Biometric quality evaluation timed out. Please re-capture biometrics."), + REG_SDK_NULL_RESPONSE("REG-BQC-004", "Invalid biometric quality score received from SDK."), + REG_SDK_RUNTIME_ERROR("REG-BQC-005", "Error occurred during SDK-based quality evaluation. Please retry capture."), + REG_SBI_SCORE_MISSING("REG-BQC-006", "Default biometric quality score unavailable."), + REG_NO_QUALITY_SOURCE("REG-BQC-007", "Biometric quality evaluation is not configured. Registration cannot proceed."), REG_OTP_VALIDATION("REG-OV-001","Erroe while validating the otp"), REG_PACKET_DATE_PARSER_CODE(PACKET_CREATION_EXP_CODE + "TGE-001", "Exception while parsing the date to display in acknowledgement receipt"), REG_PACKET_JSON_VALIDATOR_ERROR_CODE(PACKET_CREATION_EXP_CODE + "PCS-003", "Exception while validating ID json file"), diff --git a/registration/registration-services/src/main/java/io/mosip/registration/service/bio/impl/BioServiceImpl.java b/registration/registration-services/src/main/java/io/mosip/registration/service/bio/impl/BioServiceImpl.java index 48f96cbbc34..518e5596ff8 100644 --- a/registration/registration-services/src/main/java/io/mosip/registration/service/bio/impl/BioServiceImpl.java +++ b/registration/registration-services/src/main/java/io/mosip/registration/service/bio/impl/BioServiceImpl.java @@ -7,6 +7,7 @@ import java.io.InputStream; import java.time.temporal.ValueRange; import java.util.*; +import java.util.concurrent.*; import io.mosip.registration.dto.schema.UiFieldDTO; import io.mosip.registration.enums.Modality; @@ -29,6 +30,7 @@ import io.mosip.registration.context.ApplicationContext; import io.mosip.registration.context.SessionContext; import io.mosip.registration.dto.RegistrationDTO; +import io.mosip.registration.dto.biometric.QualityEvaluationResult; import io.mosip.registration.dto.packetmanager.BiometricsDto; import io.mosip.registration.exception.RegBaseCheckedException; import io.mosip.registration.exception.RegistrationExceptionConstants; @@ -68,6 +70,8 @@ public class BioServiceImpl extends BaseService implements BioService { @Autowired private BIRBuilder birBuilder; + private final ExecutorService sdkExecutor = Executors.newSingleThreadExecutor(); + /** * Gets the registration DTO from session. * @@ -97,16 +101,11 @@ public List captureModality(MDMRequestDto mdmRequestDto) throws R throw new RegBaseCheckedException(RegistrationExceptionConstants.REG_BIOMETRIC_QUALITY_SCORE_RANGE_ERROR.getErrorCode(), RegistrationExceptionConstants.REG_BIOMETRIC_QUALITY_SCORE_RANGE_ERROR.getErrorMessage()); - if (RegistrationConstants.ENABLE.equalsIgnoreCase((String) ApplicationContext.map() - .getOrDefault(RegistrationConstants.QUALITY_CHECK_WITH_SDK, RegistrationConstants.DISABLE))) { - try { - biometricsDto.setSdkScore(getSDKScore(biometricsDto)); - } catch (BiometricException e) { - LOGGER.error("Unable to fetch SDK Score ", e); - throw new RegBaseCheckedException(RegistrationExceptionConstants.REG_BIOMETRIC_QUALITY_CHECK_ERROR.getErrorCode(), - RegistrationExceptionConstants.REG_BIOMETRIC_QUALITY_CHECK_ERROR.getErrorMessage()); - } - } + QualityEvaluationResult result = evaluateQuality(biometricsDto); + biometricsDto.setSdkScore(result.getScore()); + biometricsDto.setQualitySource(result.getSource()); + LOGGER.info("Quality evaluation complete: score={}, source={}, fallback={}", + result.getScore(), result.getSource(), result.isFallback()); list.add(biometricsDto); } } catch (RegBaseCheckedException e) { @@ -161,6 +160,111 @@ public InputStream getStream(MdmBioDevice mdmBioDevice, String modality) throws RegistrationExceptionConstants.MDS_BIODEVICE_NOT_FOUND.getErrorMessage()); } + /** + * Orchestrates biometric quality evaluation across SDK and SBI sources. + * + * Decision rules: + * - SDK disabled in config: use SBI score (AS-03) + * - SDK enabled but provider unbound: fall back to SBI score (AS-02) + * - SDK enabled, provider bound, call fails (timeout/null/exception): block, no fallback + */ + private QualityEvaluationResult evaluateQuality(BiometricsDto biometricsDto) throws RegBaseCheckedException { + boolean sdkEnabled = RegistrationConstants.ENABLE.equalsIgnoreCase( + (String) ApplicationContext.map().getOrDefault( + RegistrationConstants.QUALITY_CHECK_WITH_SDK, RegistrationConstants.DISABLE)); + + if (!sdkEnabled) { + LOGGER.info("SDK quality evaluation disabled, using SBI score for {}", biometricsDto.getBioAttribute()); + return new QualityEvaluationResult(biometricsDto.getQualityScore(), "SBI", false); + } + + BiometricType biometricType = BiometricType + .fromValue(io.mosip.commons.packet.constants.Biometric + .getSingleTypeByAttribute(biometricsDto.getBioAttribute()).name()); + + // Check provider availability before committing to SDK path + Object provider = null; + try { + provider = bioAPIFactory.getBioProvider(biometricType, BiometricFunction.QUALITY_CHECK); + } catch (Exception e) { + LOGGER.warn("SDK provider not available for {}, falling back to SBI", biometricType); + } + + if (provider == null) { + LOGGER.info("SDK provider unbound, falling back to SBI score for {}", biometricsDto.getBioAttribute()); + double sbiScore = biometricsDto.getQualityScore(); + if (sbiScore <= 0) { + LOGGER.error("SBI score also unavailable for {}", biometricsDto.getBioAttribute()); + throw new RegBaseCheckedException( + RegistrationExceptionConstants.REG_SBI_SCORE_MISSING.getErrorCode(), + RegistrationExceptionConstants.REG_SBI_SCORE_MISSING.getErrorMessage()); + } + return new QualityEvaluationResult(sbiScore, "SBI", true); + } + + // SDK provider is bound - call with timeout. Any failure here blocks registration. + long timeoutMs = getSdkTimeoutMs(); + BIR bir = birBuilder.buildBir(biometricsDto, ProcessedLevelType.RAW); + BIR[] birList = new BIR[]{bir}; + + Future future = sdkExecutor.submit(() -> { + try { + Map scoreMap = bioAPIFactory + .getBioProvider(biometricType, BiometricFunction.QUALITY_CHECK) + .getModalityQuality(birList, null); + return (scoreMap != null && scoreMap.get(biometricType) != null) + ? scoreMap.get(biometricType).doubleValue() + : null; + } catch (Exception e) { + LOGGER.error("SDK provider threw during quality check", e); + throw new RuntimeException(e); + } + }); + + try { + Double score = future.get(timeoutMs, TimeUnit.MILLISECONDS); + if (score == null) { + LOGGER.error("SDK returned null quality score for {}", biometricsDto.getBioAttribute()); + throw new RegBaseCheckedException( + RegistrationExceptionConstants.REG_SDK_NULL_RESPONSE.getErrorCode(), + RegistrationExceptionConstants.REG_SDK_NULL_RESPONSE.getErrorMessage()); + } + LOGGER.info("SDK quality score {} received for {}", score, biometricsDto.getBioAttribute()); + return new QualityEvaluationResult(score, "SDK", false); + + } catch (TimeoutException e) { + future.cancel(true); + LOGGER.error("SDK quality evaluation timed out after {}ms for {}", timeoutMs, biometricsDto.getBioAttribute()); + throw new RegBaseCheckedException( + RegistrationExceptionConstants.REG_SDK_TIMEOUT.getErrorCode(), + RegistrationExceptionConstants.REG_SDK_TIMEOUT.getErrorMessage()); + + } catch (ExecutionException e) { + LOGGER.error("SDK runtime error during quality evaluation for {}", biometricsDto.getBioAttribute(), e); + throw new RegBaseCheckedException( + RegistrationExceptionConstants.REG_SDK_RUNTIME_ERROR.getErrorCode(), + RegistrationExceptionConstants.REG_SDK_RUNTIME_ERROR.getErrorMessage()); + + } catch (RegBaseCheckedException e) { + throw e; + + } catch (Exception e) { + LOGGER.error("Unexpected error during SDK quality evaluation", e); + throw new RegBaseCheckedException( + RegistrationExceptionConstants.REG_SDK_RUNTIME_ERROR.getErrorCode(), + RegistrationExceptionConstants.REG_SDK_RUNTIME_ERROR.getErrorMessage()); + } + } + + private long getSdkTimeoutMs() { + String configured = getGlobalConfigValueOf(RegistrationConstants.SDK_QUALITY_TIMEOUT); + try { + return configured != null ? Long.parseLong(configured) : RegistrationConstants.SDK_QUALITY_TIMEOUT_DEFAULT_MS; + } catch (NumberFormatException e) { + return RegistrationConstants.SDK_QUALITY_TIMEOUT_DEFAULT_MS; + } + } + @Override public double getSDKScore(BiometricsDto biometricsDto) throws BiometricException { BiometricType biometricType = BiometricType @@ -196,7 +300,15 @@ public Map getCapturedBiometrics(@NonNull UiFieldDTO fieldDto, capturedContext.put(attribute, true); continue; } - quality = quality + biometricsDto.getQualityScore(); + double effectiveScore = (biometricsDto.getSdkScore() > 0) + ? biometricsDto.getSdkScore() + : biometricsDto.getQualityScore(); + if (effectiveScore <= 0) { + LOGGER.warn("No valid quality score available for attribute {}", attribute); + capturedContext.put(attribute, false); + continue; + } + quality = quality + effectiveScore; capturedAttributes.add(attribute); } //if some attributes are captured, determine capture status based on threshold check